robusta-api 0.47.0__tar.gz → 0.48.0__tar.gz

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 (306) hide show
  1. {robusta_api-0.47.0 → robusta_api-0.48.0}/PKG-INFO +6 -5
  2. {robusta_api-0.47.0 → robusta_api-0.48.0}/pyproject.toml +16 -7
  3. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/_version.py +1 -1
  4. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/discovery/discovery.py +3 -2
  5. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/model/env_vars.py +8 -0
  6. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/reporting/utils.py +4 -4
  7. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/mail/mail_sink.py +4 -4
  8. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/robusta/dal/supabase_dal.py +106 -5
  9. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/transformer.py +96 -59
  10. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/mail/sender.py +4 -0
  11. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/msteams/msteams_adaptive_card_files_image.py +5 -4
  12. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/runner/config_loader.py +2 -1
  13. robusta_api-0.48.0/src/robusta/utils/archive.py +54 -0
  14. {robusta_api-0.47.0 → robusta_api-0.48.0}/LICENSE +0 -0
  15. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/__init__.py +0 -0
  16. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/api/__init__.py +0 -0
  17. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/clients/robusta_client.py +0 -0
  18. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/__init__.py +0 -0
  19. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/discovery/__init__.py +0 -0
  20. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/discovery/resource_names.py +0 -0
  21. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/discovery/top_service_resolver.py +0 -0
  22. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/discovery/utils.py +0 -0
  23. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/exceptions.py +0 -0
  24. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/model/__init__.py +0 -0
  25. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/model/base_params.py +0 -0
  26. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/model/cluster_status.py +0 -0
  27. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/model/events.py +0 -0
  28. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/model/helm_release.py +0 -0
  29. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/model/jobs.py +0 -0
  30. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/model/k8s_operation_type.py +0 -0
  31. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/model/namespaces.py +0 -0
  32. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/model/nodes.py +0 -0
  33. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/model/openshift_group.py +0 -0
  34. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/model/pods.py +0 -0
  35. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/model/runner_config.py +0 -0
  36. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/model/services.py +0 -0
  37. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/persistency/__init__.py +0 -0
  38. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/persistency/in_memory.py +0 -0
  39. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/persistency/scheduled_jobs_states_dal.py +0 -0
  40. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/__init__.py +0 -0
  41. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/actions_registry.py +0 -0
  42. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/base_trigger.py +0 -0
  43. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/common.py +0 -0
  44. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/container_playbook_utils.py +0 -0
  45. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/crash_reporter.py +0 -0
  46. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/generation.py +0 -0
  47. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/internal/CRD_API_DOCUMENTATION.md +0 -0
  48. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/internal/__init__.py +0 -0
  49. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/internal/ai_integration.py +0 -0
  50. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/internal/crds.py +0 -0
  51. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/internal/discovery_events.py +0 -0
  52. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/job_utils.py +0 -0
  53. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/node_enrichment_utils.py +0 -0
  54. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/node_playbook_utils.py +0 -0
  55. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/oom_killer_utils.py +0 -0
  56. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/playbook_utils.py +0 -0
  57. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/playbooks_event_handler.py +0 -0
  58. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/playbooks_event_handler_impl.py +0 -0
  59. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/pod_utils/crashloop_utils.py +0 -0
  60. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/pod_utils/imagepull_utils.py +0 -0
  61. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/pod_utils/pending_pod_utils.py +0 -0
  62. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/prometheus_enrichment_utils.py +0 -0
  63. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/playbooks/trigger.py +0 -0
  64. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/pubsub/__init__.py +0 -0
  65. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/pubsub/event_emitter.py +0 -0
  66. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/pubsub/event_subscriber.py +0 -0
  67. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/pubsub/events_pubsub.py +0 -0
  68. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/reporting/__init__.py +0 -0
  69. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/reporting/action_requests.py +0 -0
  70. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/reporting/base.py +0 -0
  71. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/reporting/blocks.py +0 -0
  72. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/reporting/callbacks.py +0 -0
  73. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/reporting/consts.py +0 -0
  74. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/reporting/custom_rendering.py +0 -0
  75. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/reporting/finding_subjects.py +0 -0
  76. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/reporting/holmes.py +0 -0
  77. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/reporting/url_helpers.py +0 -0
  78. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/schedule/__init__.py +0 -0
  79. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/schedule/model.py +0 -0
  80. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/schedule/scheduler.py +0 -0
  81. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/__init__.py +0 -0
  82. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/common/__init__.py +0 -0
  83. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/common/channel_transformer.py +0 -0
  84. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/common/html_tools.py +0 -0
  85. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/datadog/__init__.py +0 -0
  86. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/datadog/datadog_sink.py +0 -0
  87. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/datadog/datadog_sink_params.py +0 -0
  88. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/discord/__init__.py +0 -0
  89. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/discord/discord_sink.py +0 -0
  90. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/discord/discord_sink_params.py +0 -0
  91. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/file/__init__.py +0 -0
  92. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/file/file_sink.py +0 -0
  93. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/file/file_sink_params.py +0 -0
  94. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/file/object_traverser.py +0 -0
  95. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/google_chat/__init__.py +0 -0
  96. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/google_chat/google_chat.py +0 -0
  97. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/google_chat/google_chat_params.py +0 -0
  98. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/incidentio/__init__.py +0 -0
  99. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/incidentio/incidentio_api.py +0 -0
  100. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/incidentio/incidentio_client.py +0 -0
  101. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/incidentio/incidentio_sink.py +0 -0
  102. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/incidentio/incidentio_sink_params.py +0 -0
  103. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/jira/__init__.py +0 -0
  104. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/jira/jira_sink.py +0 -0
  105. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/jira/jira_sink_params.py +0 -0
  106. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/kafka/__init__.py +0 -0
  107. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/kafka/kafka_sink.py +0 -0
  108. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/kafka/kafka_sink_params.py +0 -0
  109. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/mail/__init__.py +0 -0
  110. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/mail/mail_sink_params.py +0 -0
  111. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/mattermost/__init__.py +0 -0
  112. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/mattermost/mattermost_sink.py +0 -0
  113. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/mattermost/mattermost_sink_params.py +0 -0
  114. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/msteams/__init__.py +0 -0
  115. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/msteams/msteams_sink.py +0 -0
  116. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/msteams/msteams_sink_params.py +0 -0
  117. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/msteams/msteams_webhook_tranformer.py +0 -0
  118. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/opsgenie/__init__.py +0 -0
  119. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/opsgenie/opsgenie_sink.py +0 -0
  120. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/opsgenie/opsgenie_sink_params.py +0 -0
  121. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/pagerduty/__init__.py +0 -0
  122. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/pagerduty/pagerduty_sink.py +0 -0
  123. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/pagerduty/pagerduty_sink_params.py +0 -0
  124. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/pushover/__init__.py +0 -0
  125. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/pushover/pushover_client.py +0 -0
  126. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/pushover/pushover_sink.py +0 -0
  127. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/pushover/pushover_sink_params.py +0 -0
  128. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/robusta/__init__.py +0 -0
  129. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/robusta/dal/__init__.py +0 -0
  130. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/robusta/dal/model_conversion.py +0 -0
  131. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/robusta/discovery_metrics.py +0 -0
  132. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/robusta/prometheus_discovery_utils.py +0 -0
  133. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/robusta/robusta_sink.py +0 -0
  134. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/robusta/robusta_sink_params.py +0 -0
  135. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/robusta/rrm/__init__.py +0 -0
  136. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/robusta/rrm/account_resource_fetcher.py +0 -0
  137. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/robusta/rrm/base_resource_manager.py +0 -0
  138. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/robusta/rrm/prometheus_alert_resource_manager.py +0 -0
  139. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/robusta/rrm/rrm.py +0 -0
  140. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/robusta/rrm/types.py +0 -0
  141. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/rocketchat/__init__.py +0 -0
  142. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/rocketchat/rocketchat_sink.py +0 -0
  143. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/rocketchat/rocketchat_sink_params.py +0 -0
  144. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/servicenow/__init__.py +0 -0
  145. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/servicenow/servicenow_sink.py +0 -0
  146. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/servicenow/servicenow_sink_params.py +0 -0
  147. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/sink_base.py +0 -0
  148. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/sink_base_params.py +0 -0
  149. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/sink_config.py +0 -0
  150. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/sink_factory.py +0 -0
  151. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/slack/__init__.py +0 -0
  152. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/slack/preview/slack_sink_preview.py +0 -0
  153. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/slack/preview/slack_sink_preview_params.py +0 -0
  154. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/slack/slack_sink.py +0 -0
  155. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/slack/slack_sink_params.py +0 -0
  156. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/slack/templates/header.j2 +0 -0
  157. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/slack/templates/template_loader.py +0 -0
  158. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/telegram/__init__.py +0 -0
  159. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/telegram/telegram_client.py +0 -0
  160. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/telegram/telegram_sink.py +0 -0
  161. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/telegram/telegram_sink_params.py +0 -0
  162. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/timing.py +0 -0
  163. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/victorops/__init__.py +0 -0
  164. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/victorops/victorops_sink.py +0 -0
  165. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/victorops/victorops_sink_params.py +0 -0
  166. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/webex/__init__.py +0 -0
  167. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/webex/webex_sink.py +0 -0
  168. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/webex/webex_sink_params.py +0 -0
  169. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/webhook/__init__.py +0 -0
  170. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/webhook/webhook_sink.py +0 -0
  171. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/webhook/webhook_sink_params.py +0 -0
  172. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/yamessenger/__init__.py +0 -0
  173. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/yamessenger/yamessenger_client.py +0 -0
  174. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/yamessenger/yamessenger_sink.py +0 -0
  175. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/yamessenger/yamessenger_sink_params.py +0 -0
  176. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/zulip/__init__.py +0 -0
  177. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/zulip/zulip_sink.py +0 -0
  178. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/sinks/zulip/zulip_sink_params.py +0 -0
  179. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/stream/utils.py +0 -0
  180. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/triggers/container_oom_killed_trigger.py +0 -0
  181. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/triggers/custom_triggers.py +0 -0
  182. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/triggers/error_event_trigger.py +0 -0
  183. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/triggers/helm_releases_triggers.py +0 -0
  184. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/triggers/job_failed_trigger.py +0 -0
  185. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/triggers/multi_resources_trigger.py +0 -0
  186. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/triggers/oom_killed_trigger_base.py +0 -0
  187. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/triggers/pod_crash_loop_trigger.py +0 -0
  188. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/triggers/pod_evicted_trigger.py +0 -0
  189. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/triggers/pod_image_pull_backoff.py +0 -0
  190. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/core/triggers/pod_oom_killed_trigger.py +0 -0
  191. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/__init__.py +0 -0
  192. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/argocd/argocd_client.py +0 -0
  193. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/common/__init__.py +0 -0
  194. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/common/requests.py +0 -0
  195. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/discord/__init__.py +0 -0
  196. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/discord/sender.py +0 -0
  197. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/git/__init__.py +0 -0
  198. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/git/git_repo.py +0 -0
  199. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/git/well_known_hosts.py +0 -0
  200. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/google_chat/__init__.py +0 -0
  201. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/google_chat/sender.py +0 -0
  202. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/grafana.py +0 -0
  203. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/helper.py +0 -0
  204. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/jira/__init__.py +0 -0
  205. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/jira/client.py +0 -0
  206. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/jira/sender.py +0 -0
  207. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/kubernetes/__init__.py +0 -0
  208. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/kubernetes/api_client_utils.py +0 -0
  209. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/kubernetes/autogenerated/__init__.py +0 -0
  210. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/kubernetes/autogenerated/events.py +0 -0
  211. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/kubernetes/autogenerated/models.py +0 -0
  212. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/kubernetes/autogenerated/triggers.py +0 -0
  213. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/kubernetes/autogenerated/v1/__init__.py +0 -0
  214. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/kubernetes/autogenerated/v1/models.py +0 -0
  215. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/kubernetes/base_event.py +0 -0
  216. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/kubernetes/base_triggers.py +0 -0
  217. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/kubernetes/custom_crds.py +0 -0
  218. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/kubernetes/custom_models.py +0 -0
  219. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/kubernetes/model_not_found_exception.py +0 -0
  220. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/kubernetes/process_utils.py +0 -0
  221. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/kubernetes/templates.py +0 -0
  222. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/mail/__init__.py +0 -0
  223. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/mattermost/__init__.py +0 -0
  224. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/mattermost/client.py +0 -0
  225. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/mattermost/sender.py +0 -0
  226. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/msteams/__init__.py +0 -0
  227. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/msteams/msteams_adaptive_card_files.py +0 -0
  228. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/msteams/msteams_adaptive_card_files_text.py +0 -0
  229. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/msteams/msteams_elements/__init__.py +0 -0
  230. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/msteams/msteams_elements/msteams_action.py +0 -0
  231. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/msteams/msteams_elements/msteams_base.py +0 -0
  232. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/msteams/msteams_elements/msteams_card.py +0 -0
  233. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/msteams/msteams_elements/msteams_column.py +0 -0
  234. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/msteams/msteams_elements/msteams_container.py +0 -0
  235. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/msteams/msteams_elements/msteams_images.py +0 -0
  236. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/msteams/msteams_elements/msteams_table.py +0 -0
  237. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/msteams/msteams_elements/msteams_text_block.py +0 -0
  238. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/msteams/msteams_mark_down_fix_url.py +0 -0
  239. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/msteams/msteams_msg.py +0 -0
  240. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/msteams/sender.py +0 -0
  241. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/openshift/__init__.py +0 -0
  242. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/openshift/token.py +0 -0
  243. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/prometheus/__init__.py +0 -0
  244. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/prometheus/models.py +0 -0
  245. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/prometheus/trigger.py +0 -0
  246. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/prometheus/utils.py +0 -0
  247. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/receiver.py +0 -0
  248. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/resource_analysis/__init__.py +0 -0
  249. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/resource_analysis/cpu_analyzer.py +0 -0
  250. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/resource_analysis/memory_analyzer.py +0 -0
  251. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/resource_analysis/node_cpu_analyzer.py +0 -0
  252. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/resource_analysis/prometheus_analyzer.py +0 -0
  253. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/rocketchat/__init__.py +0 -0
  254. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/rocketchat/sender.py +0 -0
  255. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/scheduled/__init__.py +0 -0
  256. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/scheduled/event.py +0 -0
  257. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/scheduled/models.py +0 -0
  258. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/scheduled/playbook_scheduler.py +0 -0
  259. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/scheduled/playbook_scheduler_manager.py +0 -0
  260. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/scheduled/playbook_scheduler_manager_impl.py +0 -0
  261. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/scheduled/trigger.py +0 -0
  262. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/servicenow/__init__.py +0 -0
  263. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/servicenow/sender.py +0 -0
  264. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/slack/__init__.py +0 -0
  265. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/slack/sender.py +0 -0
  266. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/webex/__init__.py +0 -0
  267. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/webex/sender.py +0 -0
  268. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/zulip/__init__.py +0 -0
  269. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/integrations/zulip/sender.py +0 -0
  270. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/model/alert_relabel_config.py +0 -0
  271. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/model/config.py +0 -0
  272. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/model/playbook_action.py +0 -0
  273. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/model/playbook_definition.py +0 -0
  274. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/patch/patch.py +0 -0
  275. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/runner/__init__.py +0 -0
  276. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/runner/log_init.py +0 -0
  277. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/runner/main.py +0 -0
  278. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/runner/not_found_exception.py +0 -0
  279. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/runner/object_updater.py +0 -0
  280. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/runner/process_setup.py +0 -0
  281. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/runner/ssl_utils.py +0 -0
  282. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/runner/telemetry.py +0 -0
  283. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/runner/telemetry_service.py +0 -0
  284. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/runner/web.py +0 -0
  285. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/runner/web_api.py +0 -0
  286. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/__init__.py +0 -0
  287. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/auth_provider.py +0 -0
  288. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/base64_utils.py +0 -0
  289. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/cluster_provider_discovery.py +0 -0
  290. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/common.py +0 -0
  291. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/decorators.py +0 -0
  292. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/docs.py +0 -0
  293. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/documented_pydantic.py +0 -0
  294. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/error_codes.py +0 -0
  295. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/file_system_watcher.py +0 -0
  296. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/function_hashes.py +0 -0
  297. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/json_schema.py +0 -0
  298. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/parsing.py +0 -0
  299. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/rate_limiter.py +0 -0
  300. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/scope.py +0 -0
  301. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/server_start.py +0 -0
  302. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/service_discovery.py +0 -0
  303. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/silence_utils.py +0 -0
  304. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/stack_tracer.py +0 -0
  305. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/task_queue.py +0 -0
  306. {robusta_api-0.47.0 → robusta_api-0.48.0}/src/robusta/utils/time_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: robusta-api
3
- Version: 0.47.0
3
+ Version: 0.48.0
4
4
  Summary:
5
5
  Author: Arik Alon
6
6
  Author-email: arikalon1@users.noreply.github.com
@@ -9,8 +9,7 @@ Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
11
  Provides-Extra: all
12
- Requires-Dist: CairoSVG (>=2.5.2,<3.0.0) ; extra == "all"
13
- Requires-Dist: Flask (>=3.0.0,<4.0.0) ; extra == "all"
12
+ Requires-Dist: Flask (>=3.1.3,<4.0.0) ; extra == "all"
14
13
  Requires-Dist: apprise (==1.6.0)
15
14
  Requires-Dist: attrs (>=23.1.0,<24.0.0)
16
15
  Requires-Dist: better-exceptions (>=0.3.3,<0.4.0) ; extra == "all"
@@ -21,12 +20,11 @@ Requires-Dist: certifi (>=2026.4.22)
21
20
  Requires-Dist: click (>=8.3.3)
22
21
  Requires-Dist: colorlog (>=5.0.1,<6.0.0)
23
22
  Requires-Dist: croniter (>=1.3.15,<2.0.0)
24
- Requires-Dist: cryptography (>=48.0.1,<49.0.0)
23
+ Requires-Dist: cryptography (>=50.0.0,<51.0.0)
25
24
  Requires-Dist: datadog-api-client (>=1.2.0,<2.0.0) ; extra == "all"
26
25
  Requires-Dist: docutils (==0.19)
27
26
  Requires-Dist: dpath (>=2.0.5,<3.0.0)
28
27
  Requires-Dist: filelock (>=3.20.3)
29
- Requires-Dist: fpdf2 (>=2.7.1,<3.0.0)
30
28
  Requires-Dist: grafana-api (>=1.0.3,<2.0.0) ; extra == "all"
31
29
  Requires-Dist: h2 (>=4.3.0,<5.0.0)
32
30
  Requires-Dist: hikaru-model-26 (>=1.1.1,<2.0.0)
@@ -54,13 +52,16 @@ Requires-Dist: python-json-logger (>=3.0.0,<4.0.0)
54
52
  Requires-Dist: pytz (>=2021.3,<2022.0)
55
53
  Requires-Dist: pyyaml (>=6.0,<7.0)
56
54
  Requires-Dist: regex (==2024.5.15)
55
+ Requires-Dist: reportlab (>=5.0.0,<6.0.0)
57
56
  Requires-Dist: requests (>=2.33.0,<3.0.0)
57
+ Requires-Dist: resvg-py (==0.3.4)
58
58
  Requires-Dist: rocketchat-api (>=1.30.0,<2.0.0)
59
59
  Requires-Dist: sentry-sdk[flask] (>=2.13.0,<3.0.0) ; extra == "all"
60
60
  Requires-Dist: setuptools (>=80.9.0,<81.0.0)
61
61
  Requires-Dist: slack-sdk (>=3.23.0,<4.0.0)
62
62
  Requires-Dist: supabase (==2.5.1) ; extra == "all"
63
63
  Requires-Dist: tabulate (>=0.8.10,<0.9.0) ; extra == "all"
64
+ Requires-Dist: tenacity (>=9.1.2,<10.0.0)
64
65
  Requires-Dist: toml (>=0.10.2,<0.11.0)
65
66
  Requires-Dist: urllib3 (>=2.7.0,<3.0.0)
66
67
  Requires-Dist: virtualenv (>=20.36.1,<21.0.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "robusta-api"
3
- version = "0.47.0"
3
+ version = "0.48.0"
4
4
  description = ""
5
5
  authors = ["Arik Alon <arikalon1@users.noreply.github.com>"]
6
6
  packages = [
@@ -30,13 +30,17 @@ idna= { version = ">=3.15", optional = true }
30
30
  supabase = "2.5.1"
31
31
  tabulate = { version = "^0.8.10" }
32
32
  slack-sdk = { version = "^3.23.0" }
33
- Flask = { version = "^3.0.0", optional = true }
33
+ Flask = { version = "^3.1.3", optional = true }
34
34
  werkzeug = { version = ">=3.1.6", optional = true }
35
35
  jinja2 = { version = ">=3.1.5", optional = true }
36
36
  grafana-api = { version = "^1.0.3", optional = true }
37
37
  watchdog = { version = "^2.1.0", optional = true }
38
38
  better-exceptions = { version = "^0.3.3", optional = true }
39
- CairoSVG = { version = "^2.5.2", optional = true }
39
+ # SVG -> PNG rasterizer for charts sent to chat sinks (resvg is Apache-2.0 OR MIT,
40
+ # bindings are MIT; replaced LGPL-licensed CairoSVG)
41
+ # Pinned to an exact version for supply-chain safety since the binding package
42
+ # has a small maintainer base (see PR #2136 review)
43
+ resvg-py = "0.3.4"
40
44
  # Bump to fix CVE-2026-10142, CVE-2026-10143 (High): kafka-python protocol-parser DoS
41
45
  kafka-python = { version = "^2.3.2", optional = true }
42
46
  datadog-api-client = { version = "^1.2.0", optional = true }
@@ -60,10 +64,12 @@ humanize = "^3.13.1"
60
64
  # we're freezing a specific version here because the latest version doesn't have prebuilt wheels on pypi
61
65
  # and therefore requires gcc to install which we'd like to avoid
62
66
  # this is similar to the issue at https://github.com/dulwich/dulwich/issues/839
67
+ # Pin to fix CVE-2026-69247 (High): cryptography Bleichenbacher oracle in PKCS#7 decryption
63
68
  # Pin to fix CVE-2026-39892 (Critical): cryptography buffer overflow with non-contiguous buffers
64
69
  # Pin to fix GHSA-537c-gmf6-5ccf (High): cryptography
65
- cryptography = "^48.0.1"
66
- fpdf2 = "^2.7.1"
70
+ cryptography = "^50.0.0"
71
+ # PDF renderer for KRR/Popeye scan reports (BSD-3-Clause; replaced LGPL-licensed fpdf2)
72
+ reportlab = "^5.0.0"
67
73
  attrs = "^23.1.0"
68
74
  prometrix = "0.2.11"
69
75
  hikaru-model-26 = "^1.1.1"
@@ -92,13 +98,16 @@ pyasn1 = ">=0.6.4"
92
98
  filelock = ">=3.20.3"
93
99
  # Pin to fix CVE-2026-22702 (transitive via pre-commit)
94
100
  virtualenv = "^20.36.1"
95
- # Pillow is pulled in transitively (fpdf2, CairoSVG). Pin to fix CVE-2026-54058,
101
+ # Pillow is used directly (MS Teams PNG->JPEG image conversion) and by reportlab.
102
+ # Pin to fix CVE-2026-54058,
96
103
  # CVE-2026-54059, CVE-2026-54060, CVE-2026-55379, CVE-2026-55380, CVE-2026-59197,
97
104
  # CVE-2026-59199, CVE-2026-59200, CVE-2026-59204, CVE-2026-59205 (High)
98
105
  pillow = "^12.3.0"
99
106
  # click is pulled in transitively (flask, typer). Pin to fix CVE-2026-7246 (High):
100
107
  # command injection via unsanitized filename in click.edit()
101
108
  click = ">=8.3.3"
109
+ # Used directly for the Supabase login retry (ROB-1228); was only transitive via opsgenie-sdk
110
+ tenacity = "^9.1.2"
102
111
 
103
112
  [tool.poetry.dev-dependencies]
104
113
  pre-commit = "^2.13.0"
@@ -138,7 +147,7 @@ types-toml = "^0.10.2"
138
147
  types-tabulate = "^0.8.10"
139
148
 
140
149
  [tool.poetry.extras]
141
- all = ["Flask", "grafana-api", "watchdog", "better-exceptions", "CairoSVG", "tabulate", "kafka-python", "prometheus-api-client", "supabase", "datadog-api-client", "sentry-sdk", "poetry-core"]
150
+ all = ["Flask", "grafana-api", "watchdog", "better-exceptions", "tabulate", "kafka-python", "prometheus-api-client", "supabase", "datadog-api-client", "sentry-sdk", "poetry-core"]
142
151
 
143
152
  [tool.poetry.group.dev.dependencies]
144
153
  sphinx-jinja = { git = "https://github.com/robusta-dev/sphinx-jinja.git" }
@@ -1,2 +1,2 @@
1
1
  # this is updated by .github/workflows/release.yaml
2
- __version__ = "0.47.0"
2
+ __version__ = "0.48.0"
@@ -897,12 +897,13 @@ def extract_containers_k8(resource) -> List[Container]:
897
897
 
898
898
 
899
899
  def is_pod_ready(pod) -> bool:
900
+ # Unset until kubelet reports on the pod.
900
901
  conditions = []
901
902
  if isinstance(pod, V1Pod):
902
- conditions = pod.status.conditions
903
+ conditions = getattr(pod.status, "conditions", None) or []
903
904
 
904
905
  if isinstance(pod, Pod):
905
- conditions = pod.status.conditions
906
+ conditions = getattr(pod.status, "conditions", None) or []
906
907
 
907
908
  for condition in conditions:
908
909
  if condition.type == "Ready":
@@ -28,6 +28,14 @@ DISCOVERY_CHECK_THRESHOLD_SEC = int(os.environ.get("DISCOVERY_CHECK_THRESHOLD_SE
28
28
  DISCOVERY_PROCESS_TIMEOUT_SEC = int(os.environ.get("DISCOVERY_PROCESS_TIMEOUT_SEC", 60 * 120)) # 120 min
29
29
  DISCOVERY_WATCHDOG_CHECK_SEC = int(os.environ.get("DISCOVERY_WATCHDOG_CHECK_SEC", 15 * 120)) # 15 min
30
30
  SUPABASE_TIMEOUT_SECONDS = int(os.environ.get("SUPABASE_TIMEOUT_SECONDS", 60))
31
+ # Transient network failures on the Supabase login used to kill the runner (ROB-1228).
32
+ SUPABASE_LOGIN_RETRIES = int(os.environ.get("SUPABASE_LOGIN_RETRIES", 4))
33
+ SUPABASE_LOGIN_RETRY_BACKOFF_SEC = float(os.environ.get("SUPABASE_LOGIN_RETRY_BACKOFF_SEC", 2))
34
+ SUPABASE_LOGIN_RETRY_MAX_BACKOFF_SEC = float(os.environ.get("SUPABASE_LOGIN_RETRY_MAX_BACKOFF_SEC", 30))
35
+ # A TCP handshake that has not completed in this many seconds is not going to. Capped
36
+ # separately from SUPABASE_TIMEOUT_SECONDS so an unreachable platform fails (and retries)
37
+ # promptly instead of parking every attempt on the full read budget.
38
+ SUPABASE_CONNECT_TIMEOUT_SECONDS = float(os.environ.get("SUPABASE_CONNECT_TIMEOUT_SECONDS", 10))
31
39
  GRAFANA_RENDERER_URL = os.environ.get("GRAFANA_RENDERER_URL", "http://127.0.0.1:8281/render")
32
40
  RESOURCE_UPDATES_CACHE_TTL_SEC = os.environ.get("RESOURCE_UPDATES_CACHE_TTL_SEC", 120)
33
41
  INTERNAL_PLAYBOOKS_ROOT = os.environ.get("INTERNAL_PLAYBOOKS_ROOT", "/app/src/robusta/core/playbooks/internal")
@@ -21,12 +21,12 @@ def is_image(file_name: str):
21
21
 
22
22
 
23
23
  def convert_svg_to_png(svg: bytes) -> Optional[bytes]:
24
- # we import cairosvg here and not globally because in some environments it isn't trivially installed (e.g. windows)
25
- # and we don't want to throw an exception globally when it isn't around
26
- import cairosvg
24
+ # we import resvg_py here and not globally so the rasterizer is only loaded on
25
+ # the SVG-conversion path, and environments without it can still import this module
26
+ import resvg_py
27
27
 
28
28
  try:
29
- return cairosvg.svg2png(bytestring=svg)
29
+ return resvg_py.svg_to_bytes(svg_string=svg.decode("utf-8"))
30
30
  except Exception:
31
31
  logging.error(f"error converting svg to png; svg={svg}")
32
32
  return None
@@ -10,10 +10,10 @@ class MailSink(SinkBase):
10
10
  params = sink_config.mail_sink
11
11
 
12
12
  self.sender = MailSender(
13
- params.mailto,
14
- self.signing_key,
15
- self.account_id,
16
- self.cluster_name,
13
+ mailto=params.mailto,
14
+ account_id=self.account_id,
15
+ cluster_name=self.cluster_name,
16
+ signing_key=self.signing_key,
17
17
  use_ses=params.use_ses,
18
18
  aws_region=params.aws_region,
19
19
  from_email=params.from_email,
@@ -7,7 +7,9 @@ import threading
7
7
  from typing import Any, Dict, List, Optional, Tuple
8
8
  from uuid import uuid4
9
9
  from cachetools import TTLCache
10
+ import httpx
10
11
  import requests
12
+ from gotrue.errors import AuthRetryableError
11
13
  from postgrest._sync.request_builder import SyncQueryRequestBuilder
12
14
  from postgrest.base_request_builder import BaseFilterRequestBuilder, QueryArgs
13
15
  from postgrest.exceptions import APIError as PostgrestAPIError
@@ -15,10 +17,17 @@ from postgrest.types import ReturnMethod
15
17
  from postgrest.utils import sanitize_param
16
18
  from supabase import create_client
17
19
  from supabase.lib.client_options import ClientOptions
20
+ from tenacity import RetryCallState, Retrying, retry_if_exception_type, stop_after_attempt, wait_exponential
18
21
 
19
22
  from robusta.core.model.cluster_status import ClusterStatus
20
23
  from robusta.core.exceptions import SupabaseDnsException
21
- from robusta.core.model.env_vars import SUPABASE_TIMEOUT_SECONDS
24
+ from robusta.core.model.env_vars import (
25
+ SUPABASE_CONNECT_TIMEOUT_SECONDS,
26
+ SUPABASE_LOGIN_RETRIES,
27
+ SUPABASE_LOGIN_RETRY_BACKOFF_SEC,
28
+ SUPABASE_LOGIN_RETRY_MAX_BACKOFF_SEC,
29
+ SUPABASE_TIMEOUT_SECONDS,
30
+ )
22
31
  from robusta.core.model.helm_release import HelmRelease
23
32
  from robusta.core.model.jobs import JobInfo
24
33
  from robusta.core.model.namespaces import NamespaceInfo
@@ -69,6 +78,17 @@ def pre_select_patched(*args, **kwargs) -> QueryArgs:
69
78
  supabase_request_builder.pre_select = pre_select_patched
70
79
 
71
80
 
81
+ class _AuthHttpClient(httpx.Client):
82
+ """The httpx client we hand to Supabase's auth (GoTrue) sub-client.
83
+
84
+ Mirrors ``gotrue.http_clients.SyncClient``, whose only addition over ``httpx.Client``
85
+ is the ``aclose()`` alias that GoTrue's ``close()`` calls.
86
+ """
87
+
88
+ def aclose(self) -> None:
89
+ self.close()
90
+
91
+
72
92
  class SupabaseDal(AccountResourceFetcher):
73
93
  def __init__(
74
94
  self,
@@ -92,6 +112,7 @@ class SupabaseDal(AccountResourceFetcher):
92
112
  self.cluster = cluster_name
93
113
  options = ClientOptions(postgrest_client_timeout=SUPABASE_TIMEOUT_SECONDS, auto_refresh_token=True)
94
114
  self.client = create_client(url, key, options)
115
+ self.__apply_auth_client_timeout()
95
116
  self.patch_postgrest_execute()
96
117
  self.email = email
97
118
  self.password = password
@@ -104,6 +125,53 @@ class SupabaseDal(AccountResourceFetcher):
104
125
  self.token_cache = TTLCache(maxsize=1, ttl=ttl)
105
126
  self.lock = threading.Lock()
106
127
 
128
+ def __apply_auth_client_timeout(self) -> None:
129
+ """Give the auth (GoTrue) client the configured timeout, over HTTP/1.1.
130
+
131
+ ``ClientOptions.postgrest_client_timeout`` only reaches postgrest. supabase 2.5.1
132
+ builds the auth client with neither a timeout nor an http client of its own, so
133
+ logins ran on httpx's 5s default - and because ``socket.create_connection`` applies
134
+ the connect timeout once per resolved address, a dual-stack host gave up after ~10s
135
+ no matter what SUPABASE_TIMEOUT_SECONDS said (ROB-1228). supabase only grew a
136
+ supported hook for this in 2.28 (``ClientOptions(httpx_client=...)``), so until we
137
+ bump, replace the client on the instance.
138
+
139
+ Connect is capped below the read budget (SUPABASE_CONNECT_TIMEOUT_SECONDS): with a
140
+ 60s flat timeout, every attempt against a firewalled platform parks for a full
141
+ minute before the retry, so the actionable error takes minutes to appear.
142
+
143
+ HTTP/2 is dropped at the same time: httpcore's sync HTTP/2 connection is not thread
144
+ safe, and this DAL is shared across threads (the same problem as ROB-228). No
145
+ transport is passed, so environment proxies keep being honored as before.
146
+ """
147
+ auth_client = self.client.auth
148
+ http_client = _AuthHttpClient(
149
+ timeout=httpx.Timeout(
150
+ SUPABASE_TIMEOUT_SECONDS,
151
+ connect=min(SUPABASE_CONNECT_TIMEOUT_SECONDS, SUPABASE_TIMEOUT_SECONDS),
152
+ ),
153
+ follow_redirects=True,
154
+ http2=False,
155
+ )
156
+ try:
157
+ previous = auth_client._http_client
158
+ # the admin sub-API was handed the original client at construction time
159
+ admin = auth_client.admin
160
+ except AttributeError:
161
+ # A supabase/gotrue bump moved the internals. Keep their default client rather
162
+ # than failing startup over it, but say so: auth calls stay on httpx defaults.
163
+ http_client.close()
164
+ logging.warning(
165
+ "Could not apply SUPABASE_TIMEOUT_SECONDS to the Supabase auth client - its internals changed. "
166
+ "Auth calls will use the httpx default timeout.",
167
+ exc_info=True,
168
+ )
169
+ return
170
+
171
+ auth_client._http_client = http_client
172
+ admin._http_client = http_client
173
+ previous.close()
174
+
107
175
  def patch_postgrest_execute(self):
108
176
  # This is somewhat hacky.
109
177
  def execute_with_retry(_self):
@@ -553,13 +621,46 @@ class SupabaseDal(AccountResourceFetcher):
553
621
  logging.error(f"Failed to persist helm_releases {helm_releases} error: {e}")
554
622
  raise
555
623
 
624
+ def __sign_in_once(self) -> str:
625
+ res = self.client.auth.sign_in_with_password({"email": self.email, "password": self.password})
626
+ self.client.auth.set_session(res.session.access_token, res.session.refresh_token)
627
+ self.client.postgrest.auth(res.session.access_token)
628
+ return res.user.id
629
+
630
+ @staticmethod
631
+ def __log_sign_in_retry(retry_state: RetryCallState) -> None:
632
+ logging.warning(
633
+ "Supabase login attempt %s/%s failed: %s. Retrying in %.1fs",
634
+ retry_state.attempt_number,
635
+ SUPABASE_LOGIN_RETRIES,
636
+ retry_state.outcome.exception() if retry_state.outcome else "unknown error",
637
+ retry_state.next_action.sleep if retry_state.next_action else 0,
638
+ )
639
+
556
640
  def sign_in(self) -> str:
641
+ """Log in to Supabase, retrying transient network failures.
642
+
643
+ A single failure here used to be fatal: it propagates out of ``__init__`` through
644
+ Robusta sink construction up to ConfigLoader, which kills the whole runner process
645
+ group - so one connect timeout at pod start put the runner into CrashLoopBackOff,
646
+ with no in-process recovery path (ROB-1228). GoTrue labels these errors "retryable"
647
+ (``AuthRetryableError``); honor that instead of dying. DNS failures are retried too:
648
+ cluster DNS is often not ready yet at the moment the runner starts. A wrong
649
+ password or any other 4xx is an ``AuthApiError``, so it still fails immediately.
650
+ """
557
651
  logging.info("Supabase dal login")
558
652
  try:
559
- res = self.client.auth.sign_in_with_password({"email": self.email, "password": self.password})
560
- self.client.auth.set_session(res.session.access_token, res.session.refresh_token)
561
- self.client.postgrest.auth(res.session.access_token)
562
- return res.user.id
653
+ login = Retrying(
654
+ stop=stop_after_attempt(max(1, SUPABASE_LOGIN_RETRIES)),
655
+ wait=wait_exponential(
656
+ multiplier=SUPABASE_LOGIN_RETRY_BACKOFF_SEC,
657
+ max=SUPABASE_LOGIN_RETRY_MAX_BACKOFF_SEC,
658
+ ),
659
+ retry=retry_if_exception_type((AuthRetryableError, httpx.TransportError)),
660
+ before_sleep=self.__log_sign_in_retry,
661
+ reraise=True,
662
+ )
663
+ return login(self.__sign_in_once)
563
664
  except Exception as e:
564
665
  # Check if this is a DNS-related error
565
666
  error_msg = str(e).lower()
@@ -4,8 +4,6 @@ import urllib.parse
4
4
  from typing import List, Optional, Union
5
5
 
6
6
  import markdown2
7
- from fpdf import FPDF
8
- from fpdf.fonts import FontFace
9
7
 
10
8
  try:
11
9
  from tabulate import tabulate
@@ -233,67 +231,106 @@ class Transformer:
233
231
  if not isinstance(block, ScanReportBlock):
234
232
  return block
235
233
 
236
- accent_color = (140, 249, 209)
237
- headers_color = (63, 63, 63)
238
- table_color = (207, 215, 216)
239
-
240
- def set_normal_test_style(pdf: FPDF):
241
- pdf.set_font("", "", 8)
242
- pdf.set_text_color(0, 0, 0)
243
-
244
- def write_report_header(title: str, end_time, score: Union[int, str], grade: str):
245
- pdf.cell(pdf.w * 0.7, 10, f"**{title}** {end_time.strftime('%b %d, %y %X')}", border=0, markdown=True)
246
- if int(score) >= 0:
247
- pdf.cell(pdf.w * 0.3, 10, f"**{grade}** {score}", border=0, markdown=True)
248
-
249
- pdf.ln(20)
250
-
251
- def write_section_header(pdf: FPDF, header: str):
252
- if pdf.will_page_break(50):
253
- pdf.add_page()
254
-
255
- pdf.ln(12)
256
- pdf.set_font("", "B", 12)
257
- pdf.set_text_color(headers_color)
258
- pdf.cell(txt=header, border=0)
259
- pdf.ln(12)
260
-
261
- def write_config(pdf: FPDF, config: str):
262
- pdf.set_text_color(accent_color)
263
- pdf.cell(txt="config", border=0)
264
- pdf.ln(12)
265
- set_normal_test_style(pdf)
266
- pdf.multi_cell(w=0, txt=config, border=0)
267
-
268
- def write_table(pdf: FPDF, rows: List[list[str]]):
269
- pdf.set_draw_color(table_color)
270
- set_normal_test_style(pdf)
271
- with pdf.table(
272
- borders_layout="INTERNAL",
273
- rows=rows,
274
- headings_style=FontFace(color=(headers_color)),
275
- col_widths=scan.table_widths,
276
- markdown=True,
277
- line_height=1.5 * pdf.font_size,
278
- ):
279
- pass
234
+ # reportlab is imported lazily so the PDF toolchain is only required on the
235
+ # scan-report path, not for merely importing the sink layer
236
+ from io import BytesIO
237
+
238
+ from reportlab.lib import colors
239
+ from reportlab.lib.pagesizes import A4, landscape
240
+ from reportlab.lib.styles import ParagraphStyle
241
+ from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
242
+
243
+ accent_color = colors.Color(140 / 255, 249 / 255, 209 / 255)
244
+ headers_color = colors.Color(63 / 255, 63 / 255, 63 / 255)
245
+ table_color = colors.Color(207 / 255, 215 / 255, 216 / 255)
246
+
247
+ title_style = ParagraphStyle("scan-title", fontName="Courier", fontSize=18, leading=22)
248
+ config_label_style = ParagraphStyle(
249
+ "scan-config-label", fontName="Courier", fontSize=18, leading=22, textColor=accent_color
250
+ )
251
+ section_style = ParagraphStyle(
252
+ "scan-section", fontName="Courier-Bold", fontSize=12, leading=14, textColor=headers_color
253
+ )
254
+ normal_style = ParagraphStyle("scan-normal", fontName="Courier", fontSize=8, leading=12)
255
+ heading_cell_style = ParagraphStyle(
256
+ "scan-heading-cell", fontName="Courier", fontSize=8, leading=12, textColor=headers_color
257
+ )
258
+
259
+ def cell_markup(text) -> str:
260
+ """Escape a table cell and translate the markdown supported in scan cells
261
+ (**bold** and newlines) to paragraph markup."""
262
+ escaped = str(text).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
263
+ bolded = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", escaped, flags=re.DOTALL)
264
+ return bolded.replace("\n", "<br/>")
280
265
 
281
266
  scan: ScanReportBlock = block
282
267
  scan_headers = scan.table_headers
283
268
  scan_data = scan.table_data
284
- pdf = FPDF(orientation="landscape", format="A4")
285
- pdf.add_page()
286
- pdf.set_font("courier", "", 18)
287
- pdf.set_line_width(0.1)
288
- pdf.c_margin = 2 # create default cell margin to add table "padding"
289
-
290
269
  title = f"{scan.type.capitalize()} report"
291
- write_report_header(title, scan.end_time, scan.score, scan.grade)
292
- write_config(pdf, scan.config)
293
270
 
294
- for section_name, section_data in scan_data.items():
295
- rows = [scan_headers, *section_data]
296
- write_section_header(pdf, section_name)
297
- write_table(pdf, rows)
271
+ story = []
272
+
273
+ header_cells = [Paragraph(f"<b>{cell_markup(title)}</b> {scan.end_time.strftime('%b %d, %y %X')}", title_style)]
274
+ header_widths = [0.7]
275
+ try:
276
+ # scan.grade also parses the score, so it must stay inside this guard
277
+ if int(scan.score) >= 0:
278
+ header_cells.append(Paragraph(f"<b>{scan.grade}</b> {scan.score}", title_style))
279
+ header_widths.append(0.3)
280
+ except (TypeError, ValueError):
281
+ logging.warning(f"scan report has a non-integer score {scan.score!r}; skipping the score badge")
282
+
283
+ page_width, _ = landscape(A4)
284
+ margin = 28 # ~10mm, matching the previous layout
285
+ content_width = page_width - 2 * margin
286
+ story.append(Table([header_cells], colWidths=[w * content_width for w in header_widths]))
287
+ story.append(Spacer(1, 20))
288
+
289
+ story.append(Paragraph("config", config_label_style))
290
+ story.append(Spacer(1, 6))
291
+ story.append(Paragraph(cell_markup(scan.config), normal_style))
298
292
 
299
- return FileBlock(f"{title}.pdf", pdf.output())
293
+ for section_name, section_data in scan_data.items():
294
+ story.append(Spacer(1, 12))
295
+ story.append(Paragraph(cell_markup(section_name), section_style))
296
+ story.append(Spacer(1, 6))
297
+
298
+ heading_row = [Paragraph(cell_markup(header), heading_cell_style) for header in scan_headers]
299
+ body_rows = [
300
+ [Paragraph(cell_markup(cell), normal_style) for cell in row] for row in section_data
301
+ ]
302
+ width_total = sum(scan.table_widths)
303
+ column_widths = [w / width_total * content_width for w in scan.table_widths]
304
+ # splitInRow lets a single row taller than the page (e.g. a Popeye resource
305
+ # with very many issues in one cell) split across pages instead of raising
306
+ table = Table(
307
+ [heading_row, *body_rows],
308
+ colWidths=column_widths,
309
+ repeatRows=1,
310
+ splitByRow=1,
311
+ splitInRow=1,
312
+ )
313
+ table.setStyle(
314
+ TableStyle(
315
+ [
316
+ ("INNERGRID", (0, 0), (-1, -1), 0.3, table_color),
317
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
318
+ ("LEFTPADDING", (0, 0), (-1, -1), 6),
319
+ ("RIGHTPADDING", (0, 0), (-1, -1), 6),
320
+ ]
321
+ )
322
+ )
323
+ story.append(table)
324
+
325
+ buffer = BytesIO()
326
+ doc = SimpleDocTemplate(
327
+ buffer,
328
+ pagesize=landscape(A4),
329
+ leftMargin=margin,
330
+ rightMargin=margin,
331
+ topMargin=margin,
332
+ bottomMargin=margin,
333
+ title=title,
334
+ )
335
+ doc.build(story)
336
+ return FileBlock(f"{title}.pdf", buffer.getvalue())
@@ -23,8 +23,12 @@ from robusta.core.sinks.transformer import Transformer
23
23
 
24
24
 
25
25
  class MailSender(HTMLBaseSender):
26
+ # Keyword-only: account_id, cluster_name and signing_key are all plain strings, so a
27
+ # positional call that gets their order wrong silently puts the signing key into the
28
+ # `account` query parameter of the platform links we email out.
26
29
  def __init__(
27
30
  self,
31
+ *,
28
32
  mailto: str,
29
33
  account_id: str,
30
34
  cluster_name: str,
@@ -5,7 +5,7 @@ import uuid
5
5
  from typing import List
6
6
 
7
7
  from robusta.core.reporting.blocks import FileBlock
8
- from robusta.core.reporting.utils import JPG_SUFFIX, PNG_SUFFIX, file_suffix_match, is_image
8
+ from robusta.core.reporting.utils import JPG_SUFFIX, PNG_SUFFIX, convert_svg_to_png, file_suffix_match, is_image
9
9
  from robusta.integrations.msteams.msteams_elements.msteams_images import MsTeamsImages
10
10
 
11
11
 
@@ -64,6 +64,7 @@ class MsTeamsAdaptiveCardFilesImage:
64
64
  # msteams cant read parsing of url to svg image
65
65
  @classmethod
66
66
  def __svg_convert_bytes_to_jpg(cls, svg_bytes: bytes):
67
- from cairosvg import svg2png
68
-
69
- return cls.__png_convert_bytes_to_base_64_url(svg2png(bytestring=svg_bytes))
67
+ png_bytes = convert_svg_to_png(svg_bytes)
68
+ if png_bytes is None:
69
+ raise ValueError("failed to convert svg to png for msteams image")
70
+ return cls.__png_convert_bytes_to_base_64_url(png_bytes)
@@ -41,6 +41,7 @@ from robusta.integrations.scheduled.playbook_scheduler_manager_impl import Playb
41
41
  from robusta.integrations.scheduled.trigger import ScheduledTriggerEvent
42
42
  from robusta.model.config import PlaybooksRegistry, PlaybooksRegistryImpl, Registry, SinksRegistry
43
43
  from robusta.model.playbook_definition import PlaybookDefinition
44
+ from robusta.utils.archive import safe_extract_tar
44
45
  from robusta.utils.cluster_provider_discovery import cluster_provider
45
46
  from robusta.utils.file_system_watcher import FileSystemWatcher
46
47
  from robusta.core.exceptions import SupabaseDnsException
@@ -354,7 +355,7 @@ class ConfigLoader:
354
355
  f.flush()
355
356
 
356
357
  with tarfile.open(f.name, "r:gz") as tar, tempfile.TemporaryDirectory() as temp_dir:
357
- tar.extractall(path=temp_dir)
358
+ safe_extract_tar(tar, temp_dir)
358
359
  extracted_items = os.listdir(temp_dir)
359
360
 
360
361
  pkg_path = temp_dir
@@ -0,0 +1,54 @@
1
+ import os
2
+ import tarfile
3
+ from typing import List
4
+
5
+
6
+ class UnsafeArchiveError(Exception):
7
+ """Raised when a tar archive contains a member that would be extracted outside the target directory."""
8
+
9
+
10
+ def _is_within(directory: str, target: str) -> bool:
11
+ directory = os.path.realpath(directory)
12
+ target = os.path.realpath(target)
13
+ return target == directory or target.startswith(directory + os.sep)
14
+
15
+
16
+ def _validate_members(tar: tarfile.TarFile, path: str) -> List[tarfile.TarInfo]:
17
+ members = tar.getmembers()
18
+ for member in members:
19
+ if member.isdev():
20
+ raise UnsafeArchiveError(f"Archive contains a device/fifo member: {member.name}")
21
+
22
+ if os.path.isabs(member.name) or member.name.startswith("/"):
23
+ raise UnsafeArchiveError(f"Archive contains a member with an absolute path: {member.name}")
24
+
25
+ if not _is_within(path, os.path.join(path, member.name)):
26
+ raise UnsafeArchiveError(f"Archive contains a member escaping the target directory: {member.name}")
27
+
28
+ if member.issym() or member.islnk():
29
+ # link targets are resolved relative to the directory holding the link
30
+ link_base = os.path.join(path, os.path.dirname(member.name))
31
+ if os.path.isabs(member.linkname) or not _is_within(path, os.path.join(link_base, member.linkname)):
32
+ raise UnsafeArchiveError(
33
+ f"Archive contains a link escaping the target directory: {member.name} -> {member.linkname}"
34
+ )
35
+
36
+ return members
37
+
38
+
39
+ def safe_extract_tar(tar: tarfile.TarFile, path: str) -> None:
40
+ """
41
+ Extract a tar archive into ``path``, refusing archives whose members would be written outside of it.
42
+
43
+ Guards against path traversal ("zip slip"), absolute member paths, escaping symlinks/hardlinks and
44
+ device nodes. Raises ``UnsafeArchiveError`` without extracting anything if the archive is unsafe.
45
+ """
46
+ if hasattr(tarfile, "data_filter"): # python 3.12+, and 3.10.12+/3.11.4+ backports
47
+ # pre-validate so that nothing is written before an unsafe member is detected
48
+ _validate_members(tar, path)
49
+ try:
50
+ tar.extractall(path=path, filter="data")
51
+ except tarfile.FilterError as e:
52
+ raise UnsafeArchiveError(f"Refusing to extract unsafe archive: {e}") from e
53
+ else:
54
+ tar.extractall(path=path, members=_validate_members(tar, path))
File without changes