diff --git a/keep-ui/app/(keep)/alerts/alerts-rules-builder.tsx b/keep-ui/app/(keep)/alerts/alerts-rules-builder.tsx
index f9a4da2b23..b8fe987ef0 100644
--- a/keep-ui/app/(keep)/alerts/alerts-rules-builder.tsx
+++ b/keep-ui/app/(keep)/alerts/alerts-rules-builder.tsx
@@ -7,9 +7,9 @@ import QueryBuilder, {
RuleGroupType,
defaultOperators,
formatQuery,
- parseCEL,
- parseSQL,
} from "react-querybuilder";
+import { parseCEL } from "react-querybuilder/parseCEL";
+import { parseSQL } from "react-querybuilder/parseSQL";
import "react-querybuilder/dist/query-builder.scss";
import { Table } from "@tanstack/react-table";
import { FiSave } from "react-icons/fi";
@@ -512,8 +512,8 @@ export const AlertsRulesBuilder = ({
operators: getOperators(id),
}))
: customFields
- ? customFields
- : [];
+ ? customFields
+ : [];
const onImportSQL = () => {
setImportSQLOpen(true);
diff --git a/keep-ui/app/(keep)/incidents/[id]/activity/incident-activity.tsx b/keep-ui/app/(keep)/incidents/[id]/activity/incident-activity.tsx
index fa66148db4..13f04c2a3c 100644
--- a/keep-ui/app/(keep)/incidents/[id]/activity/incident-activity.tsx
+++ b/keep-ui/app/(keep)/incidents/[id]/activity/incident-activity.tsx
@@ -28,6 +28,19 @@ interface IncidentActivity {
initiator?: string | AlertDto;
}
+const ACTION_TYPES = [
+ "alert was triggered",
+ "alert acknowledged",
+ "alert automatically resolved",
+ "alert manually resolved",
+ "alert status manually changed",
+ "alert status changed by API",
+ "alert automatically resolved by API",
+ "A comment was added to the incident",
+ "Incident status changed",
+ "Incident assigned",
+];
+
function Item({
icon,
children,
@@ -41,7 +54,7 @@ function Item({
{/* vertical line */}
{/* wrapping icon to avoid vertical line visible behind transparent background */}
-
@@ -94,6 +107,7 @@ export function IncidentActivity({ incident }: { incident: IncidentDto }) {
return (
auditEvents
.concat(incidentEvents)
+ .filter((auditEvent) => ACTION_TYPES.includes(auditEvent.action))
.sort(
(a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
diff --git a/keep-ui/app/(keep)/rules/CorrelationSidebar/RuleFields.tsx b/keep-ui/app/(keep)/rules/CorrelationSidebar/RuleFields.tsx
index 3838c0167d..16d733f71b 100644
--- a/keep-ui/app/(keep)/rules/CorrelationSidebar/RuleFields.tsx
+++ b/keep-ui/app/(keep)/rules/CorrelationSidebar/RuleFields.tsx
@@ -26,6 +26,7 @@ import { useDeduplicationFields } from "@/utils/hooks/useDeduplicationRules";
const DEFAULT_OPERATORS = defaultOperators.filter((operator) =>
[
"=",
+ "!=",
"contains",
"beginsWith",
"endsWith",
diff --git a/keep-ui/app/(keep)/rules/CorrelationSidebar/RuleGroup.tsx b/keep-ui/app/(keep)/rules/CorrelationSidebar/RuleGroup.tsx
index a204924c80..d9e435ebcc 100644
--- a/keep-ui/app/(keep)/rules/CorrelationSidebar/RuleGroup.tsx
+++ b/keep-ui/app/(keep)/rules/CorrelationSidebar/RuleGroup.tsx
@@ -1,5 +1,9 @@
import { Button } from "@tremor/react";
-import { RuleGroupProps as QueryRuleGroupProps } from "react-querybuilder";
+import {
+ RuleGroupProps as QueryRuleGroupProps,
+ RuleGroupType,
+ RuleType,
+} from "react-querybuilder";
import { RuleFields } from "./RuleFields";
export const RuleGroup = ({ actions, ruleGroup }: QueryRuleGroupProps) => {
@@ -24,7 +28,12 @@ export const RuleGroup = ({ actions, ruleGroup }: QueryRuleGroupProps) => {
{groupIndex > 0 ? "OR" : ""}
,
+ string
+ >
+ }
key={rule.id}
groupIndex={groupIndex}
onRuleAdd={onRuleAdd}
diff --git a/keep-ui/app/(keep)/rules/CorrelationTable.tsx b/keep-ui/app/(keep)/rules/CorrelationTable.tsx
index 7c6cf765d4..8241bcea6f 100644
--- a/keep-ui/app/(keep)/rules/CorrelationTable.tsx
+++ b/keep-ui/app/(keep)/rules/CorrelationTable.tsx
@@ -22,7 +22,8 @@ import {
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
-import { DefaultRuleGroupType, parseCEL } from "react-querybuilder";
+import { DefaultRuleGroupType } from "react-querybuilder";
+import { parseCEL } from "react-querybuilder/parseCEL";
import { useRouter, useSearchParams } from "next/navigation";
import { FormattedQueryCell } from "./FormattedQueryCell";
import { DeleteRuleCell } from "./CorrelationSidebar/DeleteRule";
diff --git a/keep-ui/entities/presets/model/usePresetActions.ts b/keep-ui/entities/presets/model/usePresetActions.ts
index 19d294911b..30c0a7ec67 100644
--- a/keep-ui/entities/presets/model/usePresetActions.ts
+++ b/keep-ui/entities/presets/model/usePresetActions.ts
@@ -1,7 +1,8 @@
import { useApi } from "@/shared/lib/hooks/useApi";
import { showErrorToast, showSuccessToast } from "@/shared/ui";
import { useCallback } from "react";
-import { formatQuery, parseCEL } from "react-querybuilder";
+import { formatQuery } from "react-querybuilder";
+import { parseCEL } from "react-querybuilder/parseCEL";
import { Preset, PresetCreateUpdateDto } from "./types";
import { useRevalidateMultiple } from "@/shared/lib/state-utils";
import { useLocalStorage } from "@/utils/hooks/useLocalStorage";
diff --git a/keep-ui/features/change-incident-status/ui/incident-change-status-select.tsx b/keep-ui/features/change-incident-status/ui/incident-change-status-select.tsx
index 41d7febd06..4618d2ebdf 100644
--- a/keep-ui/features/change-incident-status/ui/incident-change-status-select.tsx
+++ b/keep-ui/features/change-incident-status/ui/incident-change-status-select.tsx
@@ -3,7 +3,7 @@ import { Status } from "@/entities/incidents/model";
import { STATUS_ICONS } from "@/entities/incidents/ui";
import Select, { ClassNamesConfig } from "react-select";
import { useIncidentActions } from "@/entities/incidents/model";
-import { useCallback, useEffect, useMemo, useRef } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { capitalize } from "@/utils/helpers";
const customClassNames: ClassNamesConfig = {
@@ -41,6 +41,7 @@ export function IncidentChangeStatusSelect({
}: Props) {
// Use a portal to render the menu outside the table container with overflow: hidden
const menuPortalTarget = useRef(null);
+ const [isDisabled, setIsDisabled] = useState(false);
useEffect(() => {
menuPortalTarget.current = document.body;
}, []);
@@ -48,23 +49,27 @@ export function IncidentChangeStatusSelect({
const { changeStatus } = useIncidentActions();
const statusOptions = useMemo(
() =>
- Object.values(Status).filter((status) => status != Status.Deleted || value == Status.Deleted).map((status) => ({
- value: status,
- label: (
-
- {STATUS_ICONS[status]}
- {capitalize(status)}
-
- ),
- })),
+ Object.values(Status)
+ .filter((status) => status != Status.Deleted || value == Status.Deleted)
+ .map((status) => ({
+ value: status,
+ label: (
+
+ {STATUS_ICONS[status]}
+ {capitalize(status)}
+
+ ),
+ })),
[value]
);
const handleChange = useCallback(
(option: any) => {
const _asyncUpdate = async (option: any) => {
+ setIsDisabled(true);
await changeStatus(incidentId, option?.value || null);
onChange?.(option?.value || null);
+ setIsDisabled(false);
};
_asyncUpdate(option);
},
@@ -83,6 +88,7 @@ export function IncidentChangeStatusSelect({
options={statusOptions}
value={selectedOption}
onChange={handleChange}
+ isDisabled={isDisabled}
placeholder="Status"
classNames={customClassNames}
menuPortalTarget={menuPortalTarget.current}
diff --git a/keep-ui/package-lock.json b/keep-ui/package-lock.json
index 9a52190bee..13cb2f3bb9 100644
--- a/keep-ui/package-lock.json
+++ b/keep-ui/package-lock.json
@@ -89,7 +89,7 @@
"react-name-initials-avatar": "^0.0.7",
"react-papaparse": "^4.4.0",
"react-player": "^2.16.0",
- "react-querybuilder": "^6.5.4",
+ "react-querybuilder": "^8.3.1",
"react-quill": "^2.0.0",
"react-select": "^5.8.0",
"react-sliding-side-panel": "^2.0.3",
@@ -8080,6 +8080,42 @@
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.6.1.tgz",
+ "integrity": "sha512-SSlIqZNYhqm/oMkXbtofwZSt9lrncblzo6YcZ9zoX+zLngRBrCOjK4lNLdkNucJF58RHOWrD9txT3bT3piH7Zw==",
+ "dependencies": {
+ "immer": "^10.0.3",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
"node_modules/@repeaterjs/repeater": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.6.tgz",
@@ -9784,6 +9820,11 @@
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
+ },
"node_modules/@types/uuid": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
@@ -19877,6 +19918,14 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
+ "node_modules/numeric-quantity": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/numeric-quantity/-/numeric-quantity-2.0.1.tgz",
+ "integrity": "sha512-+Bt2X6YxM5bg8XIBl76NVeG2eL0Y5VQRoyz6GLYrZXW/TDh7We+tGeX4/WZWhaVGOg5ZjNBEOZt9a86slMhOJA==",
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/nwsapi": {
"version": "2.2.16",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.16.tgz",
@@ -21713,15 +21762,54 @@
}
},
"node_modules/react-querybuilder": {
- "version": "6.5.5",
- "resolved": "https://registry.npmjs.org/react-querybuilder/-/react-querybuilder-6.5.5.tgz",
- "integrity": "sha512-i/MG1+XMmAaaVtdbyw5Pubxz3auwD6R2u9hfhskksniI1yuvJoyraF3mVeVgPQHPU9hM7H2HyVL8PxYUvsHZcg==",
+ "version": "8.3.1",
+ "resolved": "https://registry.npmjs.org/react-querybuilder/-/react-querybuilder-8.3.1.tgz",
+ "integrity": "sha512-c66+ZwoNU9LqCAv0Rmy6EId6ZFsSUARPAsYiVCBldCfon02HI/BhGWC6c+4apNlty5sAGO6wZTpO+XYMAUXDuQ==",
"dependencies": {
- "clsx": "^2.0.0",
- "immer": "^10.0.3"
+ "@reduxjs/toolkit": "^2.6.0",
+ "immer": "^10.1.1",
+ "numeric-quantity": "^2.0.1",
+ "react-redux": "^9.2.0"
},
"peerDependencies": {
- "react": ">=16.8.0"
+ "react": ">=18"
+ }
+ },
+ "node_modules/react-querybuilder/node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-querybuilder/node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/react-querybuilder/node_modules/use-sync-external-store": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
+ "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-quill": {
@@ -22551,6 +22639,11 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
+ },
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
diff --git a/keep-ui/package.json b/keep-ui/package.json
index e9c64c5e9c..c35b5e0dce 100644
--- a/keep-ui/package.json
+++ b/keep-ui/package.json
@@ -92,7 +92,7 @@
"react-name-initials-avatar": "^0.0.7",
"react-papaparse": "^4.4.0",
"react-player": "^2.16.0",
- "react-querybuilder": "^6.5.4",
+ "react-querybuilder": "^8.3.1",
"react-quill": "^2.0.0",
"react-select": "^5.8.0",
"react-sliding-side-panel": "^2.0.3",
diff --git a/keep/api/bl/enrichments_bl.py b/keep/api/bl/enrichments_bl.py
index 5add20b968..7eb8ab4aef 100644
--- a/keep/api/bl/enrichments_bl.py
+++ b/keep/api/bl/enrichments_bl.py
@@ -13,6 +13,7 @@
from sqlmodel import Session, select
from keep.api.core.config import config
+from keep.api.core.db import batch_enrich
from keep.api.core.db import enrich_entity as enrich_alert_db
from keep.api.core.db import (
get_alert_by_event_id,
@@ -34,6 +35,7 @@
)
from keep.api.models.db.extraction import ExtractionRule
from keep.api.models.db.mapping import MappingRule
+from keep.identitymanager.authenticatedentity import AuthenticatedEntity
def is_valid_uuid(uuid_str):
@@ -520,6 +522,102 @@ def _check_matcher(self, alert: AlertDto, row: dict, matcher: list) -> bool:
)
return False
+ def get_enrichment_metadata(
+ self, enrichments: dict, authenticated_entity: AuthenticatedEntity
+ ) -> tuple[str, str, bool]:
+ """
+ Get the metadata for the enrichment
+
+ Args:
+ enrichments (dict): The enrichments to get the metadata for
+ authenticated_entity (AuthenticatedEntity): The authenticated entity that performed the enrichment
+
+ Returns:
+ tuple[str, str, bool, bool]: action_type, action_description, should_run_workflow, should_check_incidents_resolution
+ """
+ should_run_workflow = False
+ should_check_incidents_resolution = False
+ action_type = ActionType.GENERIC_ENRICH
+ action_description = (
+ f"Alert enriched by {authenticated_entity.email} - {enrichments}"
+ )
+ # Shahar: TODO, change to the specific action type, good enough for now
+ if "status" in enrichments and authenticated_entity.api_key_name is None:
+ action_type = (
+ ActionType.MANUAL_RESOLVE
+ if enrichments["status"] == "resolved"
+ else ActionType.MANUAL_STATUS_CHANGE
+ )
+ action_description = f"Alert status was changed to {enrichments['status']} by {authenticated_entity.email}"
+ should_run_workflow = True
+ if enrichments["status"] == "resolved":
+ should_check_incidents_resolution = True
+ elif "status" in enrichments and authenticated_entity.api_key_name:
+ action_type = (
+ ActionType.API_AUTOMATIC_RESOLVE
+ if enrichments["status"] == "resolved"
+ else ActionType.API_STATUS_CHANGE
+ )
+ action_description = f"Alert status was changed to {enrichments['status']} by API `{authenticated_entity.api_key_name}`"
+ should_run_workflow = True
+ if enrichments["status"] == "resolved":
+ should_check_incidents_resolution = True
+ elif "note" in enrichments and enrichments["note"]:
+ action_type = ActionType.COMMENT
+ action_description = (
+ f"Comment added by {authenticated_entity.email} - {enrichments['note']}"
+ )
+ elif "ticket_url" in enrichments:
+ action_type = ActionType.TICKET_ASSIGNED
+ action_description = f"Ticket assigned by {authenticated_entity.email} - {enrichments['ticket_url']}"
+ return (
+ action_type,
+ action_description,
+ should_run_workflow,
+ should_check_incidents_resolution,
+ )
+
+ def batch_enrich(
+ self,
+ fingerprints: list[str],
+ enrichments: dict,
+ action_type: ActionType,
+ action_callee: str,
+ action_description: str,
+ dispose_on_new_alert=False,
+ audit_enabled=True,
+ ):
+ self.logger.debug(
+ "enriching multiple fingerprints",
+ extra={"fingerprints": fingerprints, "tenant_id": self.tenant_id},
+ )
+ # if these enrichments are disposable, manipulate them with a timestamp
+ # so they can be disposed of later
+ if dispose_on_new_alert:
+ self.logger.info(
+ "Enriching disposable enrichments",
+ extra={"fingerprints": fingerprints, "tenant_id": self.tenant_id},
+ )
+ # for every key, add a disposable key with the value and a timestamp
+ disposable_enrichments = {}
+ for key, value in enrichments.items():
+ disposable_enrichments[f"disposable_{key}"] = {
+ "value": value,
+ "timestamp": datetime.datetime.now(
+ tz=datetime.timezone.utc
+ ).timestamp(), # timestamp for disposal [for future use]
+ }
+ enrichments.update(disposable_enrichments)
+ batch_enrich(
+ self.tenant_id,
+ fingerprints,
+ enrichments,
+ action_type,
+ action_callee,
+ action_description,
+ audit_enabled=audit_enabled,
+ )
+
def enrich_entity(
self,
fingerprint: str,
@@ -555,7 +653,9 @@ def enrich_entity(
for key, value in enrichments.items():
disposable_enrichments[f"disposable_{key}"] = {
"value": value,
- "timestamp": datetime.datetime.utcnow().timestamp(), # timestamp for disposal [for future use]
+ "timestamp": datetime.datetime.now(
+ tz=datetime.timezone.utc
+ ).timestamp(), # timestamp for disposal [for future use]
}
enrichments.update(disposable_enrichments)
diff --git a/keep/api/core/db.py b/keep/api/core/db.py
index 27446c8124..910babb30b 100644
--- a/keep/api/core/db.py
+++ b/keep/api/core/db.py
@@ -1036,6 +1036,104 @@ def _enrich_entity(
return alert_enrichment
+def batch_enrich(
+ tenant_id,
+ fingerprints,
+ enrichments,
+ action_type: ActionType,
+ action_callee: str,
+ action_description: str,
+ session=None,
+ audit_enabled=True,
+):
+ """
+ Batch enrich multiple alerts with the same enrichments in a single transaction.
+
+ Args:
+ tenant_id (str): The tenant ID to filter the alert enrichments by.
+ fingerprints (List[str]): List of alert fingerprints to enrich.
+ enrichments (dict): The enrichments to add to all alerts.
+ action_type (ActionType): The type of action being performed.
+ action_callee (str): The ID of the user performing the action.
+ action_description (str): Description of the action.
+ session (Session, optional): Database session to use.
+ force (bool, optional): Whether to override existing enrichments. Defaults to False.
+ audit_enabled (bool, optional): Whether to create audit entries. Defaults to True.
+
+ Returns:
+ List[AlertEnrichment]: List of enriched alert objects.
+ """
+ with existed_or_new_session(session) as session:
+ # Get all existing enrichments in one query
+ existing_enrichments = {
+ e.alert_fingerprint: e
+ for e in session.exec(
+ select(AlertEnrichment)
+ .where(AlertEnrichment.tenant_id == tenant_id)
+ .where(AlertEnrichment.alert_fingerprint.in_(fingerprints))
+ ).all()
+ }
+
+ # Prepare bulk update for existing enrichments
+ to_update = []
+ to_create = []
+ audit_entries = []
+
+ for fingerprint in fingerprints:
+ existing = existing_enrichments.get(fingerprint)
+
+ if existing:
+ to_update.append(existing.id)
+ else:
+ # For new entries
+ to_create.append(
+ AlertEnrichment(
+ tenant_id=tenant_id,
+ alert_fingerprint=fingerprint,
+ enrichments=enrichments,
+ )
+ )
+
+ if audit_enabled:
+ audit_entries.append(
+ AlertAudit(
+ tenant_id=tenant_id,
+ fingerprint=fingerprint,
+ user_id=action_callee,
+ action=action_type.value,
+ description=action_description,
+ )
+ )
+
+ # Bulk update in a single query
+ if to_update:
+ stmt = (
+ update(AlertEnrichment)
+ .where(AlertEnrichment.id.in_(to_update))
+ .values(enrichments=enrichments)
+ )
+ session.execute(stmt)
+
+ # Bulk insert new enrichments
+ if to_create:
+ session.add_all(to_create)
+
+ # Bulk insert audit entries
+ if audit_entries:
+ session.add_all(audit_entries)
+
+ session.commit()
+
+ # Get all updated/created enrichments
+ result = session.exec(
+ select(AlertEnrichment)
+ .where(AlertEnrichment.tenant_id == tenant_id)
+ .where(AlertEnrichment.alert_fingerprint.in_(fingerprints))
+ ).all()
+
+ return result
+
+
def enrich_entity(
tenant_id,
fingerprint,
diff --git a/keep/api/routes/alerts.py b/keep/api/routes/alerts.py
index 1c4f0eca2e..878490032a 100644
--- a/keep/api/routes/alerts.py
+++ b/keep/api/routes/alerts.py
@@ -816,39 +816,14 @@ def _enrich_alert(
try:
enrichement_bl = EnrichmentsBl(tenant_id, db=session)
- # Shahar: TODO, change to the specific action type, good enough for now
- if (
- "status" in enrich_data.enrichments
- and authenticated_entity.api_key_name is None
- ):
- action_type = (
- ActionType.MANUAL_RESOLVE
- if enrich_data.enrichments["status"] == "resolved"
- else ActionType.MANUAL_STATUS_CHANGE
- )
- action_description = f"Alert status was changed to {enrich_data.enrichments['status']} by {authenticated_entity.email}"
- should_run_workflow = True
- if enrich_data.enrichments["status"] == "resolved":
- should_check_incidents_resolution = True
- elif "status" in enrich_data.enrichments and authenticated_entity.api_key_name:
- action_type = (
- ActionType.API_AUTOMATIC_RESOLVE
- if enrich_data.enrichments["status"] == "resolved"
- else ActionType.API_STATUS_CHANGE
- )
- action_description = f"Alert status was changed to {enrich_data.enrichments['status']} by API `{authenticated_entity.api_key_name}`"
- should_run_workflow = True
- if enrich_data.enrichments["status"] == "resolved":
- should_check_incidents_resolution = True
- elif "note" in enrich_data.enrichments and enrich_data.enrichments["note"]:
- action_type = ActionType.COMMENT
- action_description = f"Comment added by {authenticated_entity.email} - {enrich_data.enrichments['note']}"
- elif "ticket_url" in enrich_data.enrichments:
- action_type = ActionType.TICKET_ASSIGNED
- action_description = f"Ticket assigned by {authenticated_entity.email} - {enrich_data.enrichments['ticket_url']}"
- else:
- action_type = ActionType.GENERIC_ENRICH
- action_description = f"Alert enriched by {authenticated_entity.email} - {enrich_data.enrichments}"
+ (
+ action_type,
+ action_description,
+ should_run_workflow,
+ should_check_incidents_resolution,
+ ) = enrichement_bl.get_enrichment_metadata(
+ enrich_data.enrichments, authenticated_entity
+ )
enrichments = deepcopy(enrich_data.enrichments)
enrichement_bl.enrich_entity(
diff --git a/keep/api/routes/incidents.py b/keep/api/routes/incidents.py
index 3a57c62495..4e96e9fafa 100644
--- a/keep/api/routes/incidents.py
+++ b/keep/api/routes/incidents.py
@@ -20,8 +20,8 @@
from keep.api.arq_pool import get_pool
from keep.api.bl.ai_suggestion_bl import AISuggestionBl
from keep.api.bl.enrichments_bl import EnrichmentsBl
-from keep.api.bl.incidents_bl import IncidentBl
from keep.api.bl.incident_reports import IncidentReportsBl
+from keep.api.bl.incidents_bl import IncidentBl
from keep.api.consts import KEEP_ARQ_QUEUE_BASIC, REDIS
from keep.api.core.cel_to_sql.sql_providers.base import CelToSqlException
from keep.api.core.db import (
@@ -45,11 +45,7 @@
get_incident_potential_facet_fields,
)
from keep.api.models.action_type import ActionType
-from keep.api.models.alert import (
- AlertDto,
- EnrichAlertRequestBody,
- EnrichIncidentRequestBody,
-)
+from keep.api.models.alert import AlertDto, EnrichIncidentRequestBody
from keep.api.models.db.alert import AlertAudit
from keep.api.models.db.incident import IncidentSeverity, IncidentStatus
from keep.api.models.facet import FacetOptionsQueryDto
@@ -68,7 +64,6 @@
SplitIncidentResponseDto,
)
from keep.api.models.workflow import WorkflowExecutionDTO
-from keep.api.routes.alerts import _enrich_alert
from keep.api.tasks.process_incident_task import process_incident
from keep.api.utils.enrichment_helpers import convert_db_alerts_to_dto_alerts
from keep.api.utils.pagination import (
@@ -872,14 +867,25 @@ def change_incident_status(
# We need to do something only if status really changed
if change.status != incident.status:
if change.status in [IncidentStatus.RESOLVED, IncidentStatus.ACKNOWLEDGED]:
- for alert in incident._alerts:
- _enrich_alert(
- EnrichAlertRequestBody(
- enrichments={"status": change.status.value},
- fingerprint=alert.fingerprint,
- ),
- authenticated_entity=authenticated_entity,
- )
+ enrichments = {"status": change.status.value}
+ fingerprints = [alert.fingerprint for alert in incident._alerts]
+ enrichments_bl = EnrichmentsBl(tenant_id)
+ (
+ action_type,
+ action_description,
+ should_run_workflow,
+ should_check_incidents_resolution,
+ ) = enrichments_bl.get_enrichment_metadata(
+ enrichments, authenticated_entity
+ )
+ enrichments_bl.batch_enrich(
+ fingerprints,
+ enrichments,
+ action_type,
+ authenticated_entity.email,
+ action_description,
+ dispose_on_new_alert=True,
+ )
if change.status == IncidentStatus.RESOLVED:
end_time = datetime.now(tz=timezone.utc)