diff --git a/helm/robusta/values.yaml b/helm/robusta/values.yaml index 0d35176e5..42276147e 100644 --- a/helm/robusta/values.yaml +++ b/helm/robusta/values.yaml @@ -23,7 +23,7 @@ automountServiceAccountToken: true enableHolmesGPT: false -# see https://docs.robusta.dev/master/user-guide/configuration.html#global-config and https://docs.robusta.dev/master/configuration/additional-settings.html#global-config +# see https://docs.robusta.dev/master/setup-robusta/additional-settings.html#global-config globalConfig: check_prometheus_flags: true grafana_url: "" @@ -35,7 +35,7 @@ globalConfig: custom_annotations: [] custom_severity_map: {} -# see https://docs.robusta.dev/master/user-guide/configuration/additional-settings.html#relabel-prometheus-alerts +# see https://docs.robusta.dev/master/setup-robusta/additional-settings.html#relabel-prometheus-alerts alertRelabel: [] # safe actions to enable authenticated users to run diff --git a/src/robusta/core/model/base_params.py b/src/robusta/core/model/base_params.py index 8851789b4..037c15a72 100644 --- a/src/robusta/core/model/base_params.py +++ b/src/robusta/core/model/base_params.py @@ -83,6 +83,10 @@ class ResourceInfo(BaseModel): class HolmesParams(ActionParams): holmes_url: Optional[str] model: Optional[str] + # Additional internal context that helps runner to send investigation to appropriate sinks + # for now it is used only for passing thread_ts to slack sink internally; + robusta_context: Optional[Dict[str, Any]] = None + @validator("holmes_url", allow_reuse=True) def validate_protocol(cls, v): if v and not v.startswith("http"): # if the user configured url without http(s) @@ -113,6 +117,7 @@ class AIInvestigateParams(HolmesParams): stream: bool = False + class HolmesToolsResult(BaseModel): """ :var name: Name of the tool. diff --git a/src/robusta/core/playbooks/internal/ai_integration.py b/src/robusta/core/playbooks/internal/ai_integration.py index 013bfbca0..681771139 100644 --- a/src/robusta/core/playbooks/internal/ai_integration.py +++ b/src/robusta/core/playbooks/internal/ai_integration.py @@ -112,7 +112,13 @@ def ask_holmes(event: ExecutionBaseEvent, params: AIInvestigateParams): finding.add_enrichment( [HolmesResultsBlock(holmes_result=holmes_result)], enrichment_type=EnrichmentType.ai_analysis ) - + runner_context = getattr(params, "robusta_context", None) + if runner_context: + if "thread_ts" in runner_context: + finding.robusta_context["thread_ts"] = runner_context.get("thread_ts") + if "channel_id" in runner_context: + finding.robusta_context["channel_id"] = runner_context.get("channel_id") + event.add_finding(finding) except Exception as e: diff --git a/src/robusta/core/reporting/base.py b/src/robusta/core/reporting/base.py index 9a53d0e43..12fa397cf 100644 --- a/src/robusta/core/reporting/base.py +++ b/src/robusta/core/reporting/base.py @@ -292,6 +292,7 @@ def __init__( self.starts_at = starts_at if starts_at else datetime.now() self.ends_at = ends_at self.dirty = False + self.robusta_context: Dict[str, Any] = {} @property def attribute_map(self) -> Dict[str, Union[str, Dict[str, str]]]: diff --git a/src/robusta/core/reporting/callbacks.py b/src/robusta/core/reporting/callbacks.py index 89f60500a..4c59b8de2 100644 --- a/src/robusta/core/reporting/callbacks.py +++ b/src/robusta/core/reporting/callbacks.py @@ -43,6 +43,7 @@ def create_for_func( sinks=[sink], origin="callback", ) + return ExternalActionRequest( body=body, signature=sign_action_request(body, signing_key), diff --git a/src/robusta/integrations/receiver.py b/src/robusta/integrations/receiver.py index acb0ee78d..168b398f7 100644 --- a/src/robusta/integrations/receiver.py +++ b/src/robusta/integrations/receiver.py @@ -49,10 +49,16 @@ class ValidationResponse(BaseModel): error_msg: Optional[str] = None +class SlackContainer(BaseModel): + channel_id: str + message_ts: str + + class SlackExternalActionRequest(ExternalActionRequest): # Optional Slack Params slack_username: Optional[str] = None slack_message: Optional[Any] = None + slack_container: Optional[SlackContainer] = None class SlackActionRequest(BaseModel): @@ -73,6 +79,7 @@ class SlackUserID(BaseModel): class SlackActionsMessage(BaseModel): actions: List[SlackActionRequest] user: Optional[SlackUserID] + container: Optional[SlackContainer] class ActionRequestReceiver: @@ -168,15 +175,19 @@ def __exec_external_request(self, action_request: ExternalActionRequest, validat if hasattr(action_request, 'slack_message'): action_request.body.action_params["slack_message"] = action_request.slack_message + + if hasattr(action_request, 'slack_container'): + thread_ts = action_request.slack_container.message_ts + channel_id = action_request.slack_container.channel_id + action_request.body.action_params["robusta_context"] = {"thread_ts": thread_ts, "channel_id": channel_id} response = self.event_handler.run_external_action( action_request.body.action_name, action_request.body.action_params, - action_request.body.sinks, + action_request.body.sinks, sync_response, action_request.no_sinks, ) - if sync_response: http_code = 200 if response.get("success") else 500 self.ws.send(data=json.dumps(self.__sync_response(http_code, action_request.request_id, response))) @@ -238,6 +249,7 @@ def _parse_slack_message(message: Union[str, bytes, bytearray]) -> SlackActionsM for action in slack_actions_message.actions: action.value.slack_username = slack_actions_message.user.username action.value.slack_message = json_slack_message + action.value.slack_container = slack_actions_message.container return slack_actions_message def on_message(self, ws: websocket.WebSocketApp, message: str) -> None: diff --git a/src/robusta/integrations/slack/sender.py b/src/robusta/integrations/slack/sender.py index f8a5d088e..535cb84a4 100644 --- a/src/robusta/integrations/slack/sender.py +++ b/src/robusta/integrations/slack/sender.py @@ -115,7 +115,7 @@ def __get_action_block_for_choices(self, sink: str, choices: Dict[str, CallbackC ).json(), } ) - + return [{"type": "actions", "elements": buttons}] def __to_slack_links(self, links: List[LinkProp]) -> List[SlackBlock]: @@ -568,25 +568,45 @@ def send_holmes_analysis( except Exception: logging.exception(f"error sending message to slack. {title}") + + def _resolve_slack_thread( + self, + finding: Finding, + sink_params: SlackSinkParams, + thread_ts: Optional[str] = None, + ) -> tuple[str, Optional[str]]: + + channel = ChannelTransformer.template( + sink_params.channel_override, + sink_params.slack_channel, + self.cluster_name, + finding.subject.labels, + finding.subject.annotations, + ) + + ctx = getattr(finding, "robusta_context", {}) or {} + thread_override = ctx.get("thread_ts") + channel_override = ctx.get("channel_id") + + return ( + channel_override or channel, + thread_override or thread_ts, + ) def send_finding_to_slack( self, finding: Finding, sink_params: SlackSinkParams, platform_enabled: bool, - thread_ts: str = None, + thread_ts: Optional[str] = None, ) -> str: blocks: List[BaseBlock] = [] attachment_blocks: List[BaseBlock] = [] - slack_channel = ChannelTransformer.template( - sink_params.channel_override, - sink_params.slack_channel, - self.cluster_name, - finding.subject.labels, - finding.subject.annotations, + slack_channel, thread_ts = self._resolve_slack_thread( + finding, sink_params, thread_ts ) - + if finding.finding_type == FindingType.AI_ANALYSIS: # holmes analysis message needs special handling self.send_holmes_analysis(finding, slack_channel, platform_enabled, thread_ts)