robusta-api 0.51.0__tar.gz → 0.52.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.
- {robusta_api-0.51.0 → robusta_api-0.52.0}/PKG-INFO +5 -5
- {robusta_api-0.51.0 → robusta_api-0.52.0}/pyproject.toml +7 -7
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/_version.py +1 -1
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/model/runner_config.py +2 -0
- robusta_api-0.52.0/src/robusta/core/sinks/common/destination_override.py +67 -0
- robusta_api-0.52.0/src/robusta/core/sinks/msteams_graph/__init__.py +5 -0
- robusta_api-0.52.0/src/robusta/core/sinks/msteams_graph/msteams_graph_sink.py +64 -0
- robusta_api-0.52.0/src/robusta/core/sinks/msteams_graph/msteams_graph_sink_params.py +42 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/sink_factory.py +2 -0
- robusta_api-0.52.0/src/robusta/core/sinks/webex/webex_sink.py +38 -0
- robusta_api-0.52.0/src/robusta/integrations/msteams/graph/graph_client.py +119 -0
- robusta_api-0.52.0/src/robusta/integrations/msteams/graph/graph_msg.py +167 -0
- robusta_api-0.52.0/src/robusta/integrations/msteams/graph/sender.py +41 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/msteams/msteams_msg.py +63 -36
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/msteams/sender.py +16 -59
- robusta_api-0.52.0/src/robusta/utils/__init__.py +0 -0
- robusta_api-0.51.0/src/robusta/core/sinks/webex/webex_sink.py +0 -80
- {robusta_api-0.51.0 → robusta_api-0.52.0}/LICENSE +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/api/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/clients/robusta_client.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/discovery/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/discovery/discovery.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/discovery/resource_names.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/discovery/top_service_resolver.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/discovery/utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/exceptions.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/model/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/model/base_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/model/cluster_status.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/model/env_vars.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/model/events.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/model/helm_release.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/model/jobs.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/model/k8s_operation_type.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/model/namespaces.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/model/nodes.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/model/openshift_group.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/model/pods.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/model/services.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/persistency/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/persistency/in_memory.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/persistency/scheduled_jobs_states_dal.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/actions_registry.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/base_trigger.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/common.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/container_playbook_utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/crash_reporter.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/generation.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/internal/CRD_API_DOCUMENTATION.md +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/internal/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/internal/ai_integration.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/internal/crds.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/internal/discovery_events.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/job_utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/node_enrichment_utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/node_playbook_utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/oom_killer_utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/playbook_utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/playbooks_event_handler.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/playbooks_event_handler_impl.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/pod_utils/crashloop_utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/pod_utils/imagepull_utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/pod_utils/pending_pod_utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/prometheus_enrichment_utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/playbooks/trigger.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/pubsub/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/pubsub/event_emitter.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/pubsub/event_subscriber.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/pubsub/events_pubsub.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/reporting/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/reporting/action_requests.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/reporting/base.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/reporting/blocks.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/reporting/callbacks.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/reporting/charts.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/reporting/consts.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/reporting/custom_rendering.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/reporting/finding_subjects.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/reporting/holmes.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/reporting/url_helpers.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/reporting/utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/schedule/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/schedule/model.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/schedule/scheduler.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/common/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/common/channel_transformer.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/common/html_tools.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/datadog/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/datadog/datadog_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/datadog/datadog_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/discord/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/discord/discord_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/discord/discord_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/file/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/file/file_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/file/file_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/file/object_traverser.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/google_chat/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/google_chat/google_chat.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/google_chat/google_chat_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/incidentio/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/incidentio/incidentio_api.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/incidentio/incidentio_client.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/incidentio/incidentio_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/incidentio/incidentio_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/jira/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/jira/jira_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/jira/jira_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/kafka/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/kafka/kafka_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/kafka/kafka_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/mail/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/mail/mail_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/mail/mail_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/mattermost/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/mattermost/mattermost_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/mattermost/mattermost_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/msteams/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/msteams/msteams_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/msteams/msteams_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/msteams/msteams_webhook_tranformer.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/opsgenie/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/opsgenie/opsgenie_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/opsgenie/opsgenie_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/pagerduty/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/pagerduty/pagerduty_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/pagerduty/pagerduty_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/pushover/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/pushover/pushover_client.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/pushover/pushover_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/pushover/pushover_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/robusta/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/robusta/dal/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/robusta/dal/model_conversion.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/robusta/dal/supabase_dal.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/robusta/discovery_metrics.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/robusta/prometheus_discovery_utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/robusta/robusta_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/robusta/robusta_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/robusta/rrm/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/robusta/rrm/account_resource_fetcher.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/robusta/rrm/base_resource_manager.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/robusta/rrm/prometheus_alert_resource_manager.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/robusta/rrm/rrm.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/robusta/rrm/types.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/rocketchat/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/rocketchat/rocketchat_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/rocketchat/rocketchat_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/servicenow/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/servicenow/servicenow_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/servicenow/servicenow_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/sink_base.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/sink_base_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/sink_config.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/slack/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/slack/preview/slack_sink_preview.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/slack/preview/slack_sink_preview_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/slack/slack_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/slack/slack_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/slack/templates/header.j2 +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/slack/templates/template_loader.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/telegram/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/telegram/telegram_client.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/telegram/telegram_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/telegram/telegram_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/timing.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/transformer.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/victorops/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/victorops/victorops_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/victorops/victorops_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/webex/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/webex/webex_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/webhook/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/webhook/webhook_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/webhook/webhook_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/yamessenger/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/yamessenger/yamessenger_client.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/yamessenger/yamessenger_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/yamessenger/yamessenger_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/zulip/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/zulip/zulip_sink.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/sinks/zulip/zulip_sink_params.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/stream/utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/triggers/container_oom_killed_trigger.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/triggers/custom_triggers.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/triggers/error_event_trigger.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/triggers/helm_releases_triggers.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/triggers/job_failed_trigger.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/triggers/multi_resources_trigger.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/triggers/oom_killed_trigger_base.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/triggers/pod_crash_loop_trigger.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/triggers/pod_evicted_trigger.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/triggers/pod_image_pull_backoff.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/core/triggers/pod_oom_killed_trigger.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/argocd/argocd_client.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/common/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/common/requests.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/discord/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/discord/sender.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/git/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/git/git_repo.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/git/well_known_hosts.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/google_chat/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/google_chat/sender.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/grafana.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/helper.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/jira/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/jira/client.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/jira/sender.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/kubernetes/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/kubernetes/api_client_utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/kubernetes/autogenerated/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/kubernetes/autogenerated/events.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/kubernetes/autogenerated/models.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/kubernetes/autogenerated/triggers.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/kubernetes/autogenerated/v1/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/kubernetes/autogenerated/v1/models.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/kubernetes/base_event.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/kubernetes/base_triggers.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/kubernetes/custom_crds.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/kubernetes/custom_models.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/kubernetes/model_not_found_exception.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/kubernetes/process_utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/kubernetes/templates.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/mail/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/mail/sender.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/mattermost/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/mattermost/client.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/mattermost/sender.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/msteams/__init__.py +0 -0
- {robusta_api-0.51.0/src/robusta/integrations/msteams/msteams_elements → robusta_api-0.52.0/src/robusta/integrations/msteams/graph}/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/msteams/msteams_adaptive_card_files.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/msteams/msteams_adaptive_card_files_image.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/msteams/msteams_adaptive_card_files_text.py +0 -0
- {robusta_api-0.51.0/src/robusta/integrations/prometheus → robusta_api-0.52.0/src/robusta/integrations/msteams/msteams_elements}/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/msteams/msteams_elements/msteams_action.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/msteams/msteams_elements/msteams_base.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/msteams/msteams_elements/msteams_card.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/msteams/msteams_elements/msteams_column.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/msteams/msteams_elements/msteams_container.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/msteams/msteams_elements/msteams_images.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/msteams/msteams_elements/msteams_table.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/msteams/msteams_elements/msteams_text_block.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/msteams/msteams_mark_down_fix_url.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/openshift/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/openshift/token.py +0 -0
- {robusta_api-0.51.0/src/robusta/integrations/resource_analysis → robusta_api-0.52.0/src/robusta/integrations/prometheus}/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/prometheus/models.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/prometheus/trigger.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/prometheus/utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/receiver.py +0 -0
- {robusta_api-0.51.0/src/robusta/integrations/rocketchat → robusta_api-0.52.0/src/robusta/integrations/resource_analysis}/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/resource_analysis/cpu_analyzer.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/resource_analysis/memory_analyzer.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/resource_analysis/node_cpu_analyzer.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/resource_analysis/prometheus_analyzer.py +0 -0
- {robusta_api-0.51.0/src/robusta/integrations/scheduled → robusta_api-0.52.0/src/robusta/integrations/rocketchat}/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/rocketchat/sender.py +0 -0
- {robusta_api-0.51.0/src/robusta/integrations/servicenow → robusta_api-0.52.0/src/robusta/integrations/scheduled}/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/scheduled/event.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/scheduled/models.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/scheduled/playbook_scheduler.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/scheduled/playbook_scheduler_manager.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/scheduled/playbook_scheduler_manager_impl.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/scheduled/trigger.py +0 -0
- {robusta_api-0.51.0/src/robusta/integrations/webex → robusta_api-0.52.0/src/robusta/integrations/servicenow}/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/servicenow/sender.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/slack/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/slack/sender.py +0 -0
- {robusta_api-0.51.0/src/robusta/runner → robusta_api-0.52.0/src/robusta/integrations/webex}/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/webex/sender.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/zulip/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/integrations/zulip/sender.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/model/alert_relabel_config.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/model/config.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/model/playbook_action.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/model/playbook_definition.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/patch/patch.py +0 -0
- {robusta_api-0.51.0/src/robusta/utils → robusta_api-0.52.0/src/robusta/runner}/__init__.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/runner/config_loader.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/runner/log_init.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/runner/main.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/runner/not_found_exception.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/runner/object_updater.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/runner/process_setup.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/runner/ssl_utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/runner/telemetry.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/runner/telemetry_service.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/runner/web.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/runner/web_api.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/archive.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/auth_provider.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/base64_utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/cluster_provider_discovery.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/common.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/decorators.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/docs.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/documented_pydantic.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/error_codes.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/file_system_watcher.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/function_hashes.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/json_schema.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/parsing.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/rate_limiter.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/scope.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/server_start.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/service_discovery.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/silence_utils.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/stack_tracer.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.0}/src/robusta/utils/task_queue.py +0 -0
- {robusta_api-0.51.0 → robusta_api-0.52.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.
|
|
3
|
+
Version: 0.52.0
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Arik Alon
|
|
6
6
|
Author-email: arikalon1@users.noreply.github.com
|
|
@@ -45,7 +45,7 @@ Requires-Dist: prometrix (==0.2.11)
|
|
|
45
45
|
Requires-Dist: pyasn1 (>=0.6.4)
|
|
46
46
|
Requires-Dist: pydantic (>=2.11.7,<3.0.0)
|
|
47
47
|
Requires-Dist: pydash (==8.0.0)
|
|
48
|
-
Requires-Dist: pyjwt (>=2.
|
|
48
|
+
Requires-Dist: pyjwt (>=2.14.0,<3.0.0)
|
|
49
49
|
Requires-Dist: pymsteams (>=0.1.16,<0.2.0)
|
|
50
50
|
Requires-Dist: python-json-logger (>=3.0.0,<4.0.0)
|
|
51
51
|
Requires-Dist: pytz (>=2021.3,<2022.0)
|
|
@@ -56,14 +56,14 @@ Requires-Dist: requests (>=2.33.0,<3.0.0)
|
|
|
56
56
|
Requires-Dist: resvg-py (==0.3.4)
|
|
57
57
|
Requires-Dist: rocketchat-api (>=1.30.0,<2.0.0)
|
|
58
58
|
Requires-Dist: sentry-sdk[flask] (>=2.13.0,<3.0.0) ; extra == "all"
|
|
59
|
-
Requires-Dist: setuptools (>=
|
|
59
|
+
Requires-Dist: setuptools (>=83.0.0,<84.0.0)
|
|
60
60
|
Requires-Dist: slack-sdk (>=3.23.0,<4.0.0)
|
|
61
61
|
Requires-Dist: supabase (==2.28.1) ; extra == "all"
|
|
62
62
|
Requires-Dist: tabulate (>=0.8.10,<0.9.0) ; extra == "all"
|
|
63
63
|
Requires-Dist: tenacity (>=9.1.2,<10.0.0)
|
|
64
64
|
Requires-Dist: toml (>=0.10.2,<0.11.0)
|
|
65
|
-
Requires-Dist: urllib3 (>=2.
|
|
66
|
-
Requires-Dist: virtualenv (>=
|
|
65
|
+
Requires-Dist: urllib3 (>=2.8.0,<3.0.0)
|
|
66
|
+
Requires-Dist: virtualenv (>=21.7.13,<22.0.0)
|
|
67
67
|
Requires-Dist: watchdog (>=2.1.0,<3.0.0) ; extra == "all"
|
|
68
68
|
Requires-Dist: watchgod (>=0.7,<0.8)
|
|
69
69
|
Requires-Dist: webexteamssdk (>=1.6.1,<2.0.0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "robusta-api"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.52.0"
|
|
4
4
|
description = ""
|
|
5
5
|
authors = ["Arik Alon <arikalon1@users.noreply.github.com>"]
|
|
6
6
|
packages = [
|
|
@@ -18,7 +18,7 @@ include_trailing_comma = true
|
|
|
18
18
|
|
|
19
19
|
[tool.poetry.dependencies]
|
|
20
20
|
python = ">=3.10, <3.12"
|
|
21
|
-
setuptools = "^
|
|
21
|
+
setuptools = "^83.0.0"
|
|
22
22
|
colorlog = "^5.0.1"
|
|
23
23
|
python-json-logger = "^3.0.0"
|
|
24
24
|
pydantic = "^2.11.7"
|
|
@@ -83,9 +83,9 @@ requests = "^2.33.0"
|
|
|
83
83
|
# Bump to fix certifi CVE flagged by customer scan; date-versioned, so use >= rather than caret
|
|
84
84
|
certifi = ">=2026.4.22"
|
|
85
85
|
regex = "2024.5.15"
|
|
86
|
-
# Pin to fix CVE-2026-
|
|
87
|
-
pyjwt = "^2.
|
|
88
|
-
urllib3 = "^2.
|
|
86
|
+
# Pin to fix CVE-2026-102268 (Critical) + CVE-2026-102266/102267/102271-102273 (High): PyJWT
|
|
87
|
+
pyjwt = "^2.14.0"
|
|
88
|
+
urllib3 = "^2.8.0"
|
|
89
89
|
httpx = "0.27.2"
|
|
90
90
|
# realtime (supabase dependency) requires websockets.asyncio, added in websockets 13
|
|
91
91
|
websockets = ">=13"
|
|
@@ -98,8 +98,8 @@ pyasn1 = ">=0.6.4"
|
|
|
98
98
|
anyio = "^4.14.2"
|
|
99
99
|
# Pin to fix CVE-2026-22701 (transitive via virtualenv/pre-commit)
|
|
100
100
|
filelock = ">=3.20.3"
|
|
101
|
-
# Pin to fix CVE-2026-
|
|
102
|
-
virtualenv = "^
|
|
101
|
+
# Pin to fix CVE-2026-102925/102930/102937 (transitive via pre-commit)
|
|
102
|
+
virtualenv = "^21.7.13"
|
|
103
103
|
# Pillow is used directly (MS Teams PNG->JPEG image conversion) and by reportlab.
|
|
104
104
|
# Pin to fix CVE-2026-54058,
|
|
105
105
|
# CVE-2026-54059, CVE-2026-54060, CVE-2026-55379, CVE-2026-55380, CVE-2026-59197,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# this is updated by .github/workflows/release.yaml
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.52.0"
|
|
@@ -12,6 +12,7 @@ from robusta.core.sinks.jira.jira_sink_params import JiraSinkConfigWrapper
|
|
|
12
12
|
from robusta.core.sinks.kafka.kafka_sink_params import KafkaSinkConfigWrapper
|
|
13
13
|
from robusta.core.sinks.mattermost.mattermost_sink_params import MattermostSinkConfigWrapper
|
|
14
14
|
from robusta.core.sinks.msteams.msteams_sink_params import MsTeamsSinkConfigWrapper
|
|
15
|
+
from robusta.core.sinks.msteams_graph.msteams_graph_sink_params import MsTeamsGraphSinkConfigWrapper
|
|
15
16
|
from robusta.core.sinks.opsgenie.opsgenie_sink_params import OpsGenieSinkConfigWrapper
|
|
16
17
|
from robusta.core.sinks.pagerduty.pagerduty_sink_params import PagerdutyConfigWrapper
|
|
17
18
|
from robusta.core.sinks.robusta.robusta_sink_params import RobustaSinkConfigWrapper
|
|
@@ -60,6 +61,7 @@ class RunnerConfig(BaseModel):
|
|
|
60
61
|
DataDogSinkConfigWrapper,
|
|
61
62
|
KafkaSinkConfigWrapper,
|
|
62
63
|
MsTeamsSinkConfigWrapper,
|
|
64
|
+
MsTeamsGraphSinkConfigWrapper,
|
|
63
65
|
RocketchatSinkConfigWrapper,
|
|
64
66
|
OpsGenieSinkConfigWrapper,
|
|
65
67
|
TelegramSinkConfigWrapper,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Dict, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
from robusta.core.reporting.base import Finding
|
|
5
|
+
from robusta.core.sinks.common.channel_transformer import ChannelTransformer
|
|
6
|
+
from robusta.integrations.kubernetes.api_client_utils import get_namespace_annotations, get_namespace_labels
|
|
7
|
+
|
|
8
|
+
# Sentinel passed as default_channel to ChannelTransformer.template() so we can detect
|
|
9
|
+
# the "any token missing" case from the outside. ChannelTransformer returns the default
|
|
10
|
+
# we pass in only when the override is empty (we filter that case ourselves) or when a
|
|
11
|
+
# referenced label/annotation key is missing — both of which we treat as unresolved here.
|
|
12
|
+
_UNRESOLVED = "__robusta_destination_unresolved__"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def resolve_destination_override(
|
|
16
|
+
finding: Finding,
|
|
17
|
+
cluster_name: str,
|
|
18
|
+
default_destination: str,
|
|
19
|
+
destination_override: Optional[str],
|
|
20
|
+
namespace_destination_override: Optional[str],
|
|
21
|
+
send_to_default_if_missing: bool,
|
|
22
|
+
) -> Optional[str]:
|
|
23
|
+
"""Pick the destination (room, channel, ...) for a finding.
|
|
24
|
+
|
|
25
|
+
Evaluated in order:
|
|
26
|
+
1. namespace_destination_override, resolved against the finding namespace's labels/annotations
|
|
27
|
+
2. destination_override, resolved against the finding subject's labels/annotations
|
|
28
|
+
3. default_destination, or None (drop) when an override is configured, didn't resolve,
|
|
29
|
+
and send_to_default_if_missing is False
|
|
30
|
+
"""
|
|
31
|
+
if namespace_destination_override and finding.subject.namespace:
|
|
32
|
+
ns_labels, ns_annotations = _get_namespace_metadata(finding.subject.namespace)
|
|
33
|
+
resolved = ChannelTransformer.template(
|
|
34
|
+
namespace_destination_override,
|
|
35
|
+
_UNRESOLVED,
|
|
36
|
+
cluster_name,
|
|
37
|
+
ns_labels,
|
|
38
|
+
ns_annotations,
|
|
39
|
+
)
|
|
40
|
+
if resolved != _UNRESOLVED:
|
|
41
|
+
return resolved
|
|
42
|
+
|
|
43
|
+
if destination_override:
|
|
44
|
+
resolved = ChannelTransformer.template(
|
|
45
|
+
destination_override,
|
|
46
|
+
_UNRESOLVED,
|
|
47
|
+
cluster_name,
|
|
48
|
+
finding.subject.labels or {},
|
|
49
|
+
finding.subject.annotations or {},
|
|
50
|
+
)
|
|
51
|
+
if resolved != _UNRESOLVED:
|
|
52
|
+
return resolved
|
|
53
|
+
|
|
54
|
+
if not destination_override and not namespace_destination_override:
|
|
55
|
+
return default_destination
|
|
56
|
+
|
|
57
|
+
return default_destination if send_to_default_if_missing else None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _get_namespace_metadata(namespace: str) -> Tuple[Dict[str, str], Dict[str, str]]:
|
|
61
|
+
try:
|
|
62
|
+
labels = get_namespace_labels(namespace) or {}
|
|
63
|
+
annotations = get_namespace_annotations(namespace) or {}
|
|
64
|
+
except KeyError:
|
|
65
|
+
logging.debug("namespace %s not found in cache", namespace)
|
|
66
|
+
return {}, {}
|
|
67
|
+
return labels, annotations
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional, Tuple
|
|
3
|
+
|
|
4
|
+
from robusta.core.reporting import Finding
|
|
5
|
+
from robusta.core.sinks.common.destination_override import resolve_destination_override
|
|
6
|
+
from robusta.core.sinks.msteams_graph.msteams_graph_sink_params import MsTeamsGraphSinkConfigWrapper
|
|
7
|
+
from robusta.core.sinks.sink_base import SinkBase
|
|
8
|
+
from robusta.integrations.msteams.graph.graph_client import MsTeamsGraphClient
|
|
9
|
+
from robusta.integrations.msteams.graph.sender import MsTeamsGraphSender
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MsTeamsGraphSink(SinkBase):
|
|
13
|
+
def __init__(self, sink_config: MsTeamsGraphSinkConfigWrapper, registry):
|
|
14
|
+
super().__init__(sink_config.ms_teams_graph_sink, registry)
|
|
15
|
+
params = sink_config.ms_teams_graph_sink
|
|
16
|
+
|
|
17
|
+
client = MsTeamsGraphClient(
|
|
18
|
+
tenant_id=params.tenant_id,
|
|
19
|
+
client_id=params.client_id,
|
|
20
|
+
username=params.username,
|
|
21
|
+
password=params.password.get_secret_value(),
|
|
22
|
+
client_secret=params.client_secret.get_secret_value() if params.client_secret else None,
|
|
23
|
+
scope=params.oauth_scope,
|
|
24
|
+
authority_host=params.authority_host,
|
|
25
|
+
graph_endpoint=params.graph_endpoint,
|
|
26
|
+
)
|
|
27
|
+
self.sender = MsTeamsGraphSender(
|
|
28
|
+
client=client,
|
|
29
|
+
account_id=self.account_id,
|
|
30
|
+
cluster_name=self.cluster_name,
|
|
31
|
+
prefer_redirect_to_platform=params.prefer_redirect_to_platform,
|
|
32
|
+
send_files=params.send_files,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def write_finding(self, finding: Finding, platform_enabled: bool):
|
|
36
|
+
destination = self._resolve_destination(finding)
|
|
37
|
+
if destination is None:
|
|
38
|
+
logging.debug(f"no Teams channel resolved for {finding.title}, dropping it")
|
|
39
|
+
return
|
|
40
|
+
team_id, channel_id = destination
|
|
41
|
+
if self.params.disable_platform_links:
|
|
42
|
+
platform_enabled = False
|
|
43
|
+
self.sender.send_finding(finding, platform_enabled, team_id=team_id, channel_id=channel_id)
|
|
44
|
+
|
|
45
|
+
def _resolve_destination(self, finding: Finding) -> Optional[Tuple[str, str]]:
|
|
46
|
+
channel = resolve_destination_override(
|
|
47
|
+
finding,
|
|
48
|
+
self.cluster_name,
|
|
49
|
+
self.params.channel_id,
|
|
50
|
+
self.params.channel_override,
|
|
51
|
+
self.params.namespace_channel_override,
|
|
52
|
+
self.params.send_to_default_if_missing,
|
|
53
|
+
)
|
|
54
|
+
if channel is None:
|
|
55
|
+
return None
|
|
56
|
+
return self.split_team_and_channel(channel.strip(), self.params.team_id)
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def split_team_and_channel(destination: str, default_team_id: str) -> Tuple[str, str]:
|
|
60
|
+
# team ids are GUIDs and channel ids (19:...@thread.tacv2) never contain "/"
|
|
61
|
+
if "/" in destination:
|
|
62
|
+
team_id, channel_id = destination.split("/", 1)
|
|
63
|
+
return team_id.strip(), channel_id.strip()
|
|
64
|
+
return default_team_id, destination
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic.v1 import SecretStr, validator
|
|
4
|
+
|
|
5
|
+
from robusta.core.sinks.common.channel_transformer import ChannelTransformer
|
|
6
|
+
from robusta.core.sinks.sink_base_params import SinkBaseParams
|
|
7
|
+
from robusta.core.sinks.sink_config import SinkConfigBase
|
|
8
|
+
from robusta.integrations.msteams.graph.graph_client import DEFAULT_AUTHORITY_HOST, DEFAULT_GRAPH_ENDPOINT
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MsTeamsGraphSinkParams(SinkBaseParams):
|
|
12
|
+
tenant_id: str
|
|
13
|
+
client_id: str
|
|
14
|
+
client_secret: Optional[SecretStr] = None
|
|
15
|
+
username: str
|
|
16
|
+
password: SecretStr
|
|
17
|
+
# Default destination. Overrides may resolve to "<channel_id>" (in team_id) or "<team_id>/<channel_id>"
|
|
18
|
+
team_id: str
|
|
19
|
+
channel_id: str
|
|
20
|
+
channel_override: Optional[str] = None
|
|
21
|
+
namespace_channel_override: Optional[str] = None
|
|
22
|
+
send_to_default_if_missing: bool = True
|
|
23
|
+
disable_platform_links: bool = False
|
|
24
|
+
send_files: bool = True
|
|
25
|
+
oauth_scope: Optional[str] = None # defaults to <graph_endpoint>/.default
|
|
26
|
+
authority_host: str = DEFAULT_AUTHORITY_HOST
|
|
27
|
+
graph_endpoint: str = DEFAULT_GRAPH_ENDPOINT
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def _get_sink_type(cls):
|
|
31
|
+
return "msteams_graph"
|
|
32
|
+
|
|
33
|
+
@validator("channel_override", "namespace_channel_override")
|
|
34
|
+
def validate_overrides(cls, v):
|
|
35
|
+
return ChannelTransformer.validate_channel_override(v)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MsTeamsGraphSinkConfigWrapper(SinkConfigBase):
|
|
39
|
+
ms_teams_graph_sink: MsTeamsGraphSinkParams
|
|
40
|
+
|
|
41
|
+
def get_params(self) -> SinkBaseParams:
|
|
42
|
+
return self.ms_teams_graph_sink
|
|
@@ -12,6 +12,7 @@ from robusta.core.sinks.mail.mail_sink import MailSink
|
|
|
12
12
|
from robusta.core.sinks.mail.mail_sink_params import MailSinkConfigWrapper
|
|
13
13
|
from robusta.core.sinks.mattermost import MattermostSink, MattermostSinkConfigWrapper
|
|
14
14
|
from robusta.core.sinks.msteams import MsTeamsSink, MsTeamsSinkConfigWrapper
|
|
15
|
+
from robusta.core.sinks.msteams_graph import MsTeamsGraphSink, MsTeamsGraphSinkConfigWrapper
|
|
15
16
|
from robusta.core.sinks.opsgenie import OpsGenieSink, OpsGenieSinkConfigWrapper
|
|
16
17
|
from robusta.core.sinks.pagerduty import PagerdutyConfigWrapper, PagerdutySink
|
|
17
18
|
from robusta.core.sinks.robusta import RobustaSink, RobustaSinkConfigWrapper
|
|
@@ -41,6 +42,7 @@ class SinkFactory:
|
|
|
41
42
|
RocketchatSinkConfigWrapper: RocketchatSink,
|
|
42
43
|
RobustaSinkConfigWrapper: RobustaSink,
|
|
43
44
|
MsTeamsSinkConfigWrapper: MsTeamsSink,
|
|
45
|
+
MsTeamsGraphSinkConfigWrapper: MsTeamsGraphSink,
|
|
44
46
|
KafkaSinkConfigWrapper: KafkaSink,
|
|
45
47
|
DataDogSinkConfigWrapper: DataDogSink,
|
|
46
48
|
DiscordSinkConfigWrapper: DiscordSink,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from robusta.core.reporting.base import Finding
|
|
4
|
+
from robusta.core.sinks.common.destination_override import resolve_destination_override
|
|
5
|
+
from robusta.core.sinks.sink_base import SinkBase
|
|
6
|
+
from robusta.core.sinks.webex.webex_sink_params import WebexSinkConfigWrapper
|
|
7
|
+
from robusta.integrations.webex.sender import WebexSender
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WebexSink(SinkBase):
|
|
11
|
+
def __init__(self, sink_config: WebexSinkConfigWrapper, registry):
|
|
12
|
+
super().__init__(sink_config.webex_sink, registry)
|
|
13
|
+
|
|
14
|
+
self.sender = WebexSender(
|
|
15
|
+
bot_access_token=sink_config.webex_sink.bot_access_token,
|
|
16
|
+
room_id=sink_config.webex_sink.room_id,
|
|
17
|
+
account_id=self.account_id,
|
|
18
|
+
cluster_name=self.cluster_name,
|
|
19
|
+
webex_params=self.params,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def write_finding(self, finding: Finding, platform_enabled: bool):
|
|
23
|
+
room_id = self._resolve_room_id(finding)
|
|
24
|
+
if room_id is None:
|
|
25
|
+
return
|
|
26
|
+
if self.params.disable_platform_links:
|
|
27
|
+
platform_enabled = False
|
|
28
|
+
self.sender.send_finding_to_webex(finding, platform_enabled, room_id=room_id)
|
|
29
|
+
|
|
30
|
+
def _resolve_room_id(self, finding: Finding) -> Optional[str]:
|
|
31
|
+
return resolve_destination_override(
|
|
32
|
+
finding,
|
|
33
|
+
self.cluster_name,
|
|
34
|
+
self.params.room_id,
|
|
35
|
+
self.params.room_id_override,
|
|
36
|
+
self.params.namespace_room_id_override,
|
|
37
|
+
self.params.send_to_default_if_missing,
|
|
38
|
+
)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from urllib.parse import quote, unquote
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
DEFAULT_AUTHORITY_HOST = "https://login.microsoftonline.com"
|
|
10
|
+
DEFAULT_GRAPH_ENDPOINT = "https://graph.microsoft.com"
|
|
11
|
+
REQUEST_TIMEOUT_SEC = 30
|
|
12
|
+
# refresh the access token a bit before it expires, so a message is never sent with an expiring token
|
|
13
|
+
TOKEN_EXPIRY_MARGIN_SEC = 300
|
|
14
|
+
# after a failed sign-in, don't retry on every alert: repeated bad passwords trigger Entra ID smart lockout
|
|
15
|
+
SIGN_IN_FAILURE_BACKOFF_SEC = 300
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MsTeamsGraphError(Exception):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MsTeamsGraphClient:
|
|
23
|
+
"""Posts Teams channel messages via the Microsoft Graph API.
|
|
24
|
+
|
|
25
|
+
Graph only supports posting channel messages with delegated permissions (application permissions are
|
|
26
|
+
limited to message migration), so the token is acquired on behalf of a service user with the
|
|
27
|
+
Resource Owner Password Credentials (ROPC) flow. The user must be a member of every target team,
|
|
28
|
+
and must not require interactive MFA.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
tenant_id: str,
|
|
34
|
+
client_id: str,
|
|
35
|
+
username: str,
|
|
36
|
+
password: str,
|
|
37
|
+
client_secret: Optional[str] = None,
|
|
38
|
+
scope: Optional[str] = None,
|
|
39
|
+
authority_host: str = DEFAULT_AUTHORITY_HOST,
|
|
40
|
+
graph_endpoint: str = DEFAULT_GRAPH_ENDPOINT,
|
|
41
|
+
):
|
|
42
|
+
self.tenant_id = tenant_id
|
|
43
|
+
self.client_id = client_id
|
|
44
|
+
self.username = username
|
|
45
|
+
self.password = password
|
|
46
|
+
self.client_secret = client_secret
|
|
47
|
+
self.authority_host = authority_host.rstrip("/")
|
|
48
|
+
self.graph_endpoint = graph_endpoint.rstrip("/")
|
|
49
|
+
self.scope = scope or f"{self.graph_endpoint}/.default"
|
|
50
|
+
|
|
51
|
+
self._lock = threading.Lock()
|
|
52
|
+
self._access_token: Optional[str] = None
|
|
53
|
+
self._expires_at: float = 0
|
|
54
|
+
self._next_sign_in_attempt: float = 0
|
|
55
|
+
|
|
56
|
+
def _get_access_token(self, force_refresh: bool = False) -> str:
|
|
57
|
+
with self._lock:
|
|
58
|
+
if not force_refresh and self._access_token and time.time() < self._expires_at - TOKEN_EXPIRY_MARGIN_SEC:
|
|
59
|
+
return self._access_token
|
|
60
|
+
if time.time() < self._next_sign_in_attempt:
|
|
61
|
+
raise MsTeamsGraphError(
|
|
62
|
+
f"Skipping Graph sign-in for {self.username}, it failed recently. "
|
|
63
|
+
f"Retrying in {int(self._next_sign_in_attempt - time.time())}s"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
data = {
|
|
67
|
+
"grant_type": "password",
|
|
68
|
+
"client_id": self.client_id,
|
|
69
|
+
"username": self.username,
|
|
70
|
+
"password": self.password,
|
|
71
|
+
"scope": self.scope,
|
|
72
|
+
}
|
|
73
|
+
if self.client_secret:
|
|
74
|
+
data["client_secret"] = self.client_secret
|
|
75
|
+
|
|
76
|
+
response = requests.post(
|
|
77
|
+
f"{self.authority_host}/{self.tenant_id}/oauth2/v2.0/token", data=data, timeout=REQUEST_TIMEOUT_SEC
|
|
78
|
+
)
|
|
79
|
+
if not response.ok:
|
|
80
|
+
self._next_sign_in_attempt = time.time() + SIGN_IN_FAILURE_BACKOFF_SEC
|
|
81
|
+
raise MsTeamsGraphError(
|
|
82
|
+
f"Failed to get a Graph access token for {self.username}: "
|
|
83
|
+
f"{response.status_code} {self.error_message(response)}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
body = response.json()
|
|
87
|
+
self._access_token = body["access_token"]
|
|
88
|
+
self._expires_at = time.time() + int(body.get("expires_in", 3600))
|
|
89
|
+
return self._access_token
|
|
90
|
+
|
|
91
|
+
def post_channel_message(self, team_id: str, channel_id: str, message: dict) -> requests.Response:
|
|
92
|
+
# channel ids look like 19:abc@thread.tacv2, and are often copied URL-encoded from the Teams channel link
|
|
93
|
+
url = (
|
|
94
|
+
f"{self.graph_endpoint}/v1.0/teams/{quote(unquote(team_id), safe='')}"
|
|
95
|
+
f"/channels/{quote(unquote(channel_id), safe='')}/messages"
|
|
96
|
+
)
|
|
97
|
+
response = self._post(url, message, force_refresh=False)
|
|
98
|
+
if response.status_code == 401:
|
|
99
|
+
response = self._post(url, message, force_refresh=True)
|
|
100
|
+
return response
|
|
101
|
+
|
|
102
|
+
def _post(self, url: str, message: dict, force_refresh: bool) -> requests.Response:
|
|
103
|
+
headers = {"Authorization": f"Bearer {self._get_access_token(force_refresh)}"}
|
|
104
|
+
return requests.post(url, json=message, headers=headers, timeout=REQUEST_TIMEOUT_SEC)
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def error_message(response: requests.Response) -> str:
|
|
108
|
+
try:
|
|
109
|
+
body = response.json()
|
|
110
|
+
except ValueError:
|
|
111
|
+
return response.text
|
|
112
|
+
error = body.get("error")
|
|
113
|
+
if isinstance(error, dict): # Graph errors
|
|
114
|
+
return error.get("message", response.text)
|
|
115
|
+
if body.get("error_description"): # Entra ID token errors
|
|
116
|
+
return f"{error}: {body['error_description']}"
|
|
117
|
+
logging.debug("unexpected Graph error body %s", body)
|
|
118
|
+
return response.text
|
|
119
|
+
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from robusta.core.reporting import FileBlock, Finding, FindingSeverity
|
|
8
|
+
from robusta.core.reporting.base import Emojis, FindingStatus, LinkType
|
|
9
|
+
from robusta.core.reporting.consts import FindingSource
|
|
10
|
+
from robusta.core.reporting.url_helpers import convert_prom_graph_url_to_robusta_metrics_explorer
|
|
11
|
+
from robusta.core.reporting.utils import JPG_SUFFIX, PNG_SUFFIX, convert_svg_to_png, file_suffix_match, is_image
|
|
12
|
+
from robusta.integrations.msteams.msteams_elements.msteams_base import MsTeamsBase
|
|
13
|
+
from robusta.integrations.msteams.msteams_elements.msteams_images import MsTeamsImages
|
|
14
|
+
from robusta.integrations.msteams.msteams_elements.msteams_text_block import MsTeamsTextBlock
|
|
15
|
+
from robusta.integrations.msteams.msteams_msg import MsTeamsMsg
|
|
16
|
+
|
|
17
|
+
ADAPTIVE_CARD_CONTENT_TYPE = "application/vnd.microsoft.card.adaptive"
|
|
18
|
+
# Teams only shows a handful of card buttons, keep the first ones (Investigate, Silence, the finding links)
|
|
19
|
+
MAX_CARD_ACTIONS = 6
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MsTeamsGraphMsg(MsTeamsMsg):
|
|
23
|
+
"""Builds a Teams channel message (chatMessage) for the Graph API.
|
|
24
|
+
|
|
25
|
+
Reuses the adaptive card rendering of the webhook sink for the enrichment blocks, with a Slack like header:
|
|
26
|
+
a status line, a colored title container, the source cluster, the description, and link buttons.
|
|
27
|
+
Images are sent as message hostedContents rather than inline base64, which keeps them out of the
|
|
28
|
+
~28KB message size limit.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, prefer_redirect_to_platform: bool):
|
|
32
|
+
super().__init__(prefer_redirect_to_platform)
|
|
33
|
+
self.hosted_contents: List[dict] = []
|
|
34
|
+
|
|
35
|
+
def write_title_and_desc(self, platform_enabled: bool, finding: Finding, cluster_name: str, account_id: str):
|
|
36
|
+
status = FindingStatus.RESOLVED if finding.title.startswith("[RESOLVED]") else FindingStatus.FIRING
|
|
37
|
+
title = finding.title.removeprefix("[RESOLVED] ")
|
|
38
|
+
severity = finding.severity
|
|
39
|
+
|
|
40
|
+
header_items = [
|
|
41
|
+
MsTeamsTextBlock(
|
|
42
|
+
f"{self.__status_name(finding.source, status)} "
|
|
43
|
+
f"{severity.to_emoji()} **{severity.name.capitalize()}**"
|
|
44
|
+
).get_map_value(),
|
|
45
|
+
MsTeamsTextBlock(title, weight="bolder", font_size="large").get_map_value(),
|
|
46
|
+
]
|
|
47
|
+
self._write_to_entire_msg(
|
|
48
|
+
[
|
|
49
|
+
MsTeamsBase(
|
|
50
|
+
{
|
|
51
|
+
"type": "Container",
|
|
52
|
+
"style": self.__card_style(status, severity),
|
|
53
|
+
"bleed": True,
|
|
54
|
+
"items": header_items,
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
]
|
|
58
|
+
)
|
|
59
|
+
# link buttons right under the title, like Slack
|
|
60
|
+
actions = self.__actions(platform_enabled, finding, cluster_name, account_id)
|
|
61
|
+
if actions:
|
|
62
|
+
self._write_to_entire_msg([MsTeamsBase({"type": "ActionSet", "actions": actions})])
|
|
63
|
+
self._write_to_entire_msg([MsTeamsTextBlock(f"**Source:** {cluster_name}")])
|
|
64
|
+
|
|
65
|
+
if finding.description:
|
|
66
|
+
if finding.source == FindingSource.PROMETHEUS:
|
|
67
|
+
description = f"{Emojis.Alert.value} **Alert:** {finding.description}"
|
|
68
|
+
elif finding.source == FindingSource.KUBERNETES_API_SERVER:
|
|
69
|
+
description = f"{Emojis.K8Notification.value} **K8s event detected:** {finding.description}"
|
|
70
|
+
else:
|
|
71
|
+
description = f"{Emojis.K8Notification.value} **Notification:** {finding.description}"
|
|
72
|
+
self._write_to_entire_msg([MsTeamsTextBlock(description)])
|
|
73
|
+
|
|
74
|
+
def __actions(self, platform_enabled: bool, finding: Finding, cluster_name: str, account_id: str) -> List[dict]:
|
|
75
|
+
links = []
|
|
76
|
+
if platform_enabled:
|
|
77
|
+
links.append(("Investigate 🔎", finding.get_investigate_uri(account_id, cluster_name)))
|
|
78
|
+
if finding.add_silence_url:
|
|
79
|
+
links.append(("Configure Silences 🔕", finding.get_prometheus_silence_url(account_id, cluster_name)))
|
|
80
|
+
|
|
81
|
+
for link in finding.links:
|
|
82
|
+
link_url = link.url
|
|
83
|
+
if link.type == LinkType.PROMETHEUS_GENERATOR_URL and self.prefer_redirect_to_platform and platform_enabled:
|
|
84
|
+
link_url = convert_prom_graph_url_to_robusta_metrics_explorer(link.url, cluster_name, account_id)
|
|
85
|
+
links.append((link.link_text, link_url))
|
|
86
|
+
|
|
87
|
+
if len(links) > MAX_CARD_ACTIONS:
|
|
88
|
+
logging.debug(f"Teams card for {finding.title} has {len(links)} links, keeping the first {MAX_CARD_ACTIONS}")
|
|
89
|
+
return [{"type": "Action.OpenUrl", "title": title, "url": url} for title, url in links[:MAX_CARD_ACTIONS]]
|
|
90
|
+
|
|
91
|
+
def upload_files(self, file_blocks: List[FileBlock]):
|
|
92
|
+
# text files are rendered in the card, the same as the webhook sink
|
|
93
|
+
super().upload_files([block for block in file_blocks if not is_image(block.filename)])
|
|
94
|
+
|
|
95
|
+
image_urls = [self.__add_hosted_image(block) for block in file_blocks if is_image(block.filename)]
|
|
96
|
+
image_urls = [url for url in image_urls if url]
|
|
97
|
+
if image_urls:
|
|
98
|
+
self._sub_section_separator()
|
|
99
|
+
self._write_to_current_section([MsTeamsImages(image_urls)])
|
|
100
|
+
|
|
101
|
+
def __add_hosted_image(self, block: FileBlock) -> Optional[str]:
|
|
102
|
+
if file_suffix_match(block.filename, PNG_SUFFIX):
|
|
103
|
+
content_type, contents = "image/png", block.contents
|
|
104
|
+
elif file_suffix_match(block.filename, JPG_SUFFIX):
|
|
105
|
+
content_type, contents = "image/jpeg", block.contents
|
|
106
|
+
else:
|
|
107
|
+
content_type, contents = "image/png", convert_svg_to_png(block.contents)
|
|
108
|
+
if contents is None:
|
|
109
|
+
logging.warning(f"failed to convert {block.filename} to png, not sending it to Teams")
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
temporary_id = str(len(self.hosted_contents) + 1)
|
|
113
|
+
self.hosted_contents.append(
|
|
114
|
+
{
|
|
115
|
+
"@microsoft.graph.temporaryId": temporary_id,
|
|
116
|
+
"contentBytes": base64.b64encode(contents).decode("utf-8"),
|
|
117
|
+
"contentType": content_type,
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
return f"../hostedContents/{temporary_id}/$value"
|
|
121
|
+
|
|
122
|
+
def build_message(self) -> dict:
|
|
123
|
+
card = self.build_card()["attachments"][0]["content"]
|
|
124
|
+
attachment_id = uuid.uuid4().hex
|
|
125
|
+
message = {
|
|
126
|
+
"body": {"contentType": "html", "content": f'<attachment id="{attachment_id}"></attachment>'},
|
|
127
|
+
"attachments": [
|
|
128
|
+
{
|
|
129
|
+
"id": attachment_id,
|
|
130
|
+
"contentType": ADAPTIVE_CARD_CONTENT_TYPE,
|
|
131
|
+
"contentUrl": None,
|
|
132
|
+
"content": json.dumps(card),
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
}
|
|
136
|
+
if self.hosted_contents:
|
|
137
|
+
message["hostedContents"] = self.hosted_contents
|
|
138
|
+
return message
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def without_images(message: dict) -> dict:
|
|
142
|
+
"""A copy of the message without its hosted images, used when Teams rejects them."""
|
|
143
|
+
card = json.loads(message["attachments"][0]["content"])
|
|
144
|
+
card["body"] = [element for element in card["body"] if element.get("type") != "ImageSet"]
|
|
145
|
+
message = {k: v for k, v in message.items() if k != "hostedContents"}
|
|
146
|
+
message["attachments"] = [{**message["attachments"][0], "content": json.dumps(card)}]
|
|
147
|
+
return message
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def __status_name(source: FindingSource, status: FindingStatus) -> str:
|
|
151
|
+
if source == FindingSource.PROMETHEUS:
|
|
152
|
+
if status == FindingStatus.FIRING:
|
|
153
|
+
return f"{status.to_emoji()} **Prometheus Alert Firing** {status.to_emoji()}"
|
|
154
|
+
return f"{status.to_emoji()} **Prometheus resolved**"
|
|
155
|
+
if source == FindingSource.KUBERNETES_API_SERVER:
|
|
156
|
+
return "👀 **K8s event detected**"
|
|
157
|
+
return "👀 **Notification**"
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def __card_style(status: FindingStatus, severity: FindingSeverity) -> str:
|
|
161
|
+
if status == FindingStatus.RESOLVED:
|
|
162
|
+
return "good"
|
|
163
|
+
if severity == FindingSeverity.HIGH:
|
|
164
|
+
return "attention"
|
|
165
|
+
if severity == FindingSeverity.LOW:
|
|
166
|
+
return "warning"
|
|
167
|
+
return "accent"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from robusta.core.reporting import Finding
|
|
4
|
+
from robusta.integrations.msteams.graph.graph_client import MsTeamsGraphClient
|
|
5
|
+
from robusta.integrations.msteams.graph.graph_msg import MsTeamsGraphMsg
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MsTeamsGraphSender:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
client: MsTeamsGraphClient,
|
|
12
|
+
account_id: str,
|
|
13
|
+
cluster_name: str,
|
|
14
|
+
prefer_redirect_to_platform: bool,
|
|
15
|
+
send_files: bool,
|
|
16
|
+
):
|
|
17
|
+
self.client = client
|
|
18
|
+
self.account_id = account_id
|
|
19
|
+
self.cluster_name = cluster_name
|
|
20
|
+
self.prefer_redirect_to_platform = prefer_redirect_to_platform
|
|
21
|
+
self.send_files = send_files
|
|
22
|
+
|
|
23
|
+
def send_finding(self, finding: Finding, platform_enabled: bool, team_id: str, channel_id: str):
|
|
24
|
+
msg = MsTeamsGraphMsg(self.prefer_redirect_to_platform)
|
|
25
|
+
msg.write_title_and_desc(platform_enabled, finding, self.cluster_name, self.account_id)
|
|
26
|
+
msg.write_enrichments(finding.enrichments, self.send_files)
|
|
27
|
+
message = msg.build_message()
|
|
28
|
+
|
|
29
|
+
response = self.client.post_channel_message(team_id, channel_id, message)
|
|
30
|
+
if response.status_code in (400, 413) and "hostedContents" in message:
|
|
31
|
+
logging.warning(
|
|
32
|
+
f"Teams rejected finding {finding.title} with images ({response.status_code} "
|
|
33
|
+
f"{self.client.error_message(response)}), sending it without them"
|
|
34
|
+
)
|
|
35
|
+
response = self.client.post_channel_message(team_id, channel_id, MsTeamsGraphMsg.without_images(message))
|
|
36
|
+
|
|
37
|
+
if not response.ok:
|
|
38
|
+
logging.error(
|
|
39
|
+
f"Failed to send finding {finding.title} to Teams channel {channel_id} in team {team_id}: "
|
|
40
|
+
f"{response.status_code} {self.client.error_message(response)}"
|
|
41
|
+
)
|