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,733 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Base Synqly Model"""
4
+ import json
5
+ import logging
6
+ import signal
7
+ from abc import ABC
8
+ from typing import Any, Callable, Optional, TypeVar, TYPE_CHECKING, Union
9
+
10
+ if TYPE_CHECKING:
11
+ from synqly.engine.client import SynqlyEngine
12
+ from synqly.management import ProviderConfig
13
+ from synqly.engine.resources import Asset as InventoryAsset, SecurityFinding, Ticket
14
+
15
+ import httpx
16
+ from pydantic import BaseModel, ConfigDict, Field
17
+ from rich.progress import Progress
18
+
19
+ from regscale.core.app.api import Api
20
+ from regscale.core.app.application import Application
21
+ from regscale.core.app.utils.app_utils import create_progress_object, error_and_exit
22
+ from regscale.models.integration_models.synqly_models.connector_types import ConnectorType
23
+ from regscale.models.integration_models.synqly_models.ocsf_mapper import Mapper
24
+ from regscale.models.integration_models.synqly_models.param import Param
25
+ from regscale.models.integration_models.synqly_models.tenants import Tenant
26
+
27
+ S = TypeVar("S", bound="SynqlyModel")
28
+
29
+
30
+ class SynqlyModel(BaseModel, ABC):
31
+ """Class for Synqly integration to add functionality to interact with Synqly via SDK"""
32
+
33
+ model_config = ConfigDict(populate_by_name=True, use_enum_values=True, arbitrary_types_allowed=True)
34
+
35
+ _connector_type: Optional[str] = ""
36
+ tenant: Optional[Tenant] = None
37
+ client: Optional[Any] = None
38
+ connectors: dict = Field(default_factory=dict)
39
+ # defined using the openApi spec on 7/16/2024, this is updated via _get_integrations_and_secrets()
40
+ connector_types: set = Field(default_factory=lambda: set([connector.__str__() for connector in ConnectorType]))
41
+ terminated: Optional[bool] = False
42
+ app: Application = Field(default_factory=Application, alias="app")
43
+ api: Api = Field(default_factory=Api, alias="api")
44
+ logger: logging.Logger = Field(default=logging.getLogger("rich"), alias="logger")
45
+ job_progress: Optional[Progress] = None
46
+ integration: str = ""
47
+ integration_name: str = Field(default="", description="This stores the proper name of the integration for logging.")
48
+ integrations: list = Field(default_factory=list)
49
+ integrations_and_secrets: dict = Field(default_factory=dict)
50
+ integration_config: Any = None
51
+ capabilities: list[str] = Field(default_factory=list)
52
+ auth_object: str = ""
53
+ auth_object_type: str = ""
54
+ config_types: list = Field(default_factory=list)
55
+ mapper: Mapper = Field(default_factory=Mapper, alias="mapper")
56
+ required_secrets: dict[str, list[Param]] = Field(default_factory=dict)
57
+ optional_params: dict[str, list[Param]] = Field(default_factory=dict)
58
+ required_params: dict[str, list[Param]] = Field(default_factory=dict)
59
+ created_integration_objects: list = Field(default_factory=list)
60
+ created_regscale_objects: list = Field(default_factory=list)
61
+ updated_regscale_objects: list = Field(default_factory=list)
62
+ regscale_objects_to_update: list = Field(default_factory=list)
63
+
64
+ def __init__(self: S, connector_type: Optional[str] = None, integration: Optional[str] = None, **kwargs):
65
+ try:
66
+ if connector_type and integration:
67
+ super().__init__(connector_type=connector_type, integration=integration, **kwargs)
68
+ job_progress = create_progress_object()
69
+ self.job_progress = job_progress
70
+ self.logger.info(f"Initializing {connector_type} connector for the {integration} integration...")
71
+ self.integration_name = " ".join(string.title() for string in self.integration.split("_"))
72
+ self._connector_type = connector_type.lower()
73
+ self.connectors = self._get_integrations_and_secrets()
74
+ if self._connector_type not in self.connector_types:
75
+ raise ValueError(
76
+ f"Invalid connector type: {self._connector_type}. "
77
+ f"Please use one of {', '.join(self.connector_types)}."
78
+ )
79
+ self.integrations = self.connectors[self._connector_type]
80
+ self.integration = self._correct_integration_name(integration)
81
+ if self.integration not in self.integrations:
82
+ raise ValueError(
83
+ f"Invalid integration: {self.integration}. Please use one of {', '.join(self.integrations)}."
84
+ )
85
+ # Populate the required secrets and optional params
86
+ self._flatten_secrets()
87
+ # Initialize signal handlers to intercept Ctrl-C and perform cleanup
88
+ signal.signal(signal.SIGINT, lambda sig, frame: self._cleanup_handler())
89
+ signal.signal(signal.SIGTERM, lambda sig, frame: self._cleanup_handler())
90
+ self.logger.info(f"{self._connector_type} connector for {integration} initialized.")
91
+ else:
92
+ # if the connector type and integration are not provided, we need to generate jobs
93
+ super().__init__(**kwargs)
94
+ except Exception as e:
95
+ if connector_type and integration:
96
+ error_and_exit(f"Error creating {connector_type} connector for the {integration} integration: {e}")
97
+ else:
98
+ error_and_exit(f"Error creating {self.__class__.__name__}: {e}")
99
+
100
+ def _correct_integration_name(self, provided_integration: str) -> str:
101
+ """
102
+ Correct the integration name to match the integration case
103
+
104
+ :param str provided_integration: Integration name to correct
105
+ :return: Corrected integration name
106
+ :rtype: str
107
+ """
108
+ for integration in self.integrations:
109
+ if provided_integration.lower() == integration.lower():
110
+ return integration
111
+ return provided_integration
112
+
113
+ def _update_secret_and_param(self, key: str, data: dict, attribute: str) -> None:
114
+ """
115
+ Update the secret and parameter objects
116
+
117
+ :param str key: The key to update
118
+ :param dict data: The data to update
119
+ :param str attribute: The attribute to update
120
+ :rtype: None
121
+ """
122
+ if key == "credential" and any(isinstance(v, dict) for v in data[key].values()):
123
+ for k, v in data[key].items():
124
+ if k == "optional":
125
+ continue
126
+ getattr(self, attribute)[k] = Param(**v)
127
+ elif key != "optional":
128
+ try:
129
+ getattr(self, attribute)[key] = Param(**data[key])
130
+ except Exception as e:
131
+ self.logger.error(f"Error updating {key} in {attribute}: {e}\n{data=}")
132
+
133
+ def _flatten_secrets(self, integration: Optional[str] = None, return_secrets: bool = False) -> Optional[dict]:
134
+ """
135
+ Flatten the secrets for the integration into required and optional parameters
136
+
137
+ :param Optional[str] integration: Integration to flatten secrets for, used during code gen, defaults to None
138
+ :param bool return_secrets: Whether the secrets should be returned, used during code gen, defaults to False
139
+ :return: Params and secrets as a dictionary, if return_secrets is True
140
+ :rtype: Optional[dict]
141
+ """
142
+ if return_secrets:
143
+ self.required_secrets = {}
144
+ self.optional_params = {}
145
+ self.required_params = {}
146
+ for attribute in ["required_secrets", "optional_params", "required_params"]:
147
+ key = f"{self._connector_type}_{self.integration}" if not integration else f"{integration}"
148
+ data = self.integrations_and_secrets[key][attribute]
149
+ for secret in data:
150
+ self._update_secret_and_param(secret, data, attribute)
151
+ if return_secrets:
152
+ return {
153
+ "required_secrets": self.required_secrets,
154
+ "optional_params": self.optional_params,
155
+ "expected_params": self.required_params,
156
+ }
157
+
158
+ def create_config_and_validate_secrets(self, **kwargs) -> Any:
159
+ """
160
+ Create a new config for the integration and validates the secrets using kwargs and init.yaml
161
+
162
+ :raises ModuleNotFoundError: If unable to parse the config object
163
+ :return: The ticketing configuration
164
+ :rtype: Any
165
+ """
166
+ from synqly import management as mgmt
167
+
168
+ # store config if we need to update it, if needed
169
+ config = self.app.config
170
+ # build a dictionary to contain missing secrets
171
+ missing_secrets: dict[str, Param] = {}
172
+ skip_prompts = kwargs.pop("skip_prompts", False)
173
+ config_object_name = "".join([x.title() for x in self.integration.split("_")])
174
+ config_object = getattr(mgmt, f"ProviderConfig_{self._connector_type.title()}{config_object_name}", None)
175
+ if not config_object:
176
+ raise ModuleNotFoundError(f"Unable to find the config object for {self._connector_type}_{self.integration}")
177
+ check_attributes = [self.required_secrets, self.required_params]
178
+ for attribute in check_attributes:
179
+ for secret in attribute:
180
+ if secret != "optional":
181
+ kwargs.update(
182
+ self._update_config_and_kwargs(
183
+ attribute=attribute, # type: ignore
184
+ key=secret,
185
+ config=config,
186
+ skip_prompts=skip_prompts,
187
+ missing_secrets=missing_secrets,
188
+ **kwargs,
189
+ )
190
+ )
191
+ self.app.save_config(config)
192
+ if missing_secrets:
193
+ self.logger.error("Missing required secrets:")
194
+ for secret, data in missing_secrets.items():
195
+ self.logger.error(f"{secret} ({data.expected_type}) - {data.description}")
196
+ error_and_exit("Please provide the required secrets mentioned above.")
197
+ self.integration_config = config_object(
198
+ type=f"{self._connector_type.lower()}_{self.integration.lower()}",
199
+ url=kwargs.get("url"),
200
+ credential=self._get_auth_method(**kwargs),
201
+ )
202
+
203
+ def _get_auth_method(self, **kwargs) -> Any:
204
+ """
205
+ Get the authentication method for the integration
206
+
207
+ :raises ValueError: If unable to find the authentication method
208
+ :return: The authentication method
209
+ :rtype: Any
210
+ """
211
+ from synqly import management as mgmt
212
+
213
+ if auth_object := getattr(mgmt, self.auth_object):
214
+ return auth_object(type=self.auth_object_type, **kwargs)
215
+ else:
216
+ raise ValueError(f"Unable to find the authentication method for {self.integration}")
217
+
218
+ def _update_config_and_kwargs(
219
+ self,
220
+ attribute: dict[str, Param],
221
+ key: str,
222
+ config: Any,
223
+ skip_prompts: bool,
224
+ missing_secrets: dict[str, Param],
225
+ **kwargs,
226
+ ) -> dict:
227
+ """
228
+ Update the config object and keyword arguments
229
+
230
+ :param dict[str, list[Param]] attribute: The attribute to update
231
+ :param str key: The secret to check and update
232
+ :param Any config: The config object to update
233
+ :param bool skip_prompts: Flag to indicate if prompts should be skipped
234
+ :param dict[str, Param] missing_secrets: Dictionary to store missing secrets
235
+ :return: Updated kwargs if the secret is found in the config, rather than kwargs
236
+ :rtype: dict
237
+ """
238
+ if key not in kwargs and not config.get(f"{self._connector_type}_{self.integration}_{key}"):
239
+ if not skip_prompts:
240
+ self.logger.info(f"Enter the {key} for {self.integration}. Description: {attribute[key].description}")
241
+ provided_secret = input(f"{key}: ")
242
+ kwargs[key] = provided_secret
243
+ config[f"{self._connector_type}_{self.integration}_{key}"] = provided_secret
244
+ else:
245
+ missing_secrets[key] = attribute[key]
246
+ # make sure the secret is in the config
247
+ if key in kwargs and not config.get(f"{self._connector_type}_{self.integration}_{key}"):
248
+ config[f"{self._connector_type}_{self.integration}_{key}"] = kwargs[key]
249
+ # make sure the secret is in the kwargs, load it from the config
250
+ if key not in kwargs and config.get(f"{self._connector_type}_{self.integration}_{key}"):
251
+ kwargs[key] = config[f"{self._connector_type}_{self.integration}_{key}"]
252
+ return kwargs
253
+
254
+ @staticmethod
255
+ def _load_from_package() -> dict:
256
+ """
257
+ Load the capabilities.json from the RegScale CLI package
258
+
259
+ :return: The capabilities.json
260
+ :rtype: dict
261
+ """
262
+ import importlib.resources as pkg_resources
263
+
264
+ # check if the filepath exists before trying to open it
265
+ with pkg_resources.open_text("regscale.models.integration_models.synqly_models", "capabilities.json") as file:
266
+ data = json.load(file)
267
+ return data["result"]
268
+
269
+ def _get_integrations_and_secrets(self, return_params: bool = False) -> dict:
270
+ """
271
+ Function to get the integrations and secrets from the API
272
+
273
+ :param bool return_params: Flag to indicate if the params should be returned
274
+ :return: Integrations and secrets
275
+ :rtype: dict
276
+ """
277
+ raw_data = self._load_from_package()
278
+ return self._parse_api_spec_data(raw_data, return_params)
279
+
280
+ def _parse_api_spec_data(self, data: dict, return_params: bool = False) -> dict:
281
+ """
282
+ Function to parse the Synqly OpenAPI spec metadata
283
+
284
+ :param dict data: Data to parse
285
+ :param bool return_params: Flag to indicate if the params should be returned
286
+ :return: Parsed integrations, or parsed data and params if return_params is True
287
+ :rtype: dict
288
+ """
289
+ integrations: dict = {}
290
+ # per Synqly, this is the best way to get all integrations in one place
291
+ parsed_integrations = [integration["id"] for integration in data if "mock" not in integration.get("id", "")]
292
+ self.connector_types = {key.split("_")[0] for key in parsed_integrations}
293
+ total_count = len(parsed_integrations)
294
+ scrubbed_data = {
295
+ integration["id"]: integration for integration in data if integration["id"] in parsed_integrations
296
+ }
297
+ parsed_count = 0
298
+ # check if we are initializing for a specific integration and skip processing the rest of the integrations
299
+ if scrubbed_data.get(f"{self._connector_type}_{self.integration}"):
300
+ key = f"{self._connector_type}_{self.integration}"
301
+ self._build_integration_and_secrets(
302
+ integrations=integrations,
303
+ key=key,
304
+ data=scrubbed_data,
305
+ parsed_count=parsed_count,
306
+ total_count=1,
307
+ )
308
+ else:
309
+ for key in scrubbed_data.keys():
310
+ code_gen = False
311
+ # Split the string at the underscore
312
+ if self._connector_type and self._connector_type.lower() not in key.lower():
313
+ continue
314
+ elif not self._connector_type:
315
+ code_gen = True
316
+ self._build_integration_and_secrets(
317
+ integrations=integrations,
318
+ key=key,
319
+ data=scrubbed_data,
320
+ parsed_count=parsed_count,
321
+ total_count=total_count,
322
+ )
323
+ if code_gen:
324
+ self._connector_type = None
325
+ if return_params:
326
+ return self.integrations_and_secrets
327
+ return integrations
328
+
329
+ def _build_integration_and_secrets(
330
+ self, integrations: dict, key: str, data: dict, parsed_count: int, total_count: int
331
+ ) -> None:
332
+ """
333
+ Function to build the integration and secrets
334
+
335
+ :param dict integrations: Integrations dictionary that will be updated
336
+ :param str key: Integration key in the data dictionary
337
+ :param dict data: Data containing all integrations and their config
338
+ :param int parsed_count: Parsed count, used for logging
339
+ :param int total_count: Total count, used for logging
340
+ """
341
+ self.config_types.append(key)
342
+ self.logger.debug(f"Processing secrets for {key}")
343
+ self.integrations_and_secrets[key] = self._parse_capabilities_params_and_secrets(data, key)
344
+ self.logger.debug(f"Successfully processed secrets for {key}")
345
+ parsed_count += 1
346
+ # Add the item to the dictionary
347
+ if self._connector_type not in integrations:
348
+ integrations[self._connector_type] = []
349
+ integrations[self._connector_type].append(key.replace(f"{self._connector_type}_", ""))
350
+ self.logger.debug(f"Successfully processed {parsed_count}/{total_count} integrations")
351
+
352
+ def _set_optional_flag(self, data: dict, optional: bool) -> dict:
353
+ """
354
+ Function to recursively set the optional flag in the provided dictionary
355
+
356
+ :param dict data: Object to set the optional flag for
357
+ :param bool optional: Flag to indicate if the object is optional
358
+ :return: Updated dictionary object with the optional flag set
359
+ :rtype: dict
360
+ """
361
+ # Check if there is a nested dict and set the optional flag for the nested dict
362
+ for key, value in data.items():
363
+ if isinstance(value, dict):
364
+ data[key]["optional"] = data[key].get("optional", optional)
365
+ elif isinstance(value, list):
366
+ for item in value:
367
+ if isinstance(item, dict):
368
+ index = value.index(item)
369
+ value[index] = self._set_optional_flag(item, optional) # type: ignore
370
+ data["optional"] = optional
371
+ return data
372
+
373
+ @staticmethod
374
+ def _select_auth_method(credential_schema: dict) -> dict:
375
+ """
376
+ Function to iterate different auth methods and their required secrets: basic -> token -> oath
377
+
378
+ :param dict credential_schema: Credential schema to select auth method from
379
+ :return: Auth method schema
380
+ :rtype: dict
381
+ """
382
+ for item in credential_schema["oneOf"]:
383
+ if "basic" in item.get("title", "").lower():
384
+ return item
385
+ for item in credential_schema["oneOf"]:
386
+ if "token" in item.get("title", "").lower():
387
+ return item
388
+ for item in credential_schema["oneOf"]:
389
+ if "oath" in item.get("title", "").lower():
390
+ return item
391
+
392
+ def _process_credential_schema(
393
+ self, credential_schema: dict, credentials: dict, integration: Optional[str] = None
394
+ ) -> None:
395
+ """
396
+ Function to process credential schema and update credentials dictionary
397
+
398
+ :param dict credential_schema: Credential schema to update credentials from
399
+ :param dict credentials: Credentials to update
400
+ :param Optional[str] integration: Integration to set authentication type for, defaults to None
401
+ :rtype: None
402
+ """
403
+ if "oneOf" in credential_schema:
404
+ credential_schema = self._select_auth_method(credential_schema) or {}
405
+ required_secrets = credential_schema.get("required", [])
406
+ for key, value in credential_schema.get("properties", {}).items():
407
+ if key == "type" and integration and self.integration.lower() == integration.lower():
408
+ authentication_object = f'{credential_schema["x-synqly-credential"]["type"]}_'
409
+ authentication_object += "OAuthClient" if "o_auth" in value["const"] else value["const"].title()
410
+ self.auth_object = authentication_object
411
+ self.auth_object_type = value["const"]
412
+ elif key != "type" and value.get("nullable", False):
413
+ continue
414
+ elif key != "type":
415
+ value["optional"] = value in required_secrets
416
+ credentials[key] = value
417
+
418
+ def _parse_capabilities_params_and_secrets(self, data: dict, key: Optional[str] = None) -> dict:
419
+ """
420
+ Function to parse the required secrets, params and capabilities from the Synqly metadata, with the provided key
421
+
422
+ :param dict data: Data from the OpenAPI spec
423
+ :param Optional[str] key: The schema key to parse the required secrets from, defaults to None
424
+ :raises KeyError: If the schema key is not found in the OpenAPI schema
425
+ :raises ValueError: If no 'credential' property is found for the schema key
426
+ :return: Dictionary of the required secrets
427
+ :rtype: dict
428
+ """
429
+ if key is None:
430
+ raise KeyError(f"Key '{key}' not found in the JSON schema.")
431
+
432
+ schema = data[key]
433
+ if schema.get("provider_config") is None:
434
+ raise ValueError(f"No 'provider_config' found for key '{key}'.")
435
+
436
+ if schema["provider_config"].get("properties") is None:
437
+ raise ValueError(f"No 'properties' found in the 'provider_config' for key '{key}'.")
438
+
439
+ operations = schema.get("operations", [])
440
+ capabilities = [item["name"] for item in operations if item.get("supported")]
441
+ capabilities_params = list(
442
+ field
443
+ for item in operations
444
+ if item.get("supported") and "required_fields" in item.keys()
445
+ for field in item.get("required_fields", [])
446
+ )
447
+ if self.integration.lower() in key.lower():
448
+ self.capabilities = capabilities
449
+ schema = schema["provider_config"]
450
+
451
+ credentials: dict = {}
452
+ final_creds: dict = {"description": "", "required_params": {}, "optional_params": {}, "required_secrets": {}}
453
+ required: list[str] = schema.get("required", [])
454
+
455
+ for prop_key, prop in schema["properties"].items():
456
+ if prop_key == "type":
457
+ continue
458
+ elif prop_key == "credential":
459
+ # we must remove the connector type from the key by finding the first _ and using the rest of the string
460
+ self._process_credential_schema(
461
+ credential_schema=prop, credentials=credentials, integration=key[key.find("_") + 1 :]
462
+ )
463
+ elif prop_key in required:
464
+ prop["optional"] = False
465
+ credentials[prop_key] = prop
466
+ else:
467
+ if prop.get("nullable", True):
468
+ prop["optional"] = True
469
+ final_creds["optional_params"][prop_key] = prop
470
+ else:
471
+ prop["optional"] = False
472
+ final_creds["required_params"][prop_key] = prop
473
+ self._parse_capability_params(capabilities_params=capabilities_params, integration=key, final_creds=final_creds)
474
+
475
+ final_creds["required_secrets"] = credentials
476
+ final_creds["description"] = schema.get("description", "")
477
+ final_creds["capabilities"] = capabilities
478
+ return {**data[key], **final_creds}
479
+
480
+ @staticmethod
481
+ def _parse_capability_params(capabilities_params: list[str], integration: str, final_creds: dict) -> None:
482
+ """
483
+ Function to parse the capability parameters and determine if they are required or optional
484
+
485
+ :param list[str] capabilities_params: List of capability parameters
486
+ :param str integration: The name of the integration
487
+ :param dict final_creds: The final credentials dictionary to update
488
+ :rtype: None
489
+ """
490
+ for param in capabilities_params:
491
+ if param in ["summary", "creator", "priority", "status"]:
492
+ continue
493
+ prop = {
494
+ "description": f'{integration[integration.find("_") + 1:]} {" ".join(param.split("_"))}',
495
+ "type": "string",
496
+ "optional": False,
497
+ }
498
+ final_creds["required_params"][param] = prop
499
+
500
+ @staticmethod
501
+ def create_name_safe_string(tenant_name: str, replace_char: Optional[str] = "-") -> str:
502
+ """
503
+ Function to create a friendly Synqly tenant name
504
+
505
+ :param str tenant_name: The original string to convert
506
+ :param Optional[str] replace_char: The character to replace unsafe characters with, defaults to "-"
507
+ :return: Safe tenant name
508
+ :rtype: str
509
+ """
510
+ for unsafe_char in [".", " ", "/", ":"]:
511
+ tenant_name = tenant_name.replace(unsafe_char, replace_char)
512
+ return tenant_name.lower()
513
+
514
+ def _cleanup_handler(self):
515
+ """
516
+ Deletes resources created by the connector and integration
517
+ """
518
+ if self.tenant:
519
+ self.logger.info(f"\nCleaning up {self._connector_type} connector resources...")
520
+ self.tenant.management_client.accounts.delete(self.tenant.account_id)
521
+ self.logger.debug("Cleaned up Account " + self.tenant.account_id)
522
+ self.terminated = True
523
+ self.logger.info("Cleanup complete.")
524
+
525
+ def get_or_create_tenant(self, synqly_org_token: str, new_tenant_name: str):
526
+ """
527
+ Adds a new "tenant" to the App. A tenant represents a user or
528
+ organization within your application.
529
+
530
+ :param str synqly_org_token: The Synqly Organization token
531
+ :param str new_tenant_name: The name of the new tenant
532
+ :raises ValueError: If the tenant already exists
533
+ """
534
+ from synqly import management as mgmt
535
+ from synqly.management.client import SynqlyManagement
536
+
537
+ # configure a custom httpx_client so that all errors are retried
538
+ transport = httpx.HTTPTransport(retries=3)
539
+
540
+ # this creates a httpx logger
541
+ management_client = SynqlyManagement(
542
+ token=synqly_org_token,
543
+ httpx_client=httpx.Client(transport=transport),
544
+ )
545
+ # Get the httpx logger and set the logging level to CRITICAL in order to suppress all lower level log messages
546
+ httpx_logger = logging.getLogger("httpx")
547
+ httpx_logger.setLevel(logging.CRITICAL)
548
+
549
+ # Each tenant needs an associated Account in Synqly, so we create that now.
550
+
551
+ account_request = mgmt.CreateAccountRequest(name=new_tenant_name)
552
+ try:
553
+ account_response = management_client.accounts.create(request=account_request)
554
+ account_id = account_response.result.account.id
555
+ except Exception as ex:
556
+ existing_accounts = management_client.accounts.list(filter=f"name[eq]{new_tenant_name}")
557
+ account_id = [account.id for account in existing_accounts.result if account.fullname == new_tenant_name]
558
+ if not account_id:
559
+ raise ValueError("Failed to create account: " + str(ex))
560
+ account_id = account_id[0] # type: ignore
561
+
562
+ self.tenant = Tenant(
563
+ tenant_name=new_tenant_name,
564
+ account_id=account_id,
565
+ management_client=management_client,
566
+ engine_client=None,
567
+ )
568
+
569
+ def configure_integration(self, tenant_name: str, provider_config: "ProviderConfig", retries: int = 3):
570
+ """
571
+ Configures a Synqly Integration for a tenant
572
+
573
+ :param str tenant_name: The name of the tenant
574
+ :param ProviderConfig provider_config: The provider configuration
575
+ :param int retries: The number of retries to attempt to create or recreate the integration
576
+ :raises RuntimeError: If unable to create the Integration after 3 attempts
577
+ :raises ValueError: If unable to find an existing Integration for the provided tenant_name
578
+ """
579
+ from synqly import management as mgmt
580
+ from synqly.engine.client import SynqlyEngine
581
+
582
+ # check retries
583
+ if retries == 0:
584
+ raise RuntimeError("Failed to create Integration after 3 attempts")
585
+ # Use the Management API to create a Synqly Integration
586
+ integration_name = f"{tenant_name}-integration"
587
+ integration_req = mgmt.CreateIntegrationRequest(
588
+ name=integration_name,
589
+ provider_config=provider_config,
590
+ )
591
+ # try to create it, if there is an error see if it already exists
592
+ try:
593
+ integration_resp = self.tenant.management_client.integrations.create(
594
+ account_id=self.tenant.account_id, request=integration_req
595
+ )
596
+ # Add Synqly Engine client to the Tenant for use in the background job
597
+ self.tenant.engine_client = SynqlyEngine(
598
+ token=integration_resp.result.token.access.secret,
599
+ )
600
+ self.client = self.tenant.engine_client
601
+ self.logger.debug(
602
+ "Created {} Integration '{}' for {}".format(
603
+ integration_resp.result.integration.category,
604
+ integration_resp.result.integration.id,
605
+ tenant_name,
606
+ )
607
+ )
608
+ except Exception as ex:
609
+ existing_integrations = self.tenant.management_client.integrations.list(
610
+ filter=f"name[eq]{integration_name}"
611
+ )
612
+ integrations = [
613
+ integration for integration in existing_integrations.result if integration.name == integration_name
614
+ ]
615
+ if not integrations:
616
+ raise ValueError(f"Failed to create Integration. {ex}")
617
+ for integration in integrations:
618
+ self.logger.debug(
619
+ "Deleting existing %s Integration '%s' for %s.",
620
+ integration.category,
621
+ integration.id,
622
+ tenant_name,
623
+ )
624
+ self.tenant.management_client.integrations.delete(
625
+ account_id=self.tenant.account_id, integration_id=integration.id
626
+ )
627
+ self.logger.debug("Retrying to create Integration, remaining attempts: %i...", retries)
628
+ self.configure_integration(tenant_name, provider_config, retries - 1)
629
+
630
+ def integration_sync(self, *args, **kwargs):
631
+ """
632
+ Method to run the integration sync process
633
+ """
634
+ pass
635
+
636
+ def fetch_integration_data(
637
+ self, func: Callable, **kwargs
638
+ ) -> list[Union["InventoryAsset", "SecurityFinding", "Ticket"]]:
639
+ """
640
+ Fetches data from the integration using the provided function and handles pagination
641
+
642
+ :param Callable func: The function to fetch data from the integration
643
+ :return: The data from the integration
644
+ :rtype: list[Union[InventoryAsset, SecurityFinding, Ticket]]
645
+ """
646
+ query_filter = kwargs.get("filter")
647
+ limit = kwargs.get("limit", 200)
648
+ integration_data: list = []
649
+ fetch_res = func(
650
+ filter=query_filter,
651
+ limit=limit,
652
+ )
653
+ self.logger.info(f"Received {len(fetch_res.result)} record(s) from {self.integration_name}.")
654
+ integration_data.extend(fetch_res.result)
655
+ # check and handle pagination
656
+ if fetch_res.cursor:
657
+ try:
658
+ # fetch.cursor can be an int as a string, or a continuation token
659
+ while int(fetch_res.cursor) == len(integration_data):
660
+ fetch_res = func(
661
+ filter=query_filter,
662
+ limit=limit,
663
+ cursor=fetch_res.cursor,
664
+ )
665
+ integration_data.extend(fetch_res.result)
666
+ except ValueError:
667
+ while fetch_res.cursor:
668
+ fetch_res = func(
669
+ filter=query_filter,
670
+ limit=limit,
671
+ cursor=fetch_res.cursor,
672
+ )
673
+ integration_data.extend(fetch_res.result)
674
+ self.logger.info(f"Received {len(integration_data)} record(s) from {self.integration_name}...")
675
+ self.logger.info(f"Fetched {len(integration_data)} total record(s) from {self.integration_name}...")
676
+ return integration_data
677
+
678
+ def run_integration_sync(self, *args, **kwargs) -> None:
679
+ """
680
+ Runs the sync process for the integration
681
+
682
+ :param dict kwargs: The keyword arguments to pass to the main function
683
+ :raises Exception: If an error occurs during the sync process, but will clean up
684
+ any resources created by the connector and integration
685
+ :rtype: None
686
+ """
687
+ import os
688
+ from regscale import __version__
689
+
690
+ synqly_access_token = os.getenv("SYNQLY_ACCESS_TOKEN") or self.app.config.get("synqlyAccessToken")
691
+ if not synqly_access_token or synqly_access_token == self.app.template.get("synqlyAccessToken"):
692
+ error_and_exit(
693
+ "SYNQLY_ACCESS_TOKEN environment variable and synqlyAccessToken in init.yaml is not set or empty "
694
+ "and is required. Please set it and try again."
695
+ )
696
+ self.create_config_and_validate_secrets(**kwargs)
697
+
698
+ from regscale.validation.record import validate_regscale_object
699
+ from urllib.parse import urlparse
700
+
701
+ regscale_id = kwargs.get("regscale_id") or kwargs.get("regscale_ssp_id") or 0
702
+ regscale_module = "securityplans" if kwargs.get("regscale_ssp_id") else kwargs.get("regscale_module")
703
+
704
+ if not validate_regscale_object(parent_id=regscale_id, parent_module=regscale_module):
705
+ error_and_exit(f"RegScale {regscale_module} ID #{regscale_id} does not exist.")
706
+
707
+ domain_name = self.create_name_safe_string(urlparse(self.app.config.get("domain")).netloc)
708
+ tenant_name = (
709
+ f"regscale-cliv{self.create_name_safe_string(__version__, '-')}-{domain_name}-{self.integration.lower()}"
710
+ )
711
+ try:
712
+ self.get_or_create_tenant(synqly_access_token, tenant_name)
713
+ self.logger.debug(f"{tenant_name} tenant created")
714
+ except Exception as e:
715
+ self.logger.error(f"Error creating Tenant {tenant_name}:" + str(e))
716
+ self._cleanup_handler()
717
+ raise e
718
+
719
+ try:
720
+ self.configure_integration(tenant_name, self.integration_config)
721
+ except Exception as e:
722
+ self.logger.error(f"Error configuring provider integration for Tenant {tenant_name}: " + str(e))
723
+ self._cleanup_handler()
724
+ raise e
725
+
726
+ try:
727
+ self.integration_sync(*args, **kwargs)
728
+ except Exception as e:
729
+ self.logger.error("Error running sync job: " + str(e))
730
+ self._cleanup_handler()
731
+ raise e
732
+ finally:
733
+ self._cleanup_handler()