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 */} -
+
{icon}
@@ -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)