diff --git a/keep/api/bl/maintenance_windows_bl.py b/keep/api/bl/maintenance_windows_bl.py index d6415cf99..feec78448 100644 --- a/keep/api/bl/maintenance_windows_bl.py +++ b/keep/api/bl/maintenance_windows_bl.py @@ -72,10 +72,19 @@ def check_if_alert_in_maintenance_windows(self, alert: AlertDto) -> bool: try: cel_result = prgm.evaluate(activation) except celpy.evaluation.CELEvalError as e: - if "no such member" in str(e): + error_msg = str(e).lower() + if "no such member" in error_msg or "undeclared reference" in error_msg: + self.logger.debug( + f"Skipping maintenance window rule due to missing field: {str(e)}", + extra={**extra, "maintenance_rule_id": maintenance_rule.id}, + ) continue - # wtf - raise + # Log unexpected CEL errors but don't fail the entire event processing + self.logger.error( + f"Unexpected CEL evaluation error: {str(e)}", + extra={**extra, "maintenance_rule_id": maintenance_rule.id}, + ) + continue if cel_result: self.logger.info( "Alert is in maintenance window", diff --git a/keep/api/routes/metrics.py b/keep/api/routes/metrics.py index 4eff4d52c..8c041fb99 100644 --- a/keep/api/routes/metrics.py +++ b/keep/api/routes/metrics.py @@ -127,9 +127,9 @@ def get_metrics( for label in labels: label_value = chevron.render("{{ " + label + " }}", last_alert_dto) label = label.replace(".", "_") - extra_labels += f' {label}="{label_value}"' + extra_labels += f',{label}="{label_value}"' - export += f'alerts_total{{incident_name="{incident_name}" incident_id="{incident.id}"{extra_labels}}} {incident.alerts_count}\n' + export += f'alerts_total{{incident_name="{incident_name}",incident_id="{incident.id}"{extra_labels}}} {incident.alerts_count}\n' # Exporting stats about open incidents export += "\n\n" diff --git a/keep/providers/kibana_provider/kibana_provider.py b/keep/providers/kibana_provider/kibana_provider.py index a3d89d23a..a4d67dbc9 100644 --- a/keep/providers/kibana_provider/kibana_provider.py +++ b/keep/providers/kibana_provider/kibana_provider.py @@ -67,6 +67,7 @@ class KibanaProvider(BaseProvider): } } ) + SIEM_WEBHOOK_PAYLOAD = """{{#context.alerts}}{{{.}}}{{/context.alerts}}""" # Mock payloads for validating scopes MOCK_ALERT_PAYLOAD = { @@ -373,6 +374,7 @@ def __setup_webhook_alerts(self, tenant_id: str, keep_api_url: str, api_key: str self.logger.info(f"Alert {alert_rule['id']} already updated, skipping") continue + rule_type_id = alert_rule.get("rule_type_id") action_groups = rule_types.get(alert_rule["rule_type_id"], {}).get( "action_groups", [] ) @@ -381,7 +383,14 @@ def __setup_webhook_alerts(self, tenant_id: str, keep_api_url: str, api_key: str { "group": action_group.get("id"), "id": connector_id, - "params": {"body": KibanaProvider.WEBHOOK_PAYLOAD}, + "params": { + # SIEM can use a different payload for more context + "body": ( + KibanaProvider.WEBHOOK_PAYLOAD + if "siem" not in rule_type_id + else KibanaProvider.SIEM_WEBHOOK_PAYLOAD + ) + }, "frequency": { "notify_when": "onActionGroupChange", "throttle": None, @@ -558,6 +567,49 @@ def _format_alert( if "payload" in event: return KibanaProvider.format_alert_from_watcher(event) + # SIEM alert + if "kibana" in event: + logger.info("Parsing SIEM Kibana alert") + description = ( + event.get("kibana", {}) + .get("alert", {}) + .get("rule", {}) + .get("description", "") + ) + if not description: + logger.warning("Could not find description in SIEM Kibana alert") + + name = ( + event.get("kibana", {}).get("alert", {}).get("rule", {}).get("name", "") + ) + if not name: + logger.warning("Could not find name in SIEM Kibana alert") + name = "SIEM Kibana Alert" + + status = event.get("kibana", {}).get("alert", {}).get("status", "") + if not status: + logger.warning("Could not find status in SIEM Kibana alert") + name = "active" + + # use map + status = KibanaProvider.STATUS_MAP.get(status, AlertStatus.FIRING) + severity = ( + event.get("kibana", {}) + .get("alert", {}) + .get("severity", "could not find severity") + ) + # use map + severity = KibanaProvider.SEVERITIES_MAP.get(severity, AlertSeverity.INFO) + alert_dto = AlertDto( + name=name, + description=description, + status=status, + severity=severity, + source=["kibana"], + **event, + ) + logger.info("Finished to parse SIEM Kibana alert") + return alert_dto # Check if this is the new webhook format # New Kibana webhook format if "webhook_body" in event: @@ -624,6 +676,9 @@ def _format_alert( if not event.get("url"): event.pop("url", None) + if "name" not in event: + event["name"] = event.get("rule.name") + return AlertDto( environment=environment, labels=labels, diff --git a/pyproject.toml b/pyproject.toml index 375afe99a..70153bce9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "keep" -version = "0.35.8" +version = "0.35.10" description = "Alerting. for developers, by developers." authors = ["Keep Alerting LTD"] packages = [{include = "keep"}] diff --git a/tests/test_maintenance_windows_bl.py b/tests/test_maintenance_windows_bl.py index 6f8401507..170895bd3 100644 --- a/tests/test_maintenance_windows_bl.py +++ b/tests/test_maintenance_windows_bl.py @@ -182,3 +182,19 @@ def test_alert_ignored_due_to_acknowledged_status( # Should return False because the alert status is ACKNOWLEDGED assert result is False + + +def test_alert_with_missing_cel_field(mock_session, active_maintenance_window_rule, alert_dto): + # Modify the cel_query to reference a non-existent field + active_maintenance_window_rule.cel_query = 'alertname == "test-alert"' + mock_session.query.return_value.filter.return_value.filter.return_value.filter.return_value.all.return_value = [ + active_maintenance_window_rule + ] + + maintenance_window_bl = MaintenanceWindowsBl( + tenant_id="test-tenant", session=mock_session + ) + result = maintenance_window_bl.check_if_alert_in_maintenance_windows(alert_dto) + + # Should return False because the field doesn't exist + assert result is False