regscale-cli 6.21.2.0__py3-none-any.whl → 6.28.2.1__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.
Files changed (314) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/api.py +5 -2
  4. regscale/core/app/application.py +36 -6
  5. regscale/core/app/internal/control_editor.py +73 -21
  6. regscale/core/app/internal/evidence.py +727 -204
  7. regscale/core/app/internal/login.py +4 -2
  8. regscale/core/app/internal/model_editor.py +219 -64
  9. regscale/core/app/utils/app_utils.py +86 -12
  10. regscale/core/app/utils/catalog_utils/common.py +1 -1
  11. regscale/core/login.py +21 -4
  12. regscale/core/utils/async_graphql_client.py +363 -0
  13. regscale/core/utils/date.py +77 -1
  14. regscale/dev/cli.py +26 -0
  15. regscale/dev/code_gen.py +109 -24
  16. regscale/dev/version.py +72 -0
  17. regscale/integrations/commercial/__init__.py +30 -2
  18. regscale/integrations/commercial/aws/audit_manager_compliance.py +3908 -0
  19. regscale/integrations/commercial/aws/cli.py +3107 -54
  20. regscale/integrations/commercial/aws/cloudtrail_control_mappings.py +333 -0
  21. regscale/integrations/commercial/aws/cloudtrail_evidence.py +501 -0
  22. regscale/integrations/commercial/aws/cloudwatch_control_mappings.py +357 -0
  23. regscale/integrations/commercial/aws/cloudwatch_evidence.py +490 -0
  24. regscale/integrations/commercial/{amazon → aws}/common.py +71 -19
  25. regscale/integrations/commercial/aws/config_compliance.py +914 -0
  26. regscale/integrations/commercial/aws/conformance_pack_mappings.py +198 -0
  27. regscale/integrations/commercial/aws/control_compliance_analyzer.py +439 -0
  28. regscale/integrations/commercial/aws/evidence_generator.py +283 -0
  29. regscale/integrations/commercial/aws/guardduty_control_mappings.py +340 -0
  30. regscale/integrations/commercial/aws/guardduty_evidence.py +1053 -0
  31. regscale/integrations/commercial/aws/iam_control_mappings.py +368 -0
  32. regscale/integrations/commercial/aws/iam_evidence.py +574 -0
  33. regscale/integrations/commercial/aws/inventory/__init__.py +338 -22
  34. regscale/integrations/commercial/aws/inventory/base.py +107 -5
  35. regscale/integrations/commercial/aws/inventory/resources/analytics.py +390 -0
  36. regscale/integrations/commercial/aws/inventory/resources/applications.py +234 -0
  37. regscale/integrations/commercial/aws/inventory/resources/audit_manager.py +513 -0
  38. regscale/integrations/commercial/aws/inventory/resources/cloudtrail.py +315 -0
  39. regscale/integrations/commercial/aws/inventory/resources/cloudtrail_logs_metadata.py +476 -0
  40. regscale/integrations/commercial/aws/inventory/resources/cloudwatch.py +191 -0
  41. regscale/integrations/commercial/aws/inventory/resources/compute.py +328 -9
  42. regscale/integrations/commercial/aws/inventory/resources/config.py +464 -0
  43. regscale/integrations/commercial/aws/inventory/resources/containers.py +74 -9
  44. regscale/integrations/commercial/aws/inventory/resources/database.py +481 -31
  45. regscale/integrations/commercial/aws/inventory/resources/developer_tools.py +253 -0
  46. regscale/integrations/commercial/aws/inventory/resources/guardduty.py +286 -0
  47. regscale/integrations/commercial/aws/inventory/resources/iam.py +470 -0
  48. regscale/integrations/commercial/aws/inventory/resources/inspector.py +476 -0
  49. regscale/integrations/commercial/aws/inventory/resources/integration.py +175 -61
  50. regscale/integrations/commercial/aws/inventory/resources/kms.py +447 -0
  51. regscale/integrations/commercial/aws/inventory/resources/machine_learning.py +358 -0
  52. regscale/integrations/commercial/aws/inventory/resources/networking.py +390 -67
  53. regscale/integrations/commercial/aws/inventory/resources/s3.py +394 -0
  54. regscale/integrations/commercial/aws/inventory/resources/security.py +268 -72
  55. regscale/integrations/commercial/aws/inventory/resources/securityhub.py +473 -0
  56. regscale/integrations/commercial/aws/inventory/resources/storage.py +288 -29
  57. regscale/integrations/commercial/aws/inventory/resources/systems_manager.py +657 -0
  58. regscale/integrations/commercial/aws/inventory/resources/vpc.py +655 -0
  59. regscale/integrations/commercial/aws/kms_control_mappings.py +288 -0
  60. regscale/integrations/commercial/aws/kms_evidence.py +879 -0
  61. regscale/integrations/commercial/aws/ocsf/__init__.py +7 -0
  62. regscale/integrations/commercial/aws/ocsf/constants.py +115 -0
  63. regscale/integrations/commercial/aws/ocsf/mapper.py +435 -0
  64. regscale/integrations/commercial/aws/org_control_mappings.py +286 -0
  65. regscale/integrations/commercial/aws/org_evidence.py +666 -0
  66. regscale/integrations/commercial/aws/s3_control_mappings.py +356 -0
  67. regscale/integrations/commercial/aws/s3_evidence.py +632 -0
  68. regscale/integrations/commercial/aws/scanner.py +1072 -205
  69. regscale/integrations/commercial/aws/security_hub.py +319 -0
  70. regscale/integrations/commercial/aws/session_manager.py +282 -0
  71. regscale/integrations/commercial/aws/ssm_control_mappings.py +291 -0
  72. regscale/integrations/commercial/aws/ssm_evidence.py +492 -0
  73. regscale/integrations/commercial/jira.py +489 -153
  74. regscale/integrations/commercial/microsoft_defender/defender.py +326 -5
  75. regscale/integrations/commercial/microsoft_defender/defender_api.py +348 -14
  76. regscale/integrations/commercial/microsoft_defender/defender_constants.py +157 -0
  77. regscale/integrations/commercial/qualys/__init__.py +167 -68
  78. regscale/integrations/commercial/qualys/scanner.py +305 -39
  79. regscale/integrations/commercial/sarif/sairf_importer.py +432 -0
  80. regscale/integrations/commercial/sarif/sarif_converter.py +67 -0
  81. regscale/integrations/commercial/sicura/api.py +79 -42
  82. regscale/integrations/commercial/sicura/commands.py +8 -2
  83. regscale/integrations/commercial/sicura/scanner.py +83 -44
  84. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  85. regscale/integrations/commercial/synqly/assets.py +133 -16
  86. regscale/integrations/commercial/synqly/edr.py +2 -8
  87. regscale/integrations/commercial/synqly/query_builder.py +536 -0
  88. regscale/integrations/commercial/synqly/ticketing.py +27 -0
  89. regscale/integrations/commercial/synqly/vulnerabilities.py +165 -28
  90. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  91. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  92. regscale/integrations/commercial/tenablev2/commands.py +146 -5
  93. regscale/integrations/commercial/tenablev2/scanner.py +1 -3
  94. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  95. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  96. regscale/integrations/commercial/wizv2/click.py +191 -76
  97. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  98. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  99. regscale/integrations/commercial/wizv2/compliance_report.py +1592 -0
  100. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  101. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +7 -3
  102. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +92 -89
  103. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  104. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  105. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +66 -9
  106. regscale/integrations/commercial/wizv2/file_cleanup.py +104 -0
  107. regscale/integrations/commercial/wizv2/issue.py +776 -28
  108. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  109. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  110. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  111. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  112. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  113. regscale/integrations/commercial/wizv2/reports.py +243 -0
  114. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  115. regscale/integrations/commercial/wizv2/scanner.py +1031 -441
  116. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  117. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  118. regscale/integrations/commercial/wizv2/variables.py +89 -3
  119. regscale/integrations/compliance_integration.py +1036 -151
  120. regscale/integrations/control_matcher.py +432 -0
  121. regscale/integrations/due_date_handler.py +333 -0
  122. regscale/integrations/milestone_manager.py +291 -0
  123. regscale/integrations/public/__init__.py +14 -0
  124. regscale/integrations/public/cci_importer.py +834 -0
  125. regscale/integrations/public/csam/__init__.py +0 -0
  126. regscale/integrations/public/csam/csam.py +938 -0
  127. regscale/integrations/public/csam/csam_agency_defined.py +179 -0
  128. regscale/integrations/public/csam/csam_common.py +154 -0
  129. regscale/integrations/public/csam/csam_controls.py +432 -0
  130. regscale/integrations/public/csam/csam_poam.py +124 -0
  131. regscale/integrations/public/fedramp/click.py +77 -6
  132. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  133. regscale/integrations/public/fedramp/fedramp_cis_crm.py +675 -289
  134. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  135. regscale/integrations/public/fedramp/poam/scanner.py +75 -7
  136. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  137. regscale/integrations/scanner_integration.py +1961 -430
  138. regscale/models/integration_models/CCI_List.xml +1 -0
  139. regscale/models/integration_models/aqua.py +2 -2
  140. regscale/models/integration_models/cisa_kev_data.json +805 -11
  141. regscale/models/integration_models/flat_file_importer/__init__.py +5 -8
  142. regscale/models/integration_models/nexpose.py +36 -10
  143. regscale/models/integration_models/qualys.py +3 -4
  144. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  145. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +87 -18
  146. regscale/models/integration_models/synqly_models/filter_parser.py +332 -0
  147. regscale/models/integration_models/synqly_models/ocsf_mapper.py +124 -25
  148. regscale/models/integration_models/synqly_models/synqly_model.py +89 -16
  149. regscale/models/locking.py +12 -8
  150. regscale/models/platform.py +4 -2
  151. regscale/models/regscale_models/__init__.py +7 -0
  152. regscale/models/regscale_models/assessment.py +2 -1
  153. regscale/models/regscale_models/catalog.py +1 -1
  154. regscale/models/regscale_models/compliance_settings.py +251 -1
  155. regscale/models/regscale_models/component.py +1 -0
  156. regscale/models/regscale_models/control_implementation.py +236 -41
  157. regscale/models/regscale_models/control_objective.py +74 -5
  158. regscale/models/regscale_models/file.py +2 -0
  159. regscale/models/regscale_models/form_field_value.py +5 -3
  160. regscale/models/regscale_models/inheritance.py +44 -0
  161. regscale/models/regscale_models/issue.py +301 -102
  162. regscale/models/regscale_models/milestone.py +33 -14
  163. regscale/models/regscale_models/organization.py +3 -0
  164. regscale/models/regscale_models/regscale_model.py +310 -73
  165. regscale/models/regscale_models/security_plan.py +4 -2
  166. regscale/models/regscale_models/vulnerability.py +3 -3
  167. regscale/regscale.py +25 -4
  168. regscale/templates/__init__.py +0 -0
  169. regscale/utils/threading/threadhandler.py +20 -15
  170. regscale/validation/record.py +23 -1
  171. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/METADATA +17 -33
  172. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/RECORD +310 -111
  173. tests/core/__init__.py +0 -0
  174. tests/core/utils/__init__.py +0 -0
  175. tests/core/utils/test_async_graphql_client.py +472 -0
  176. tests/fixtures/test_fixture.py +13 -8
  177. tests/regscale/core/test_login.py +171 -4
  178. tests/regscale/integrations/commercial/__init__.py +0 -0
  179. tests/regscale/integrations/commercial/aws/__init__.py +0 -0
  180. tests/regscale/integrations/commercial/aws/test_audit_manager_compliance.py +1304 -0
  181. tests/regscale/integrations/commercial/aws/test_audit_manager_evidence_aggregation.py +341 -0
  182. tests/regscale/integrations/commercial/aws/test_aws_analytics_collector.py +260 -0
  183. tests/regscale/integrations/commercial/aws/test_aws_applications_collector.py +242 -0
  184. tests/regscale/integrations/commercial/aws/test_aws_audit_manager_collector.py +1155 -0
  185. tests/regscale/integrations/commercial/aws/test_aws_cloudtrail_collector.py +534 -0
  186. tests/regscale/integrations/commercial/aws/test_aws_config_collector.py +400 -0
  187. tests/regscale/integrations/commercial/aws/test_aws_developer_tools_collector.py +203 -0
  188. tests/regscale/integrations/commercial/aws/test_aws_guardduty_collector.py +315 -0
  189. tests/regscale/integrations/commercial/aws/test_aws_iam_collector.py +458 -0
  190. tests/regscale/integrations/commercial/aws/test_aws_inspector_collector.py +353 -0
  191. tests/regscale/integrations/commercial/aws/test_aws_inventory_integration.py +530 -0
  192. tests/regscale/integrations/commercial/aws/test_aws_kms_collector.py +919 -0
  193. tests/regscale/integrations/commercial/aws/test_aws_machine_learning_collector.py +237 -0
  194. tests/regscale/integrations/commercial/aws/test_aws_s3_collector.py +722 -0
  195. tests/regscale/integrations/commercial/aws/test_aws_scanner_integration.py +722 -0
  196. tests/regscale/integrations/commercial/aws/test_aws_securityhub_collector.py +792 -0
  197. tests/regscale/integrations/commercial/aws/test_aws_systems_manager_collector.py +918 -0
  198. tests/regscale/integrations/commercial/aws/test_aws_vpc_collector.py +996 -0
  199. tests/regscale/integrations/commercial/aws/test_cli_evidence.py +431 -0
  200. tests/regscale/integrations/commercial/aws/test_cloudtrail_control_mappings.py +452 -0
  201. tests/regscale/integrations/commercial/aws/test_cloudtrail_evidence.py +788 -0
  202. tests/regscale/integrations/commercial/aws/test_config_compliance.py +298 -0
  203. tests/regscale/integrations/commercial/aws/test_conformance_pack_mappings.py +200 -0
  204. tests/regscale/integrations/commercial/aws/test_control_compliance_analyzer.py +375 -0
  205. tests/regscale/integrations/commercial/aws/test_datetime_parsing.py +223 -0
  206. tests/regscale/integrations/commercial/aws/test_evidence_generator.py +386 -0
  207. tests/regscale/integrations/commercial/aws/test_guardduty_control_mappings.py +564 -0
  208. tests/regscale/integrations/commercial/aws/test_guardduty_evidence.py +1041 -0
  209. tests/regscale/integrations/commercial/aws/test_iam_control_mappings.py +718 -0
  210. tests/regscale/integrations/commercial/aws/test_iam_evidence.py +1375 -0
  211. tests/regscale/integrations/commercial/aws/test_kms_control_mappings.py +656 -0
  212. tests/regscale/integrations/commercial/aws/test_kms_evidence.py +1163 -0
  213. tests/regscale/integrations/commercial/aws/test_ocsf_mapper.py +370 -0
  214. tests/regscale/integrations/commercial/aws/test_org_control_mappings.py +546 -0
  215. tests/regscale/integrations/commercial/aws/test_org_evidence.py +1240 -0
  216. tests/regscale/integrations/commercial/aws/test_s3_control_mappings.py +672 -0
  217. tests/regscale/integrations/commercial/aws/test_s3_evidence.py +987 -0
  218. tests/regscale/integrations/commercial/aws/test_scanner_evidence.py +373 -0
  219. tests/regscale/integrations/commercial/aws/test_security_hub_config_filtering.py +539 -0
  220. tests/regscale/integrations/commercial/aws/test_session_manager.py +516 -0
  221. tests/regscale/integrations/commercial/aws/test_ssm_control_mappings.py +588 -0
  222. tests/regscale/integrations/commercial/aws/test_ssm_evidence.py +735 -0
  223. tests/regscale/integrations/commercial/conftest.py +28 -0
  224. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  225. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  226. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  227. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  228. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  229. tests/regscale/integrations/commercial/test_aws.py +3742 -0
  230. tests/regscale/integrations/commercial/test_burp.py +48 -0
  231. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  232. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  233. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  234. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  235. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  236. tests/regscale/integrations/commercial/test_jira.py +2204 -0
  237. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  238. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  239. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  240. tests/regscale/integrations/commercial/test_sicura.py +349 -0
  241. tests/regscale/integrations/commercial/test_snow.py +423 -0
  242. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  243. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  244. tests/regscale/integrations/commercial/test_stig.py +33 -0
  245. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  246. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  247. tests/regscale/integrations/commercial/test_wiz.py +1365 -0
  248. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  249. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  250. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  251. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  252. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  253. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  254. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  255. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  256. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  257. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  258. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  259. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  260. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  261. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  262. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  263. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  264. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  265. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  266. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  267. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  268. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  269. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  270. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  271. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  272. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  273. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  274. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
  275. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  276. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  277. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  278. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  279. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  280. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1218 -0
  281. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
  282. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  283. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  284. tests/regscale/integrations/public/__init__.py +0 -0
  285. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  286. tests/regscale/integrations/public/fedramp/test_gen_asset_list.py +150 -0
  287. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  288. tests/regscale/integrations/public/test_alienvault.py +220 -0
  289. tests/regscale/integrations/public/test_cci.py +1053 -0
  290. tests/regscale/integrations/public/test_cisa.py +1021 -0
  291. tests/regscale/integrations/public/test_emass.py +518 -0
  292. tests/regscale/integrations/public/test_fedramp.py +1152 -0
  293. tests/regscale/integrations/public/test_fedramp_cis_crm.py +3661 -0
  294. tests/regscale/integrations/public/test_file_uploads.py +506 -0
  295. tests/regscale/integrations/public/test_oscal.py +453 -0
  296. tests/regscale/integrations/test_compliance_status_mapping.py +406 -0
  297. tests/regscale/integrations/test_control_matcher.py +1421 -0
  298. tests/regscale/integrations/test_control_matching.py +155 -0
  299. tests/regscale/integrations/test_milestone_manager.py +408 -0
  300. tests/regscale/models/test_control_implementation.py +118 -3
  301. tests/regscale/models/test_form_field_value_integration.py +304 -0
  302. tests/regscale/models/test_issue.py +378 -1
  303. tests/regscale/models/test_module_integration.py +582 -0
  304. tests/regscale/models/test_tenable_integrations.py +811 -105
  305. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3057
  306. regscale/integrations/public/fedramp/mappings/fedramp_r4_parts.json +0 -7388
  307. regscale/integrations/public/fedramp/mappings/fedramp_r5_parts.json +0 -9605
  308. regscale/integrations/public/fedramp/parts_mapper.py +0 -107
  309. /regscale/integrations/commercial/{amazon → sarif}/__init__.py +0 -0
  310. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  311. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/LICENSE +0 -0
  312. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/WHEEL +0 -0
  313. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/entry_points.txt +0 -0
  314. {regscale_cli-6.21.2.0.dist-info → regscale_cli-6.28.2.1.dist-info}/top_level.txt +0 -0
regscale/core/login.py CHANGED
@@ -6,6 +6,7 @@ from os import getenv
6
6
  from typing import Optional, Tuple
7
7
  from urllib.parse import urljoin
8
8
 
9
+ from requests.exceptions import HTTPError
9
10
  from regscale.core.app.api import Api
10
11
  from regscale.core.app.utils.app_utils import error_and_exit
11
12
 
@@ -14,10 +15,11 @@ logger = logging.getLogger("regscale")
14
15
 
15
16
  def get_regscale_token(
16
17
  api: Api,
17
- username: str = getenv("REGSCALE_USER"),
18
- password: str = getenv("REGSCALE_PASSWORD"),
19
- domain: str = getenv("REGSCALE_DOMAIN"),
18
+ username: Optional[str] = getenv("REGSCALE_USER"),
19
+ password: Optional[str] = getenv("REGSCALE_PASSWORD"),
20
+ domain: Optional[str] = getenv("REGSCALE_DOMAIN"),
20
21
  mfa_token: Optional[str] = "",
22
+ app_id: Optional[int] = 1,
21
23
  ) -> Tuple[str, str]:
22
24
  """
23
25
  Authenticate with RegScale and return a token
@@ -27,6 +29,7 @@ def get_regscale_token(
27
29
  :param str password: a string defaulting to the envar REGSCALE_PASSWORD
28
30
  :param str domain: a string representing the RegScale domain, checks environment REGSCALE_DOMAIN
29
31
  :param Optional[str] mfa_token: MFA token to login with
32
+ :param Optional[int] app_id: The app ID to login with
30
33
  :raises EnvironmentError: if domain is not passed or retrieved
31
34
  :return: a tuple of user_id and auth_token
32
35
  :rtype: Tuple[str, str]
@@ -47,7 +50,19 @@ def get_regscale_token(
47
50
  logger.info("Logging into: %s", domain)
48
51
  # suggest structuring the login paths so that they all exist in one place
49
52
  url = urljoin(domain, "/api/authentication/login")
50
- response = api.post(url=url, json=auth, headers={})
53
+ try:
54
+ # Try to authenticate with the new API version
55
+ auth["appId"] = app_id
56
+ response = api.post(url=url, json=auth, headers={"X-Api-Version": "2.0"})
57
+ if response is None:
58
+ raise HTTPError("No response received from api.post(). Possible connection issue or internal error.")
59
+ response.raise_for_status()
60
+ app_id_compatible = True
61
+ except HTTPError:
62
+ # Fallback to the old API version
63
+ del auth["appId"]
64
+ response = api.post(url=url, json=auth, headers={})
65
+ app_id_compatible = False
51
66
  error_msg = "Unable to authenticate with RegScale. Please check your credentials."
52
67
  if response is None:
53
68
  logger.error("No response received from api.post(). Possible connection issue or internal error.")
@@ -63,4 +78,6 @@ def get_regscale_token(
63
78
  error_and_exit(f"{error_msg}\n{response.status_code}: {response.text}")
64
79
  if isinstance(response_dict, str):
65
80
  response_dict = json.loads(response_dict)
81
+ if app_id_compatible:
82
+ return response_dict["accessToken"]["id"], response_dict["accessToken"]["authToken"]
66
83
  return response_dict["id"], response_dict["auth_token"]
@@ -0,0 +1,363 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Generic async GraphQL client for concurrent query processing in RegScale CLI."""
4
+
5
+ import asyncio
6
+ import logging
7
+ from typing import Any, Callable, Dict, List, Optional
8
+
9
+ import anyio
10
+ from gql import Client, gql
11
+ from gql.transport.aiohttp import AIOHTTPTransport
12
+ from gql.transport.aiohttp import log as aiohttp_logger
13
+
14
+ from regscale.core.app.application import Application
15
+
16
+ logger = logging.getLogger("regscale")
17
+
18
+
19
+ class GraphQLQueryError(Exception):
20
+ """Exception raised when a GraphQL query fails."""
21
+
22
+ pass
23
+
24
+
25
+ class GraphQLAuthenticationError(GraphQLQueryError):
26
+ """Exception raised when GraphQL authentication fails."""
27
+
28
+ pass
29
+
30
+
31
+ class AsyncRegScaleGraphQLClient:
32
+ """
33
+ Generic async GraphQL client optimized for concurrent RegScale API queries.
34
+
35
+ This client can execute multiple GraphQL queries concurrently, significantly
36
+ improving performance when fetching paginated data from RegScale.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ endpoint: str,
42
+ headers: Optional[Dict[str, str]] = None,
43
+ timeout: float = 30.0,
44
+ max_concurrent: int = 5,
45
+ token_refresh_callback: Optional[Callable[[], str]] = None,
46
+ ):
47
+ """
48
+ Initialize the async GraphQL client.
49
+
50
+ :param str endpoint: GraphQL endpoint URL
51
+ :param Optional[Dict[str, str]] headers: HTTP headers for requests
52
+ :param float timeout: Request timeout in seconds
53
+ :param int max_concurrent: Maximum concurrent requests
54
+ :param Optional[Callable[[], str]] token_refresh_callback: Callback to refresh auth token
55
+ """
56
+ self.app = Application()
57
+ self.ssl_verify = self.app.config.get("sslVerify", True)
58
+ self.endpoint = endpoint
59
+ self.headers = headers or {"Authorization": self.app.config.get("token")}
60
+ self.timeout = timeout
61
+ self.max_concurrent = max_concurrent
62
+ self.token_refresh_callback = token_refresh_callback
63
+ self._semaphore = anyio.Semaphore(max_concurrent)
64
+
65
+ # Set logging level for aiohttp transport
66
+ aiohttp_logger.setLevel(logging.CRITICAL)
67
+
68
+ def _create_client(self) -> Client:
69
+ """
70
+ Create a new GQL client with transport.
71
+
72
+ Each concurrent request needs its own transport and client to avoid
73
+ "Transport is already connected" errors.
74
+
75
+ :return: New GQL Client instance
76
+ :rtype: Client
77
+ """
78
+
79
+ # Create the transport with authentication headers
80
+ # Note: AIOHTTPTransport uses ssl parameter, not verify_ssl
81
+ import ssl as ssl_module
82
+
83
+ ssl_context = None
84
+ if not self.ssl_verify:
85
+ # SECURITY WARNING: SSL verification is intentionally disabled
86
+ # This is required for environments with self-signed certificates or corporate proxies
87
+ # where SSL verification cannot be performed. This should only be used when:
88
+ # 1. Working with internal/trusted networks with self-signed certificates
89
+ # 2. Behind corporate proxies that intercept SSL/TLS traffic
90
+ # 3. Development/testing environments
91
+ # DO NOT use in production with untrusted networks
92
+ logger.warning(
93
+ "SSL certificate verification is disabled. "
94
+ "This is insecure and should only be used in controlled environments "
95
+ "with self-signed certificates or corporate proxies."
96
+ )
97
+ # Use default context but disable verification for compatibility
98
+ # The default context still uses secure TLS 1.2+ protocols
99
+ ssl_context = ssl_module.create_default_context() # NOSONAR - Uses TLS 1.2+ by default in Python 3.9+
100
+ ssl_context.check_hostname = False # NOSONAR - Intentionally disabled when sslVerify=false is configured
101
+ ssl_context.verify_mode = ssl_module.CERT_NONE # NOSONAR - Required for self-signed certs/corporate proxies
102
+
103
+ transport = AIOHTTPTransport(
104
+ url=self.endpoint,
105
+ headers=self.headers,
106
+ timeout=int(self.timeout),
107
+ ssl=ssl_context if not self.ssl_verify else True,
108
+ )
109
+
110
+ # Create and return a new GQL client
111
+ return Client(
112
+ transport=transport,
113
+ fetch_schema_from_transport=False, # Skip schema introspection for performance
114
+ )
115
+
116
+ def _is_auth_error(self, error_msg: str) -> bool:
117
+ """
118
+ Check if an error message indicates an authentication issue.
119
+
120
+ :param str error_msg: Error message to check
121
+ :return: True if the error is authentication-related
122
+ :rtype: bool
123
+ """
124
+ auth_indicators = ["AUTH_NOT_AUTHENTICATED", "UNAUTHENTICATED", "401", "403"]
125
+ return any(indicator in error_msg for indicator in auth_indicators)
126
+
127
+ async def _execute_single_attempt(
128
+ self, query: str, variables: Optional[Dict[str, Any]], progress_callback: Optional[Callable], task_name: str
129
+ ) -> Dict[str, Any]:
130
+ """
131
+ Execute a single query attempt.
132
+
133
+ :param str query: GraphQL query string
134
+ :param Optional[Dict[str, Any]] variables: Query variables
135
+ :param Optional[callable] progress_callback: Callback for progress updates
136
+ :param str task_name: Name for progress tracking
137
+ :return: Query response data
138
+ :rtype: Dict[str, Any]
139
+ """
140
+ if progress_callback:
141
+ progress_callback(task_name, "requesting")
142
+
143
+ # Parse the query string to a GraphQL document
144
+ doc = gql(query)
145
+
146
+ # Create a new client for this request to avoid connection conflicts
147
+ client = self._create_client()
148
+
149
+ # Execute the query using the GQL client
150
+ async with client as session:
151
+ result = await session.execute(doc, variable_values=variables or {})
152
+
153
+ if progress_callback:
154
+ progress_callback(task_name, "completed")
155
+
156
+ return result
157
+
158
+ async def execute_query(
159
+ self,
160
+ query: str,
161
+ variables: Optional[Dict[str, Any]] = None,
162
+ progress_callback: Optional[Callable] = None,
163
+ task_name: str = "GraphQL Query",
164
+ ) -> Dict[str, Any]:
165
+ """
166
+ Execute a single GraphQL query asynchronously.
167
+
168
+ :param str query: GraphQL query string
169
+ :param Optional[Dict[str, Any]] variables: Query variables
170
+ :param Optional[callable] progress_callback: Callback for progress updates
171
+ :param str task_name: Name for progress tracking
172
+ :return: Query response data
173
+ :rtype: Dict[str, Any]
174
+ """
175
+ async with self._semaphore: # Limit concurrent requests
176
+ if progress_callback:
177
+ progress_callback(task_name, "starting")
178
+
179
+ logger.debug("Async GraphQL request to %s", self.endpoint)
180
+
181
+ # Try up to 2 times (initial + 1 retry with token refresh)
182
+ max_attempts = 2
183
+ for attempt in range(max_attempts):
184
+ try:
185
+ result = await self._execute_single_attempt(query, variables, progress_callback, task_name)
186
+ return result
187
+
188
+ except Exception as e:
189
+ error_msg = str(e)
190
+
191
+ # Check for authentication errors and retry if possible
192
+ if self._is_auth_error(error_msg):
193
+ if attempt < max_attempts - 1 and self.token_refresh_callback:
194
+ logger.warning("Authentication error, refreshing token and retrying")
195
+ new_token = self.token_refresh_callback()
196
+ self.headers["Authorization"] = f"Bearer {new_token}"
197
+ continue
198
+
199
+ # Log and re-raise other errors
200
+ error_msg = f"Error in {task_name}: {error_msg}"
201
+ logger.error(error_msg)
202
+ if progress_callback:
203
+ progress_callback(task_name, "failed")
204
+ raise GraphQLQueryError(error_msg) from e
205
+
206
+ # Should never reach here, but just in case
207
+ raise GraphQLQueryError(f"Failed to execute query after {max_attempts} attempts")
208
+
209
+ async def execute_paginated_query_concurrent(
210
+ self,
211
+ query_builder: Callable[[int, int], str],
212
+ topic_key: str,
213
+ total_count: int,
214
+ page_size: int = 50,
215
+ starting_skip: int = 0,
216
+ progress_callback: Optional[Callable] = None,
217
+ task_name: str = "Paginated Query",
218
+ ) -> List[Dict[str, Any]]:
219
+ """
220
+ Execute a paginated GraphQL query with concurrent page fetching.
221
+
222
+ :param Callable[[int, int], str] query_builder: Function that builds query with skip and take
223
+ :param str topic_key: Key to extract nodes from response
224
+ :param int total_count: Total number of items expected
225
+ :param int page_size: Items per page (default: 50)
226
+ :param int starting_skip: Starting skip value (default: 0)
227
+ :param Optional[callable] progress_callback: Callback for progress updates
228
+ :param str task_name: Name for progress tracking
229
+ :return: All nodes from all pages
230
+ :rtype: List[Dict[str, Any]]
231
+ """
232
+ # Calculate number of pages needed
233
+ num_pages = (total_count + page_size - 1) // page_size
234
+
235
+ logger.debug("Fetching %d pages concurrently for %s", num_pages, task_name)
236
+
237
+ # Create tasks for all pages
238
+ tasks = []
239
+ for page_num in range(num_pages):
240
+ skip = starting_skip + (page_num * page_size)
241
+ query = query_builder(skip, page_size)
242
+
243
+ page_task_name = f"{task_name} (Page {page_num + 1}/{num_pages})"
244
+ tasks.append(
245
+ self._fetch_single_page(
246
+ query=query,
247
+ variables={},
248
+ topic_key=topic_key,
249
+ page_num=page_num + 1,
250
+ progress_callback=progress_callback,
251
+ task_name=page_task_name,
252
+ )
253
+ )
254
+
255
+ # Execute all page fetches concurrently
256
+ results = await asyncio.gather(*tasks, return_exceptions=True)
257
+
258
+ # Collect all nodes from successful pages
259
+ all_nodes: List[Dict[str, Any]] = []
260
+ for i, result in enumerate(results):
261
+ if isinstance(result, Exception):
262
+ logger.error("Error fetching page %d: %s", i + 1, str(result))
263
+ elif isinstance(result, list):
264
+ all_nodes.extend(result)
265
+
266
+ return all_nodes
267
+
268
+ async def _fetch_single_page(
269
+ self,
270
+ query: str,
271
+ variables: Dict[str, Any],
272
+ topic_key: str,
273
+ page_num: int,
274
+ progress_callback: Optional[Callable] = None,
275
+ task_name: str = "Page",
276
+ ) -> List[Dict[str, Any]]:
277
+ """
278
+ Fetch a single page of results.
279
+
280
+ :param str query: GraphQL query string
281
+ :param Dict[str, Any] variables: Query variables
282
+ :param str topic_key: Key to extract nodes from response
283
+ :param int page_num: Page number for logging
284
+ :param Optional[callable] progress_callback: Callback for progress updates
285
+ :param str task_name: Name for progress tracking
286
+ :return: Nodes from this page
287
+ :rtype: List[Dict[str, Any]]
288
+ """
289
+ try:
290
+ data = await self.execute_query(
291
+ query=query, variables=variables, progress_callback=progress_callback, task_name=task_name
292
+ )
293
+
294
+ topic_data = data.get(topic_key, {})
295
+ nodes = topic_data.get("items", [])
296
+
297
+ # Handle case where nodes is explicitly None
298
+ if nodes is None:
299
+ nodes = []
300
+
301
+ if progress_callback:
302
+ progress_callback(task_name, f"fetched_{len(nodes)}_items")
303
+
304
+ return nodes
305
+
306
+ except Exception as e:
307
+ logger.error("Error fetching page %d: %s", page_num, str(e))
308
+ raise
309
+
310
+
311
+ def run_async_paginated_query(
312
+ endpoint: str,
313
+ headers: Dict[str, str],
314
+ query_builder: Callable[[int, int], str],
315
+ topic_key: str,
316
+ total_count: int,
317
+ page_size: int = 50,
318
+ starting_skip: int = 0,
319
+ max_concurrent: int = 5,
320
+ timeout: int = 60,
321
+ progress_callback: Optional[Callable] = None,
322
+ task_name: str = "Paginated Query",
323
+ token_refresh_callback: Optional[Callable[[], str]] = None,
324
+ ) -> List[Dict[str, Any]]:
325
+ """
326
+ Convenience function to run async paginated query from synchronous code.
327
+
328
+ :param str endpoint: GraphQL endpoint URL
329
+ :param Dict[str, str] headers: HTTP headers
330
+ :param Callable[[int, int], str] query_builder: Function to build query with skip and take
331
+ :param str topic_key: Key to extract data from response
332
+ :param int total_count: Total number of items to fetch
333
+ :param int page_size: Items per page
334
+ :param int starting_skip: Starting skip value
335
+ :param int max_concurrent: Maximum concurrent requests
336
+ :param int timeout: Request timeout in seconds
337
+ :param Optional[callable] progress_callback: Progress callback
338
+ :param str task_name: Task name for progress tracking
339
+ :param Optional[Callable[[], str]] token_refresh_callback: Callback to refresh auth token
340
+ :return: Query results
341
+ :rtype: List[Dict[str, Any]]
342
+ """
343
+
344
+ async def _run():
345
+ client = AsyncRegScaleGraphQLClient(
346
+ endpoint=endpoint,
347
+ headers=headers,
348
+ max_concurrent=max_concurrent,
349
+ timeout=timeout,
350
+ token_refresh_callback=token_refresh_callback,
351
+ )
352
+ return await client.execute_paginated_query_concurrent(
353
+ query_builder=query_builder,
354
+ topic_key=topic_key,
355
+ total_count=total_count,
356
+ page_size=page_size,
357
+ starting_skip=starting_skip,
358
+ progress_callback=progress_callback,
359
+ task_name=task_name,
360
+ )
361
+
362
+ # Use anyio.run for better compatibility
363
+ return anyio.run(_run)
@@ -2,8 +2,10 @@
2
2
  # -*- coding: utf-8 -*-
3
3
  """Utility functions for handling date and datetime conversions"""
4
4
 
5
+ import calendar
5
6
  import datetime
6
7
  import logging
8
+ import re
7
9
  from typing import Any, List, Optional, Union
8
10
 
9
11
 
@@ -39,7 +41,8 @@ def date_str(date_object: Union[str, datetime.datetime, datetime.date, None], da
39
41
  return date_object.strftime(date_format)
40
42
 
41
43
  return date_object.isoformat()
42
- except Exception:
44
+ except (AttributeError, TypeError, ValueError) as e:
45
+ logger.debug(f"Error converting date object to string: {e}")
43
46
  return ""
44
47
 
45
48
 
@@ -82,6 +85,7 @@ def date_obj(date_str: Union[str, datetime.datetime, datetime.date, int, None])
82
85
  def datetime_obj(date_str: Union[str, datetime.datetime, datetime.date, int, None]) -> Optional[datetime.datetime]:
83
86
  """
84
87
  Convert a string, datetime, date, integer, or timestamp string to a datetime object.
88
+ If the day of the month is invalid (e.g., November 31), adjusts to the last valid day of that month.
85
89
 
86
90
  :param Union[str, datetime.datetime, datetime.date, int, None] date_str: The value to convert.
87
91
  :return: The datetime object.
@@ -93,6 +97,10 @@ def datetime_obj(date_str: Union[str, datetime.datetime, datetime.date, int, Non
93
97
  try:
94
98
  return parse(date_str)
95
99
  except ParserError as e:
100
+ # Try to fix invalid day of month (e.g., 2023/11/31 -> 2023/11/30)
101
+ if fixed_date := _fix_invalid_day_of_month(date_str):
102
+ return fixed_date
103
+
96
104
  if date_str and str(date_str).lower() not in ["n/a", "none"]:
97
105
  logger.warning(f"Warning could not parse date string: {date_str}\n{e}")
98
106
  return None
@@ -105,6 +113,74 @@ def datetime_obj(date_str: Union[str, datetime.datetime, datetime.date, int, Non
105
113
  return None
106
114
 
107
115
 
116
+ def _parse_date_components(match_groups: tuple) -> tuple[int, int, int]:
117
+ """
118
+ Parse year, month, day from regex match groups.
119
+
120
+ :param tuple match_groups: Tuple of matched groups from regex
121
+ :return: Tuple of (year, month, day)
122
+ :rtype: tuple[int, int, int]
123
+ """
124
+ if len(match_groups[0]) == 4: # First group is year (YYYY/MM/DD)
125
+ return int(match_groups[0]), int(match_groups[1]), int(match_groups[2])
126
+ # Last group is year (MM/DD/YYYY)
127
+ return int(match_groups[2]), int(match_groups[0]), int(match_groups[1])
128
+
129
+
130
+ def _adjust_invalid_day(year: int, month: int, day: int) -> Optional[datetime.datetime]:
131
+ """
132
+ Adjust invalid day of month to last valid day.
133
+
134
+ :param int year: Year
135
+ :param int month: Month
136
+ :param int day: Day
137
+ :return: Adjusted datetime or None if invalid
138
+ :rtype: Optional[datetime.datetime]
139
+ """
140
+ last_valid_day = calendar.monthrange(year, month)[1]
141
+ if day <= last_valid_day:
142
+ return None
143
+
144
+ logger.warning(f"Invalid day {day} for month {month}/{year}. Adjusting to last valid day: {last_valid_day}")
145
+ try:
146
+ return datetime.datetime(year, month, last_valid_day)
147
+ except ValueError:
148
+ return None
149
+
150
+
151
+ def _fix_invalid_day_of_month(date_str: str) -> Optional[datetime.datetime]:
152
+ """
153
+ Attempt to fix an invalid day of month in a date string.
154
+ For example, 2023/11/31 would become 2023/11/30.
155
+
156
+ :param str date_str: The date string to fix
157
+ :return: A datetime object with a valid day, or None if it can't be fixed
158
+ :rtype: Optional[datetime.datetime]
159
+ """
160
+ try:
161
+ patterns = [
162
+ r"(\d{4})[/-](\d{1,2})[/-](\d{1,2})", # YYYY/MM/DD or YYYY-MM-DD
163
+ r"(\d{1,2})[/-](\d{1,2})[/-](\d{4})", # MM/DD/YYYY or MM-DD-YYYY
164
+ ]
165
+
166
+ for pattern in patterns:
167
+ if not (match := re.search(pattern, date_str)):
168
+ continue
169
+
170
+ year, month, day = _parse_date_components(match.groups())
171
+
172
+ if not (1 <= month <= 12):
173
+ continue
174
+
175
+ if result := _adjust_invalid_day(year, month, day):
176
+ return result
177
+
178
+ return None
179
+ except (ValueError, TypeError, AttributeError, IndexError) as e:
180
+ logger.debug(f"Could not fix invalid day of month for: {date_str} - {e}")
181
+ return None
182
+
183
+
108
184
  def time_str(time_obj: Union[str, datetime.datetime, datetime.time]) -> str:
109
185
  """
110
186
  Convert a datetime/time object to a string.
regscale/dev/cli.py CHANGED
@@ -2,8 +2,11 @@
2
2
 
3
3
  import contextlib
4
4
  import os
5
+ import re
5
6
  import sys
6
7
 
8
+ from pathlib import Path
9
+
7
10
  import click
8
11
  from rich.console import Console
9
12
 
@@ -231,5 +234,28 @@ def update_docs(readme_token: str, confluence_user: str, confluence_token: str,
231
234
  update_confluence(api, confluence_url, root, file)
232
235
 
233
236
 
237
+ @cli.command()
238
+ @click.option("--version", "-v", type=click.STRING, help="The version to upgrade the CLI to use.")
239
+ @click.option("--current", "-c", is_flag=True, help="Get the current version of the CLI.")
240
+ def version(version: str, current: bool) -> None:
241
+ """Manage the version of the regscale-cli package."""
242
+ from regscale.dev.version import (
243
+ get_current_version,
244
+ update_fallback_version_in_version_py,
245
+ update_version_in_pyproject_toml,
246
+ )
247
+
248
+ if current:
249
+ print(get_current_version())
250
+ return
251
+
252
+ if not version:
253
+ print("❌ Please provide a version to upgrade to using the --version flag.")
254
+ return
255
+
256
+ update_version_in_pyproject_toml(version)
257
+ update_fallback_version_in_version_py(version)
258
+
259
+
234
260
  if __name__ == "__main__":
235
261
  cli()