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,321 @@
1
+ """Data model that will be used to validate the input data and that it has the required headers before proceeding."""
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING, Optional, Union
5
+
6
+ if TYPE_CHECKING:
7
+ import pandas as pd
8
+ from regscale.models import Mapping
9
+
10
+ import json
11
+ from pathlib import Path
12
+
13
+ from regscale.exceptions.validation_exception import ValidationException
14
+
15
+ logger = logging.getLogger("regscale")
16
+ JSON = ".json"
17
+ XLSX = ".xlsx"
18
+ XLSM = ".xlsm"
19
+ CSV = ".csv"
20
+ XML = ".xml"
21
+ NESSUS = ".nessus"
22
+
23
+
24
+ class ImportValidater:
25
+ """
26
+ Data importer that validates the input data
27
+
28
+ :param list required_headers: List of required headers (columns)
29
+ :param Union[str, Path] file_path: Path to the file to import
30
+ :raises ValidationException: If the file type is not supported
31
+ """
32
+
33
+ _supported_types = [CSV, XLSX, XLSM, JSON, XML, NESSUS]
34
+ xml_tag = "record"
35
+ keys: Optional[list] = []
36
+ key: Optional[str] = ""
37
+ required_headers: list
38
+ parsed_headers: list
39
+ data: Union["pd.DataFrame", list]
40
+ mapping: "Mapping"
41
+ worksheet_name: Optional[str] = None
42
+ skip_rows: Optional[int] = None
43
+ file_path: Path
44
+ file_type: str
45
+ mapping_file_path: Path
46
+ disable_mapping: bool
47
+
48
+ def __init__(
49
+ self,
50
+ required_headers: list,
51
+ file_path: Union[str, Path],
52
+ mapping_file_path: Union[str, Path],
53
+ disable_mapping: bool = False,
54
+ xml_tag: Optional[str] = None,
55
+ key: Optional[str] = None,
56
+ keys: Optional[list] = None,
57
+ worksheet_name: Optional[str] = None,
58
+ skip_rows: Optional[int] = None,
59
+ prompt: bool = True,
60
+ ignore_unnamed: bool = False,
61
+ ):
62
+ self.ignore_unnamed = ignore_unnamed
63
+ self.prompt = prompt
64
+ self.required_headers = required_headers
65
+ file_path = self._convert_str_to_path(file_path)
66
+ self.mapping_file_path = self._convert_str_to_path(mapping_file_path)
67
+ self.file_path = file_path
68
+ self.file_type = file_path.suffix
69
+ if not self.mapping_file_path.suffix:
70
+ self.mapping_file_path = Path(self.mapping_file_path / f"{self.file_type[1:]}_mapping.json")
71
+ self.disable_mapping = disable_mapping
72
+ self.xml_tag = xml_tag
73
+ self.key = key
74
+ self.keys = keys
75
+ self.worksheet_name = worksheet_name
76
+ self.skip_rows = skip_rows
77
+ if self.file_type not in self._supported_types:
78
+ raise ValidationException(
79
+ f"Unsupported file type: {self.file_type}, supported types are: {', '.join(self._supported_types)}",
80
+ )
81
+ self._parse_headers()
82
+
83
+ @staticmethod
84
+ def _convert_str_to_path(file_path: Union[str, Path]) -> Path:
85
+ """
86
+ Converts a string to a Path object
87
+
88
+ :param Union[str, Path] file_path: File path as a string
89
+ :returns: Path object
90
+ :rtype: Path
91
+ """
92
+ if isinstance(file_path, Path):
93
+ return file_path
94
+ return Path(file_path)
95
+
96
+ def import_data(self) -> Union["pd.DataFrame", list, dict]:
97
+ """
98
+ Imports the data from the file and returns it
99
+
100
+ :returns: Imported data
101
+ :rtype: Union[pd.DataFrame, list, dict]
102
+ """
103
+ if self.file_type == CSV:
104
+ return self.import_csv(self.file_path)
105
+ elif self.file_type == XLSX or self.file_type == XLSM:
106
+ return self.import_xlsx(self.file_path)
107
+ elif self.file_type == JSON:
108
+ return self.import_json(self.file_path)
109
+ elif self.file_type in (XML, NESSUS):
110
+ return self.import_xml(self.file_path, self.xml_tag)
111
+
112
+ def _parse_headers(self):
113
+ """
114
+ Parses the headers from the file based on the file type and returns them
115
+
116
+ :returns: List of headers
117
+ :rtype: list
118
+ """
119
+ self.data = self.import_data()
120
+ if self.file_type in [CSV, XLSX, XLSM]:
121
+ self.parsed_headers = self.data.columns # type: ignore
122
+ elif self.file_type == XML:
123
+ self.parsed_headers = list(self.data.keys())
124
+ elif self.file_type == JSON and not self.parsed_headers:
125
+ raise ValidationException(f"Unable to parse headers from JSON file: {self.file_path}")
126
+
127
+ def validate_headers(self, headers: Union[list, "pd.Index"]) -> None:
128
+ """
129
+ Validates that all required headers are present
130
+
131
+ :param Union[list, pd.Index] headers: List of headers from the file
132
+ """
133
+ import re
134
+
135
+ from regscale.models import Mapping
136
+
137
+ if not self.ignore_unnamed and any(re.search(r"unnamed", header, re.IGNORECASE) for header in headers): # type: ignore
138
+ raise ValidationException(
139
+ f"Unable to parse headers from the file. Please ensure the headers are named in {self.file_path}"
140
+ f"\n{', '.join(headers)}"
141
+ )
142
+
143
+ if not self.prompt:
144
+ # Let's not prompt the user to find the header, and just raise a validation exception
145
+ missing_headers = [header for header in self.required_headers if header not in headers]
146
+ extra_headers = [header for header in headers if header not in self.required_headers]
147
+ if missing_headers:
148
+ raise ValidationException(
149
+ f"{', '.join([f'`{header}`' for header in missing_headers])} header(s) not found in {self.file_path}"
150
+ )
151
+ if extra_headers:
152
+ logger.warning("Extra headers found in the file: %s", ", ".join(extra_headers))
153
+
154
+ if self.disable_mapping:
155
+ logger.debug("Mapping is disabled, using headers as is.")
156
+ self.mapping = Mapping(
157
+ mapping={header: header for header in headers},
158
+ expected_field_names=self.required_headers,
159
+ file_path_for_prompt=self.file_path,
160
+ )
161
+ else:
162
+ logger.debug(f"Mapping is enabled, validating headers for {self.file_path}")
163
+ self.mapping = Mapping.from_file(
164
+ file_path=self.mapping_file_path,
165
+ expected_field_names=self.required_headers,
166
+ mapping={"mapping": {header: header for header in headers}},
167
+ parsed_headers=[header for header in headers], # this converts the pd.Index to a list
168
+ file_path_for_prompt=self.file_path,
169
+ )
170
+
171
+ def import_csv(self, file_path: Union[str, Path]) -> "pd.DataFrame":
172
+ """
173
+ Imports a CSV file and validates headers
174
+
175
+ :param file_path: Path to the CSV file
176
+ :returns: DataFrame with the imported data
177
+ :rtype: pd.DataFrame
178
+ """
179
+ import pandas
180
+
181
+ try:
182
+ if self.skip_rows:
183
+ df = pandas.read_csv(file_path, skiprows=self.skip_rows - 1, on_bad_lines="warn")
184
+ else:
185
+ df = pandas.read_csv(file_path, on_bad_lines="warn")
186
+ except pandas.errors.ParserError:
187
+ raise ValidationException(f"Unable to parse the {CSV} file: {file_path}")
188
+ self.validate_headers(df.columns)
189
+ df = df.fillna("")
190
+ return df
191
+
192
+ def import_xlsx(self, file_path: Union[str, Path]) -> "pd.DataFrame":
193
+ """
194
+ Imports an XLSX file and validates headers
195
+
196
+ :param Union[str, Path] file_path: Path to the XLSX file
197
+ :returns: DataFrame with the imported data
198
+ :rtype: pd.DataFrame
199
+ """
200
+ import pandas
201
+
202
+ try:
203
+ if self.skip_rows and self.worksheet_name:
204
+ df = pandas.read_excel(file_path, sheet_name=self.worksheet_name, skiprows=self.skip_rows - 1)
205
+ elif self.worksheet_name:
206
+ df = pandas.read_excel(file_path, sheet_name=self.worksheet_name)
207
+ elif self.skip_rows:
208
+ df = pandas.read_excel(file_path, skiprows=self.skip_rows)
209
+ else:
210
+ df = pandas.read_excel(file_path)
211
+ except Exception as e:
212
+ raise ValidationException(f"Unable to parse the {XLSX} file: {file_path}\nDetails: {e}")
213
+ self.validate_headers(df.columns)
214
+ df = df.fillna("")
215
+ return df
216
+
217
+ def _handle_keys(self, data: dict) -> list:
218
+ """
219
+ Handles the keys for JSON data
220
+
221
+ :param dict data: JSON data
222
+
223
+ :returns: List of keys
224
+ :rtype: list
225
+ """
226
+ if self.keys:
227
+ value = data
228
+ # iterate each key and see if it is in the data
229
+ for key in self.keys:
230
+ value = value.get(key, {})
231
+ if isinstance(value, dict):
232
+ return list(value.keys())
233
+ elif isinstance(value, list):
234
+ if value:
235
+ return list(value[0].keys())
236
+ else:
237
+ raise ValidationException(
238
+ f"JSON file must contain a key '{self.keys[-1]}' with a list of dictionaries"
239
+ )
240
+
241
+ return data.get(self.key) or data.get(self.keys[0])
242
+
243
+ def import_json(self, file_path: Union[str, Path]) -> list:
244
+ """
245
+ Imports a JSON file and validates keys (treated as headers)
246
+
247
+ :param file_path: Path to the JSON file
248
+ :raises ValidationException: If the JSON file is empty or not a list of dictionaries
249
+ :returns: List of dictionaries with the imported data
250
+ :rtype: list
251
+ """
252
+ with open(file_path, "r", encoding="utf-8") as f:
253
+ data = json.load(f)
254
+
255
+ if isinstance(data, list) and not self.key and not self.keys and data:
256
+ # Assuming JSON is a list of dictionaries
257
+ self.validate_headers(data[0].keys())
258
+ self.parsed_headers = list(data[0].keys())
259
+ elif isinstance(data, list) and self.keys and data:
260
+ # Assuming JSON is a list of dictionaries
261
+ self.validate_headers(self._handle_keys(data[0]))
262
+ self.parsed_headers = list(self._handle_keys(data[0]))
263
+ elif isinstance(data, dict) and (self.key or self.keys):
264
+ if self.keys:
265
+ self.validate_headers(self._handle_keys(data))
266
+ self.parsed_headers = self._handle_keys(data)
267
+ elif findings := data.get(self.key):
268
+ self.validate_headers(findings[0].keys())
269
+ self.parsed_headers = list(findings[0].keys())
270
+ elif isinstance(data, dict):
271
+ self.validate_headers(list(data.keys()))
272
+ self.parsed_headers = list(data.keys())
273
+ else:
274
+ raise ValidationException(f"Unable to parse headers from JSON file: {file_path}")
275
+
276
+ return data
277
+
278
+ def _remove_at_prefix(self, xml_data: Union[list, dict]) -> Union[list, dict]:
279
+ """
280
+ Recursively remove the '@' prefix from keys in a dictionary
281
+
282
+ :param Union[list, dict] xml_data: Parsed XML data, either a dictionary or a list of dictionaries
283
+ :returns: Dictionary with '@' prefix removed or list of dictionaries with '@' prefix removed
284
+ :rtype: Union[list, dict]
285
+ """
286
+ if isinstance(xml_data, dict):
287
+ return {k.lstrip("@"): self._remove_at_prefix(v) for k, v in xml_data.items()}
288
+ elif isinstance(xml_data, list):
289
+ return [self._remove_at_prefix(i) for i in xml_data]
290
+ else:
291
+ return xml_data
292
+
293
+ def import_xml(self, file_path: Union[str, Path], record_tag: Optional[str] = None) -> dict:
294
+ """
295
+ Imports an XML file and validates keys (treated as headers)
296
+
297
+ :param Union[str, Path] file_path: Path to the XML file
298
+ :param str record_tag: The XML tag that represents a record, defaults to "record"
299
+ :raises ValidationException: If the XML file contains no records
300
+ :returns: Dictionary with the imported data
301
+ :rtype: dict
302
+ """
303
+ import xmltodict
304
+
305
+ if isinstance(file_path, str):
306
+ file_path = Path(file_path)
307
+ with open(file_path, "r", encoding="utf-8") as file:
308
+ xml_content = file.read()
309
+ try:
310
+ if dict_content := xmltodict.parse(xml_content):
311
+ if record_tag:
312
+ dict_content = dict_content.get(record_tag)
313
+ dict_content = self._remove_at_prefix(dict_content)
314
+ self.validate_headers(list(dict_content.keys()))
315
+ return dict_content
316
+ else:
317
+ raise ValidationException("XML file contains no records.")
318
+ except xmltodict.expat.ExpatError as e:
319
+ raise ValidationException(
320
+ f"Error parsing {self.file_type.strip('.')} file: {file_path}.\nDetails: {e.args[0]}"
321
+ ) from e
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Pydantic class for custom container scan mappings"""
4
+ import json
5
+ import logging
6
+ from json import JSONDecodeError
7
+ from logging import Logger
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional, Type, Union
10
+
11
+ from pydantic import BaseModel, ConfigDict, field_validator
12
+
13
+ from regscale.core.app import create_logger
14
+ from regscale.core.app.utils.app_utils import check_file_path, error_and_exit, save_data_to
15
+ from regscale.exceptions import ValidationException
16
+
17
+
18
+ class Mapping(BaseModel):
19
+ """
20
+ Pydantic class for custom container scan mappings
21
+ """
22
+
23
+ model_config = ConfigDict(populate_by_name=True, json_encoders={bytes: lambda v: v.decode()})
24
+
25
+ mapping: Dict[str, str]
26
+ # expected field names for validation
27
+ expected_field_names: List[str] = []
28
+ _logger: Optional[Logger] = logging.getLogger("regscale")
29
+ file_path_for_prompt: Optional[Path] = None
30
+
31
+ @classmethod
32
+ def _prompt_for_field(
33
+ cls,
34
+ field: str,
35
+ mapping: Union[dict, "Mapping"],
36
+ parsed_headers: Optional[List[str]] = None,
37
+ file_path_for_prompt: Optional[Path] = None,
38
+ ) -> str:
39
+ """
40
+ Prompt for a field value
41
+
42
+ :param str field: Field to prompt for
43
+ :param Union[dict, "Mapping"] mapping: Mapping object or dictionary of the mapping object
44
+ :param Optional[List[str]] parsed_headers: Parsed headers from file, defaults to None
45
+ :return: Field value
46
+ :rtype: str
47
+ """
48
+ info_msg = f"Field '{field}' not found in mapping, please enter the mapping for this field, enter 'exit' to exit, or enter 'skip' to skip"
49
+ if file_path_for_prompt:
50
+ info_msg += f"\nFile path for headers: {file_path_for_prompt.name}"
51
+ if parsed_headers:
52
+ info_msg += f"\nParsed headers: {parsed_headers}"
53
+ cls._logger.default.info(info_msg)
54
+ custom_mapping = input(f"Enter the mapping for field '{field}':")
55
+ cls._handle_user_input(custom_mapping, field, mapping, parsed_headers, file_path_for_prompt)
56
+
57
+ @classmethod
58
+ def _handle_user_input(
59
+ cls,
60
+ custom_mapping: str,
61
+ field: str,
62
+ mapping: Union[dict, "Mapping"],
63
+ parsed_headers: Optional[List[str]],
64
+ file_path_for_prompt: Optional[Path],
65
+ ) -> None:
66
+ if custom_mapping.lower() in ["exit", "skip"]:
67
+ cls._handle_exit_or_skip(custom_mapping, file_path_for_prompt)
68
+ else:
69
+ cls._confirm_and_update_mapping(custom_mapping, field, mapping, parsed_headers)
70
+
71
+ @classmethod
72
+ def _handle_exit_or_skip(cls, custom_mapping: str, file_path_for_prompt: Optional[Path]) -> None:
73
+ if custom_mapping.lower() == "exit":
74
+ error_and_exit("Exiting...")
75
+ elif custom_mapping.lower() == "skip":
76
+ raise ValidationException(f"Skipping file {file_path_for_prompt.name if file_path_for_prompt else ''}...")
77
+
78
+ @classmethod
79
+ def _confirm_and_update_mapping(
80
+ cls, custom_mapping: str, field: str, mapping: Union[dict, "Mapping"], parsed_headers: Optional[List[str]]
81
+ ) -> None:
82
+ confirm = input(f"Confirm the mapping for field '{field}' is '{custom_mapping}' (y/n)")
83
+ if confirm.lower() == "y":
84
+ cls._validate_and_update_mapping(custom_mapping, field, mapping, parsed_headers)
85
+ elif confirm.lower() in ["exit", "skip"]:
86
+ cls._handle_exit_or_skip(confirm, None)
87
+ else:
88
+ cls._prompt_for_field(field=field, mapping=mapping, parsed_headers=parsed_headers)
89
+
90
+ @classmethod
91
+ def _validate_and_update_mapping(
92
+ cls, custom_mapping: str, field: str, mapping: Union[dict, "Mapping"], parsed_headers: Optional[List[str]]
93
+ ) -> None:
94
+ if parsed_headers and custom_mapping not in parsed_headers:
95
+ cls._logger.default.warning(
96
+ f"Mapping {custom_mapping} is not found in the headers. "
97
+ f"Please select one of the following headers: {parsed_headers}."
98
+ )
99
+ cls._prompt_for_field(field=field, mapping=mapping, parsed_headers=parsed_headers)
100
+ elif custom_mapping not in mapping.values() and not parsed_headers:
101
+ cls._logger.default.warning(
102
+ f"Mapping {custom_mapping} is not found in the headers. "
103
+ f"Please select one of the following headers: {list(mapping.values())}."
104
+ )
105
+ cls._prompt_for_field(field=field, mapping=mapping, parsed_headers=parsed_headers)
106
+ else:
107
+ if isinstance(mapping, dict):
108
+ mapping[field] = custom_mapping
109
+ else:
110
+ mapping.mapping[field] = custom_mapping
111
+
112
+ @field_validator("expected_field_names")
113
+ def validate_mapping(cls: Type["Mapping"], expected_field_names: List[str], values: Dict[str, Any]) -> List[str]:
114
+ """
115
+ Validate the expected field names
116
+
117
+ :param List[str] expected_field_names: Expected field names
118
+ :param Dict[str, Any] values: Values
119
+ :return: Expected field names
120
+ """
121
+ mapping = values.data.get("mapping")
122
+ if mapping is not None and expected_field_names is not None:
123
+ if missing_fields := [field for field in expected_field_names if field not in mapping]:
124
+ for field in missing_fields:
125
+ cls._prompt_for_field(field, mapping, cls.file_path_for_prompt)
126
+ return expected_field_names
127
+
128
+ @field_validator("expected_field_names")
129
+ def validate_expected_field_names(cls: Type["Mapping"], expected_field_names: Any) -> List[str]:
130
+ """
131
+ Validate the expected field names and types
132
+
133
+ :param Any expected_field_names: Expected field names
134
+ :raises ValidationError: If expected_field_names is not a list or if any element in the list is not a string
135
+ :rtype: List[str]
136
+ :return: Expected field names
137
+ """
138
+ if not isinstance(expected_field_names, list):
139
+ raise ValidationException("expected_field_names must be a list")
140
+ if not all(isinstance(field_name, str) for field_name in expected_field_names):
141
+ raise ValidationException("All elements in expected_field_names must be strings")
142
+ return expected_field_names
143
+
144
+ # Add a from file method to load the mapping from a JSON file
145
+ @classmethod
146
+ def from_file(cls, file_path: Path, expected_field_names: List[str], **kwargs) -> "Mapping":
147
+ """
148
+ Load the mapping from a JSON file
149
+
150
+ :param Path file_path: Path to the JSON file
151
+ :param List[str] expected_field_names: Expected field names
152
+ :return: Validated Mapping ob
153
+ :rtype: Mapping
154
+ """
155
+ file_path_for_prompt = kwargs.get("file_path_for_prompt")
156
+ if not file_path.exists() and kwargs.get("mapping"):
157
+ check_file_path(file_path.parent)
158
+ dat = kwargs.get("mapping")
159
+ else:
160
+ with open(file_path, "r") as file:
161
+ try:
162
+ dat = json.load(file)
163
+ # if mapping is not found in the JSON file, check the kwargs for the provided mapping and use that
164
+ if not dat.get("mapping"):
165
+ dat["mapping"] = kwargs.get("mapping")
166
+ if not dat.get("mapping"):
167
+ error_and_exit("Mapping not found in JSON file")
168
+ except JSONDecodeError as jex:
169
+ cls._logger.default.debug(jex)
170
+ error_and_exit("JSON file is badly formatted, please check the file")
171
+ except (ValueError, SyntaxError) as exc:
172
+ error_and_exit(f"Error parsing JSON file: {exc}")
173
+ if parsed_headers := kwargs.get("parsed_headers"):
174
+ cls._verify_parsed_headers(parsed_headers, expected_field_names, dat, file_path_for_prompt)
175
+ mapping = cls(
176
+ mapping=dat["mapping"],
177
+ expected_field_names=expected_field_names,
178
+ file_path_for_prompt=file_path_for_prompt,
179
+ )
180
+ if not mapping:
181
+ error_and_exit("Error loading mapping from file. Exiting...")
182
+ mapping.save_mapping(file_path)
183
+ return mapping
184
+
185
+ @classmethod
186
+ def _verify_parsed_headers(
187
+ cls, parsed_headers: List[str], expected_field_names: List[str], dat: dict, file_path_for_prompt: Optional[Path]
188
+ ) -> None:
189
+ """
190
+ Verify the parsed headers and prompt for missing headers
191
+
192
+ :param List[str] parsed_headers: Parsed headers
193
+ :param List[str] expected_field_names: Expected field names
194
+ :param Dict[str, Any] dat: Data dictionary
195
+ :param Optional[Path] file_path_for_prompt: File path for prompt
196
+ :rtype: None
197
+ """
198
+ if missing_headers := [
199
+ header
200
+ for header in expected_field_names
201
+ if header not in parsed_headers and dat["mapping"].get(header) not in parsed_headers
202
+ ]:
203
+ for header in missing_headers:
204
+ cls._logger.default.info(
205
+ f"Header '{header}' not found in parsed headers in {file_path_for_prompt.name if file_path_for_prompt else 'file'} Please enter the mapping."
206
+ )
207
+ cls._prompt_for_field(field=header, mapping=dat["mapping"], parsed_headers=parsed_headers)
208
+
209
+ def save_mapping(self, file_path: Path) -> None:
210
+ """
211
+ Save the mapping to a JSON file
212
+
213
+ :param Path file_path: Path to save the mapping JSON file
214
+ :rtype: None
215
+ """
216
+ check_file_path(file_path.parent)
217
+ save_data_to(file_path, {"mapping": self.mapping})
218
+
219
+ def get_header(self, key: str) -> str:
220
+ """
221
+ Get the header for a key
222
+
223
+ :param str key: Key to get the header for
224
+ :return: Header for the key
225
+ :rtype: str
226
+ """
227
+ return self.mapping.get(key)
228
+
229
+ def get_value(self, dat: Optional[dict], key: str, default_val: Optional[Any] = "", warnings: bool = True) -> Any:
230
+ """
231
+ Get the value from a dictionary by mapped key
232
+
233
+ :param Optional[dict] dat: Data dictionary, defaults to None
234
+ :param str key: Key to get the value for
235
+ :param Optional[Any] default_val: Default value to return, defaults to empty string
236
+ :param bool warnings: Whether to log warnings, defaults to False
237
+ :return: Value for the key
238
+ :rtype: Any
239
+ """
240
+ # check mapping
241
+ if key == "None" or key is None:
242
+ return default_val
243
+ mapped_key = self.mapping.get(key)
244
+ if not mapped_key and warnings:
245
+ self._logger.warning(f"Value for key '{key}' not found in mapping.")
246
+ if dat and mapped_key:
247
+ val = dat.get(mapped_key)
248
+ if isinstance(val, str):
249
+ return val.strip()
250
+ return val or default_val
251
+ return default_val
252
+
253
+ def to_header(self) -> list[str]:
254
+ """
255
+ Convert the mapping to a header
256
+ :return: Mapping as a header
257
+ :rtype: list[str]
258
+ """
259
+ # convert mapping to a list of strings
260
+ return [f"{value}" for key, value in self.mapping.items()]
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Dataclass for a user's pipeline"""
4
+
5
+ # standard python imports
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+
10
+ @dataclass
11
+ class Pipeline:
12
+ """Pipeline Model"""
13
+
14
+ email: str # Required
15
+ fullName: str = None
16
+ pipelines: Any = None
17
+ totalTasks: int = None
18
+ analyzed: bool = False
19
+ emailed: bool = False
20
+
21
+ def __getitem__(self, key: Any) -> Any:
22
+ """
23
+ Get attribute from Pipeline
24
+ :param Any key:
25
+ :return: value of provided key
26
+ :rtype: Any
27
+ """
28
+ return getattr(self, key)
29
+
30
+ def __setitem__(self, key: Any, value: Any) -> None:
31
+ """
32
+ Set attribute in Pipeline with provided key
33
+ :param Any key: Key to change to provided value
34
+ :param Any value: New value for provided Key
35
+ :rtype: None
36
+ """
37
+ return setattr(self, key, value)