diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/get_raw_data_or_default/index.test.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/get_raw_data_or_default/index.test.ts deleted file mode 100644 index 899b156d21767..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/get_raw_data_or_default/index.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getRawDataOrDefault } from '.'; - -describe('getRawDataOrDefault', () => { - it('returns the raw data when it is valid', () => { - const rawData = { - field1: [1, 2, 3], - field2: ['a', 'b', 'c'], - }; - - expect(getRawDataOrDefault(rawData)).toEqual(rawData); - }); - - it('returns an empty object when the raw data is invalid', () => { - const rawData = { - field1: [1, 2, 3], - field2: 'invalid', - }; - - expect(getRawDataOrDefault(rawData)).toEqual({}); - }); -}); diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/get_raw_data_or_default/index.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/get_raw_data_or_default/index.ts deleted file mode 100644 index edbe320c95305..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/get_raw_data_or_default/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isRawDataValid } from '../is_raw_data_valid'; -import type { MaybeRawData } from '../types'; - -/** Returns the raw data if it valid, or a default if it's not */ -export const getRawDataOrDefault = (rawData: MaybeRawData): Record => - isRawDataValid(rawData) ? rawData : {}; diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/is_raw_data_valid/index.test.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/is_raw_data_valid/index.test.ts deleted file mode 100644 index cc205250e84db..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/is_raw_data_valid/index.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isRawDataValid } from '.'; - -describe('isRawDataValid', () => { - it('returns true for valid raw data', () => { - const rawData = { - field1: [1, 2, 3], // the Fields API may return a number array - field2: ['a', 'b', 'c'], // the Fields API may return a string array - }; - - expect(isRawDataValid(rawData)).toBe(true); - }); - - it('returns true when a field array is empty', () => { - const rawData = { - field1: [1, 2, 3], // the Fields API may return a number array - field2: ['a', 'b', 'c'], // the Fields API may return a string array - field3: [], // the Fields API may return an empty array - }; - - expect(isRawDataValid(rawData)).toBe(true); - }); - - it('returns false when a field does not have an array of values', () => { - const rawData = { - field1: [1, 2, 3], - field2: 'invalid', - }; - - expect(isRawDataValid(rawData)).toBe(false); - }); - - it('returns true for empty raw data', () => { - const rawData = {}; - - expect(isRawDataValid(rawData)).toBe(true); - }); - - it('returns false when raw data is an unexpected type', () => { - const rawData = 1234; - - // @ts-expect-error - expect(isRawDataValid(rawData)).toBe(false); - }); -}); diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/is_raw_data_valid/index.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/is_raw_data_valid/index.ts deleted file mode 100644 index 1a9623b15ea98..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/is_raw_data_valid/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MaybeRawData } from '../types'; - -export const isRawDataValid = (rawData: MaybeRawData): rawData is Record => - typeof rawData === 'object' && Object.keys(rawData).every((x) => Array.isArray(rawData[x])); diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/size_is_out_of_range/index.test.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/size_is_out_of_range/index.test.ts deleted file mode 100644 index b118a5c94b26e..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/size_is_out_of_range/index.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { sizeIsOutOfRange } from '.'; -import { MAX_SIZE, MIN_SIZE } from '../types'; - -describe('sizeIsOutOfRange', () => { - it('returns true when size is undefined', () => { - const size = undefined; - - expect(sizeIsOutOfRange(size)).toBe(true); - }); - - it('returns true when size is less than MIN_SIZE', () => { - const size = MIN_SIZE - 1; - - expect(sizeIsOutOfRange(size)).toBe(true); - }); - - it('returns true when size is greater than MAX_SIZE', () => { - const size = MAX_SIZE + 1; - - expect(sizeIsOutOfRange(size)).toBe(true); - }); - - it('returns false when size is exactly MIN_SIZE', () => { - const size = MIN_SIZE; - - expect(sizeIsOutOfRange(size)).toBe(false); - }); - - it('returns false when size is exactly MAX_SIZE', () => { - const size = MAX_SIZE; - - expect(sizeIsOutOfRange(size)).toBe(false); - }); - - it('returns false when size is within the valid range', () => { - const size = MIN_SIZE + 1; - - expect(sizeIsOutOfRange(size)).toBe(false); - }); -}); diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/size_is_out_of_range/index.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/size_is_out_of_range/index.ts deleted file mode 100644 index b2a93b79cbb42..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/size_is_out_of_range/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { MAX_SIZE, MIN_SIZE } from '../types'; - -/** Return true if the provided size is out of range */ -export const sizeIsOutOfRange = (size?: number): boolean => - size == null || size < MIN_SIZE || size > MAX_SIZE; diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/types.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/types.ts deleted file mode 100644 index 5c81c99ce5732..0000000000000 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/helpers/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; - -export const MIN_SIZE = 10; -export const MAX_SIZE = 10000; - -/** currently the same shape as "fields" property in the ES response */ -export type MaybeRawData = SearchResponse['fields'] | undefined; diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/common_attributes.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/common_attributes.gen.ts index 8ade6084fd7de..9599e8596e553 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/common_attributes.gen.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/common_attributes.gen.ts @@ -39,7 +39,7 @@ export const AttackDiscovery = z.object({ /** * A short (no more than a sentence) summary of the attack discovery featuring only the host.name and user.name fields (when they are applicable), using the same syntax */ - entitySummaryMarkdown: z.string().optional(), + entitySummaryMarkdown: z.string(), /** * An array of MITRE ATT&CK tactic for the attack discovery */ @@ -55,7 +55,7 @@ export const AttackDiscovery = z.object({ /** * The time the attack discovery was generated */ - timestamp: NonEmptyString.optional(), + timestamp: NonEmptyString, }); /** diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/common_attributes.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/common_attributes.schema.yaml index 3adf2f7836804..dcb72147f9408 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/common_attributes.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/attack_discovery/common_attributes.schema.yaml @@ -12,7 +12,9 @@ components: required: - 'alertIds' - 'detailsMarkdown' + - 'entitySummaryMarkdown' - 'summaryMarkdown' + - 'timestamp' - 'title' properties: alertIds: diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.gen.ts b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.gen.ts index a0cbc22282c7b..b6d51b9bea3fc 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.gen.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.gen.ts @@ -22,12 +22,10 @@ export type PostEvaluateBody = z.infer; export const PostEvaluateBody = z.object({ graphs: z.array(z.string()), datasetName: z.string(), - evaluatorConnectorId: z.string().optional(), connectorIds: z.array(z.string()), runName: z.string().optional(), alertsIndexPattern: z.string().optional().default('.alerts-security.alerts-default'), langSmithApiKey: z.string().optional(), - langSmithProject: z.string().optional(), replacements: Replacements.optional().default({}), size: z.number().optional().default(20), }); diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.schema.yaml b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.schema.yaml index 071d80156890b..d0bec37344165 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.schema.yaml +++ b/x-pack/packages/kbn-elastic-assistant-common/impl/schemas/evaluation/post_evaluate_route.schema.yaml @@ -61,8 +61,6 @@ components: type: string datasetName: type: string - evaluatorConnectorId: - type: string connectorIds: type: array items: @@ -74,8 +72,6 @@ components: default: ".alerts-security.alerts-default" langSmithApiKey: type: string - langSmithProject: - type: string replacements: $ref: "../conversations/common_attributes.schema.yaml#/components/schemas/Replacements" default: {} diff --git a/x-pack/packages/kbn-elastic-assistant-common/index.ts b/x-pack/packages/kbn-elastic-assistant-common/index.ts index 41ed86dacd9db..d8b4858d3ba8b 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/index.ts +++ b/x-pack/packages/kbn-elastic-assistant-common/index.ts @@ -25,19 +25,3 @@ export { export { transformRawData } from './impl/data_anonymization/transform_raw_data'; export { parseBedrockBuffer, handleBedrockChunk } from './impl/utils/bedrock'; export * from './constants'; - -/** currently the same shape as "fields" property in the ES response */ -export { type MaybeRawData } from './impl/alerts/helpers/types'; - -/** - * This query returns open and acknowledged (non-building block) alerts in the last 24 hours. - * - * The alerts are ordered by risk score, and then from the most recent to the oldest. - */ -export { getOpenAndAcknowledgedAlertsQuery } from './impl/alerts/get_open_and_acknowledged_alerts_query'; - -/** Returns the raw data if it valid, or a default if it's not */ -export { getRawDataOrDefault } from './impl/alerts/helpers/get_raw_data_or_default'; - -/** Return true if the provided size is out of range */ -export { sizeIsOutOfRange } from './impl/alerts/helpers/size_is_out_of_range'; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings.tsx index 3b48c8d0861c5..60078178a1771 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings.tsx @@ -16,7 +16,7 @@ import * as i18n from '../../../knowledge_base/translations'; export const MIN_LATEST_ALERTS = 10; export const MAX_LATEST_ALERTS = 100; export const TICK_INTERVAL = 10; -export const RANGE_CONTAINER_WIDTH = 600; // px +export const RANGE_CONTAINER_WIDTH = 300; // px const LABEL_WRAPPER_MIN_WIDTH = 95; // px interface Props { @@ -52,7 +52,6 @@ const AlertsSettingsComponent = ({ knowledgeBase, setUpdatedKnowledgeBaseSetting diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings_management.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings_management.tsx index 7a3998879078d..1a6f826bd415f 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings_management.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/alerts_settings/alerts_settings_management.tsx @@ -40,7 +40,6 @@ export const AlertsSettingsManagement: React.FC = React.memo( knowledgeBase={knowledgeBase} setUpdatedKnowledgeBaseSettings={setUpdatedKnowledgeBaseSettings} compressed={false} - value={knowledgeBase.latestAlerts} /> ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/evaluation_settings.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/evaluation_settings.tsx index ffbcad48d1cac..cefc008eba992 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/evaluation_settings.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/evaluation_settings.tsx @@ -17,34 +17,28 @@ import { EuiComboBox, EuiButton, EuiComboBoxOptionOption, - EuiComboBoxSingleSelectionShape, EuiTextColor, EuiFieldText, - EuiFieldNumber, EuiFlexItem, EuiFlexGroup, EuiLink, EuiPanel, } from '@elastic/eui'; + import { css } from '@emotion/react'; import { FormattedMessage } from '@kbn/i18n-react'; import type { GetEvaluateResponse, PostEvaluateRequestBodyInput, } from '@kbn/elastic-assistant-common'; -import { isEmpty } from 'lodash/fp'; - import * as i18n from './translations'; import { useAssistantContext } from '../../../assistant_context'; -import { DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS } from '../../../assistant_context/constants'; import { useLoadConnectors } from '../../../connectorland/use_load_connectors'; import { getActionTypeTitle, getGenAiConfig } from '../../../connectorland/helpers'; import { PRECONFIGURED_CONNECTOR } from '../../../connectorland/translations'; import { usePerformEvaluation } from '../../api/evaluate/use_perform_evaluation'; import { useEvaluationData } from '../../api/evaluate/use_evaluation_data'; -const AS_PLAIN_TEXT: EuiComboBoxSingleSelectionShape = { asPlainText: true }; - /** * Evaluation Settings -- development-only feature for evaluating models */ @@ -127,18 +121,6 @@ export const EvaluationSettings: React.FC = React.memo(() => { }, [setSelectedModelOptions] ); - - const [selectedEvaluatorModel, setSelectedEvaluatorModel] = useState< - Array> - >([]); - - const onSelectedEvaluatorModelChange = useCallback( - (selected: Array>) => setSelectedEvaluatorModel(selected), - [] - ); - - const [size, setSize] = useState(`${DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS}`); - const visColorsBehindText = euiPaletteComplementary(connectors?.length ?? 0); const modelOptions = useMemo(() => { return ( @@ -188,40 +170,19 @@ export const EvaluationSettings: React.FC = React.memo(() => { // Perform Evaluation Button const handlePerformEvaluation = useCallback(async () => { - const evaluatorConnectorId = - selectedEvaluatorModel[0]?.key != null - ? { evaluatorConnectorId: selectedEvaluatorModel[0].key } - : {}; - - const langSmithApiKey = isEmpty(traceOptions.langSmithApiKey) - ? undefined - : traceOptions.langSmithApiKey; - - const langSmithProject = isEmpty(traceOptions.langSmithProject) - ? undefined - : traceOptions.langSmithProject; - const evalParams: PostEvaluateRequestBodyInput = { connectorIds: selectedModelOptions.flatMap((option) => option.key ?? []).sort(), graphs: selectedGraphOptions.map((option) => option.label).sort(), datasetName: selectedDatasetOptions[0]?.label, - ...evaluatorConnectorId, - langSmithApiKey, - langSmithProject, runName, - size: Number(size), }; performEvaluation(evalParams); }, [ performEvaluation, runName, selectedDatasetOptions, - selectedEvaluatorModel, selectedGraphOptions, selectedModelOptions, - size, - traceOptions.langSmithApiKey, - traceOptions.langSmithProject, ]); const getSection = (title: string, description: string) => ( @@ -394,29 +355,6 @@ export const EvaluationSettings: React.FC = React.memo(() => { onChange={onGraphOptionsChange} /> - - - - - - - setSize(e.target.value)} value={size} /> - diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/translations.ts index 26eddb8a223c7..62902d0f14095 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/translations.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/settings/evaluation_settings/translations.ts @@ -78,36 +78,6 @@ export const CONNECTORS_LABEL = i18n.translate( } ); -export const EVALUATOR_MODEL = i18n.translate( - 'xpack.elasticAssistant.assistant.settings.evaluationSettings.evaluatorModelLabel', - { - defaultMessage: 'Evaluator model (optional)', - } -); - -export const DEFAULT_MAX_ALERTS = i18n.translate( - 'xpack.elasticAssistant.assistant.settings.evaluationSettings.defaultMaxAlertsLabel', - { - defaultMessage: 'Default max alerts', - } -); - -export const EVALUATOR_MODEL_DESCRIPTION = i18n.translate( - 'xpack.elasticAssistant.assistant.settings.evaluationSettings.evaluatorModelDescription', - { - defaultMessage: - 'Judge the quality of all predictions using a single model. (Default: use the same model as the connector)', - } -); - -export const DEFAULT_MAX_ALERTS_DESCRIPTION = i18n.translate( - 'xpack.elasticAssistant.assistant.settings.evaluationSettings.defaultMaxAlertsDescription', - { - defaultMessage: - 'The default maximum number of alerts to send as context, which may be overridden by the Example input', - } -); - export const CONNECTORS_DESCRIPTION = i18n.translate( 'xpack.elasticAssistant.assistant.settings.evaluationSettings.connectorsDescription', { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/constants.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/constants.tsx index 92a2a3df2683b..be7724d882278 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/constants.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/constants.tsx @@ -10,9 +10,7 @@ import { KnowledgeBaseConfig } from '../assistant/types'; export const ATTACK_DISCOVERY_STORAGE_KEY = 'attackDiscovery'; export const DEFAULT_ASSISTANT_NAMESPACE = 'elasticAssistantDefault'; export const LAST_CONVERSATION_ID_LOCAL_STORAGE_KEY = 'lastConversationId'; -export const MAX_ALERTS_LOCAL_STORAGE_KEY = 'maxAlerts'; export const KNOWLEDGE_BASE_LOCAL_STORAGE_KEY = 'knowledgeBase'; -export const SHOW_SETTINGS_TOUR_LOCAL_STORAGE_KEY = 'showSettingsTour'; export const STREAMING_LOCAL_STORAGE_KEY = 'streaming'; export const TRACE_OPTIONS_SESSION_STORAGE_KEY = 'traceOptions'; export const CONVERSATION_TABLE_SESSION_STORAGE_KEY = 'conversationTable'; @@ -23,9 +21,6 @@ export const ANONYMIZATION_TABLE_SESSION_STORAGE_KEY = 'anonymizationTable'; /** The default `n` latest alerts, ordered by risk score, sent as context to the assistant */ export const DEFAULT_LATEST_ALERTS = 20; -/** The default maximum number of alerts to be sent as context when generating Attack discoveries */ -export const DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS = 200; - export const DEFAULT_KNOWLEDGE_BASE_SETTINGS: KnowledgeBaseConfig = { latestAlerts: DEFAULT_LATEST_ALERTS, }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx index 2319bf67de89a..c7b15f681a717 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.tsx @@ -262,10 +262,7 @@ export const AssistantProvider: React.FC = ({ docLinks, getComments, http, - knowledgeBase: { - ...DEFAULT_KNOWLEDGE_BASE_SETTINGS, - ...localStorageKnowledgeBase, - }, + knowledgeBase: { ...DEFAULT_KNOWLEDGE_BASE_SETTINGS, ...localStorageKnowledgeBase }, promptContexts, navigateToApp, nameSpace, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/alerts_range.tsx b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/alerts_range.tsx index 6cfa60eff282d..63bd86121dcc1 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/alerts_range.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/knowledge_base/alerts_range.tsx @@ -7,7 +7,7 @@ import { EuiRange, useGeneratedHtmlId } from '@elastic/eui'; import { css } from '@emotion/react'; -import React, { useCallback } from 'react'; +import React from 'react'; import { MAX_LATEST_ALERTS, MIN_LATEST_ALERTS, @@ -16,57 +16,35 @@ import { import { KnowledgeBaseConfig } from '../assistant/types'; import { ALERTS_RANGE } from './translations'; -export type SingleRangeChangeEvent = - | React.ChangeEvent - | React.KeyboardEvent - | React.MouseEvent; - interface Props { + knowledgeBase: KnowledgeBaseConfig; + setUpdatedKnowledgeBaseSettings: React.Dispatch>; compressed?: boolean; - maxAlerts?: number; - minAlerts?: number; - onChange?: (e: SingleRangeChangeEvent) => void; - knowledgeBase?: KnowledgeBaseConfig; - setUpdatedKnowledgeBaseSettings?: React.Dispatch>; - step?: number; - value: string | number; } const MAX_ALERTS_RANGE_WIDTH = 649; // px export const AlertsRange: React.FC = React.memo( - ({ - compressed = true, - knowledgeBase, - maxAlerts = MAX_LATEST_ALERTS, - minAlerts = MIN_LATEST_ALERTS, - onChange, - setUpdatedKnowledgeBaseSettings, - step = TICK_INTERVAL, - value, - }) => { + ({ knowledgeBase, setUpdatedKnowledgeBaseSettings, compressed = true }) => { const inputRangeSliderId = useGeneratedHtmlId({ prefix: 'inputRangeSlider' }); - const handleOnChange = useCallback( - (e: SingleRangeChangeEvent) => { - if (knowledgeBase != null && setUpdatedKnowledgeBaseSettings != null) { - setUpdatedKnowledgeBaseSettings({ - ...knowledgeBase, - latestAlerts: Number(e.currentTarget.value), - }); - } - - if (onChange != null) { - onChange(e); - } - }, - [knowledgeBase, onChange, setUpdatedKnowledgeBaseSettings] - ); - return ( + setUpdatedKnowledgeBaseSettings({ + ...knowledgeBase, + latestAlerts: Number(e.currentTarget.value), + }) + } + showTicks + step={TICK_INTERVAL} + value={knowledgeBase.latestAlerts} css={css` max-inline-size: ${MAX_ALERTS_RANGE_WIDTH}px; & .euiRangeTrack { @@ -74,14 +52,6 @@ export const AlertsRange: React.FC = React.memo( margin-inline-end: 0; } `} - data-test-subj="alertsRange" - id={inputRangeSliderId} - max={maxAlerts} - min={minAlerts} - onChange={handleOnChange} - showTicks - step={step} - value={value} /> ); } diff --git a/x-pack/packages/kbn-elastic-assistant/index.ts b/x-pack/packages/kbn-elastic-assistant/index.ts index 7ec65c9601268..0baff57648cc8 100644 --- a/x-pack/packages/kbn-elastic-assistant/index.ts +++ b/x-pack/packages/kbn-elastic-assistant/index.ts @@ -77,17 +77,10 @@ export { AssistantAvatar } from './impl/assistant/assistant_avatar/assistant_ava export { ConnectorSelectorInline } from './impl/connectorland/connector_selector_inline/connector_selector_inline'; export { - /** The Attack discovery local storage key */ ATTACK_DISCOVERY_STORAGE_KEY, DEFAULT_ASSISTANT_NAMESPACE, - /** The default maximum number of alerts to be sent as context when generating Attack discoveries */ - DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS, DEFAULT_LATEST_ALERTS, KNOWLEDGE_BASE_LOCAL_STORAGE_KEY, - /** The local storage key that specifies the maximum number of alerts to send as context */ - MAX_ALERTS_LOCAL_STORAGE_KEY, - /** The local storage key that specifies whether the settings tour should be shown */ - SHOW_SETTINGS_TOUR_LOCAL_STORAGE_KEY, } from './impl/assistant_context/constants'; export { useLoadConnectors } from './impl/connectorland/use_load_connectors'; @@ -147,16 +140,3 @@ export { mergeBaseWithPersistedConversations } from './impl/assistant/helpers'; export { UpgradeButtons } from './impl/upgrade/upgrade_buttons'; export { getUserConversations, getPrompts, bulkUpdatePrompts } from './impl/assistant/api'; - -export { - /** A range slider component, typically used to configure the number of alerts sent as context */ - AlertsRange, - /** This event occurs when the `AlertsRange` slider is changed */ - type SingleRangeChangeEvent, -} from './impl/knowledge_base/alerts_range'; -export { - /** A label instructing the user to send fewer alerts */ - SELECT_FEWER_ALERTS, - /** Your anonymization settings will apply to these alerts (label) */ - YOUR_ANONYMIZATION_SETTINGS, -} from './impl/knowledge_base/translations'; diff --git a/x-pack/plugins/elastic_assistant/README.md b/x-pack/plugins/elastic_assistant/README.md index 8cf2c0b8903dd..2a1e47c177591 100755 --- a/x-pack/plugins/elastic_assistant/README.md +++ b/x-pack/plugins/elastic_assistant/README.md @@ -10,21 +10,15 @@ Maintained by the Security Solution team ## Graph structure -### Default Assistant graph - ![DefaultAssistantGraph](./docs/img/default_assistant_graph.png) -### Default Attack discovery graph - -![DefaultAttackDiscoveryGraph](./docs/img/default_attack_discovery_graph.png) - ## Development ### Generate graph structure To generate the graph structure, run `yarn draw-graph` from the plugin directory. -The graphs will be generated in the `docs/img` directory of the plugin. +The graph will be generated in the `docs/img` directory of the plugin. ### Testing -To run the tests for this plugin, run `node scripts/jest --watch x-pack/plugins/elastic_assistant/jest.config.js --coverage` from the Kibana root directory. +To run the tests for this plugin, run `node scripts/jest --watch x-pack/plugins/elastic_assistant/jest.config.js --coverage` from the Kibana root directory. \ No newline at end of file diff --git a/x-pack/plugins/elastic_assistant/docs/img/default_assistant_graph.png b/x-pack/plugins/elastic_assistant/docs/img/default_assistant_graph.png index 159b69c6d9572..e4ef8382317e5 100644 Binary files a/x-pack/plugins/elastic_assistant/docs/img/default_assistant_graph.png and b/x-pack/plugins/elastic_assistant/docs/img/default_assistant_graph.png differ diff --git a/x-pack/plugins/elastic_assistant/docs/img/default_attack_discovery_graph.png b/x-pack/plugins/elastic_assistant/docs/img/default_attack_discovery_graph.png deleted file mode 100644 index 658490900cca6..0000000000000 Binary files a/x-pack/plugins/elastic_assistant/docs/img/default_attack_discovery_graph.png and /dev/null differ diff --git a/x-pack/plugins/elastic_assistant/scripts/draw_graph_script.ts b/x-pack/plugins/elastic_assistant/scripts/draw_graph_script.ts index 3b65d307ce385..c44912ebf8d94 100644 --- a/x-pack/plugins/elastic_assistant/scripts/draw_graph_script.ts +++ b/x-pack/plugins/elastic_assistant/scripts/draw_graph_script.ts @@ -5,13 +5,11 @@ * 2.0. */ -import type { ElasticsearchClient } from '@kbn/core/server'; import { ToolingLog } from '@kbn/tooling-log'; import fs from 'fs/promises'; import path from 'path'; import { ActionsClientChatOpenAI, - type ActionsClientLlm, ActionsClientSimpleChatModel, } from '@kbn/langchain/server/language_models'; import type { Logger } from '@kbn/logging'; @@ -19,11 +17,6 @@ import { ChatPromptTemplate } from '@langchain/core/prompts'; import { FakeLLM } from '@langchain/core/utils/testing'; import { createOpenAIFunctionsAgent } from 'langchain/agents'; import { getDefaultAssistantGraph } from '../server/lib/langchain/graphs/default_assistant_graph/graph'; -import { getDefaultAttackDiscoveryGraph } from '../server/lib/attack_discovery/graphs/default_attack_discovery_graph'; - -interface Drawable { - drawMermaidPng: () => Promise; -} // Just defining some test variables to get the graph to compile.. const testPrompt = ChatPromptTemplate.fromMessages([ @@ -41,7 +34,7 @@ const createLlmInstance = () => { return mockLlm; }; -async function getAssistantGraph(logger: Logger): Promise { +async function getGraph(logger: Logger) { const agentRunnable = await createOpenAIFunctionsAgent({ llm: mockLlm, tools: [], @@ -58,49 +51,16 @@ async function getAssistantGraph(logger: Logger): Promise { return graph.getGraph(); } -async function getAttackDiscoveryGraph(logger: Logger): Promise { - const mockEsClient = {} as unknown as ElasticsearchClient; - - const graph = getDefaultAttackDiscoveryGraph({ - anonymizationFields: [], - esClient: mockEsClient, - llm: mockLlm as unknown as ActionsClientLlm, - logger, - replacements: {}, - size: 20, - }); - - return graph.getGraph(); -} - -export const drawGraph = async ({ - getGraph, - outputFilename, -}: { - getGraph: (logger: Logger) => Promise; - outputFilename: string; -}) => { +export const draw = async () => { const logger = new ToolingLog({ level: 'info', writeTo: process.stdout, }) as unknown as Logger; logger.info('Compiling graph'); - const outputPath = path.join(__dirname, outputFilename); + const outputPath = path.join(__dirname, '../docs/img/default_assistant_graph.png'); const graph = await getGraph(logger); const output = await graph.drawMermaidPng(); const buffer = Buffer.from(await output.arrayBuffer()); logger.info(`Writing graph to ${outputPath}`); await fs.writeFile(outputPath, buffer); }; - -export const draw = async () => { - await drawGraph({ - getGraph: getAssistantGraph, - outputFilename: '../docs/img/default_assistant_graph.png', - }); - - await drawGraph({ - getGraph: getAttackDiscoveryGraph, - outputFilename: '../docs/img/default_attack_discovery_graph.png', - }); -}; diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/attack_discovery_schema.mock.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/attack_discovery_schema.mock.ts index ee54e9c451ea2..9e8a0b5d2ac90 100644 --- a/x-pack/plugins/elastic_assistant/server/__mocks__/attack_discovery_schema.mock.ts +++ b/x-pack/plugins/elastic_assistant/server/__mocks__/attack_discovery_schema.mock.ts @@ -6,7 +6,7 @@ */ import { estypes } from '@elastic/elasticsearch'; -import { EsAttackDiscoverySchema } from '../lib/attack_discovery/persistence/types'; +import { EsAttackDiscoverySchema } from '../ai_assistant_data_clients/attack_discovery/types'; export const getAttackDiscoverySearchEsMock = () => { const searchResponse: estypes.SearchResponse = { diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/data_clients.mock.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/data_clients.mock.ts index 473965a835f14..7e20e292a9868 100644 --- a/x-pack/plugins/elastic_assistant/server/__mocks__/data_clients.mock.ts +++ b/x-pack/plugins/elastic_assistant/server/__mocks__/data_clients.mock.ts @@ -8,7 +8,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { AIAssistantConversationsDataClient } from '../ai_assistant_data_clients/conversations'; import { AIAssistantDataClient } from '../ai_assistant_data_clients'; -import { AttackDiscoveryDataClient } from '../lib/attack_discovery/persistence'; +import { AttackDiscoveryDataClient } from '../ai_assistant_data_clients/attack_discovery'; type ConversationsDataClientContract = PublicMethodsOf; export type ConversationsDataClientMock = jest.Mocked; diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/request_context.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/request_context.ts index d53ceaa586975..b52e7db536a3d 100644 --- a/x-pack/plugins/elastic_assistant/server/__mocks__/request_context.ts +++ b/x-pack/plugins/elastic_assistant/server/__mocks__/request_context.ts @@ -26,7 +26,7 @@ import { GetAIAssistantKnowledgeBaseDataClientParams, } from '../ai_assistant_data_clients/knowledge_base'; import { defaultAssistantFeatures } from '@kbn/elastic-assistant-common'; -import { AttackDiscoveryDataClient } from '../lib/attack_discovery/persistence'; +import { AttackDiscoveryDataClient } from '../ai_assistant_data_clients/attack_discovery'; export const createMockClients = () => { const core = coreMock.createRequestHandlerContext(); diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/response.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/response.ts index ae736c77c30ef..def0a81acea37 100644 --- a/x-pack/plugins/elastic_assistant/server/__mocks__/response.ts +++ b/x-pack/plugins/elastic_assistant/server/__mocks__/response.ts @@ -16,7 +16,7 @@ import { getPromptsSearchEsMock } from './prompts_schema.mock'; import { EsAnonymizationFieldsSchema } from '../ai_assistant_data_clients/anonymization_fields/types'; import { getAnonymizationFieldsSearchEsMock } from './anonymization_fields_schema.mock'; import { getAttackDiscoverySearchEsMock } from './attack_discovery_schema.mock'; -import { EsAttackDiscoverySchema } from '../lib/attack_discovery/persistence/types'; +import { EsAttackDiscoverySchema } from '../ai_assistant_data_clients/attack_discovery/types'; export const responseMock = { create: httpServerMock.createResponseFactory, diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/create_attack_discovery/create_attack_discovery.test.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/create_attack_discovery.test.ts similarity index 94% rename from x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/create_attack_discovery/create_attack_discovery.test.ts rename to x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/create_attack_discovery.test.ts index a82ec24c7041e..6e9cc39597bd7 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/create_attack_discovery/create_attack_discovery.test.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/create_attack_discovery.test.ts @@ -10,11 +10,11 @@ import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; import { createAttackDiscovery } from './create_attack_discovery'; import { AttackDiscoveryCreateProps, AttackDiscoveryResponse } from '@kbn/elastic-assistant-common'; import { AuthenticatedUser } from '@kbn/core-security-common'; -import { getAttackDiscovery } from '../get_attack_discovery/get_attack_discovery'; +import { getAttackDiscovery } from './get_attack_discovery'; import { loggerMock } from '@kbn/logging-mocks'; const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); const mockLogger = loggerMock.create(); -jest.mock('../get_attack_discovery/get_attack_discovery'); +jest.mock('./get_attack_discovery'); const attackDiscoveryCreate: AttackDiscoveryCreateProps = { attackDiscoveries: [], apiConfig: { diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/create_attack_discovery/create_attack_discovery.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/create_attack_discovery.ts similarity index 95% rename from x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/create_attack_discovery/create_attack_discovery.ts rename to x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/create_attack_discovery.ts index fc511dc559d30..7304ab3488529 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/create_attack_discovery/create_attack_discovery.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/create_attack_discovery.ts @@ -9,8 +9,8 @@ import { v4 as uuidv4 } from 'uuid'; import { AuthenticatedUser, ElasticsearchClient, Logger } from '@kbn/core/server'; import { AttackDiscoveryCreateProps, AttackDiscoveryResponse } from '@kbn/elastic-assistant-common'; -import { getAttackDiscovery } from '../get_attack_discovery/get_attack_discovery'; -import { CreateAttackDiscoverySchema } from '../types'; +import { getAttackDiscovery } from './get_attack_discovery'; +import { CreateAttackDiscoverySchema } from './types'; export interface CreateAttackDiscoveryParams { esClient: ElasticsearchClient; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/field_maps_configuration/field_maps_configuration.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/field_maps_configuration.ts similarity index 100% rename from x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/field_maps_configuration/field_maps_configuration.ts rename to x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/field_maps_configuration.ts diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/find_all_attack_discoveries/find_all_attack_discoveries.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/find_all_attack_discoveries.ts similarity index 92% rename from x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/find_all_attack_discoveries/find_all_attack_discoveries.ts rename to x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/find_all_attack_discoveries.ts index 945603b517938..e80d1e4589838 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/find_all_attack_discoveries/find_all_attack_discoveries.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/find_all_attack_discoveries.ts @@ -8,8 +8,8 @@ import { ElasticsearchClient, Logger } from '@kbn/core/server'; import { AttackDiscoveryResponse } from '@kbn/elastic-assistant-common'; import { AuthenticatedUser } from '@kbn/security-plugin/common'; -import { EsAttackDiscoverySchema } from '../types'; -import { transformESSearchToAttackDiscovery } from '../transforms/transforms'; +import { EsAttackDiscoverySchema } from './types'; +import { transformESSearchToAttackDiscovery } from './transforms'; const MAX_ITEMS = 10000; export interface FindAllAttackDiscoveriesParams { esClient: ElasticsearchClient; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/find_attack_discovery_by_connector_id/find_attack_discovery_by_connector_id.test.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/find_attack_discovery_by_connector_id.test.ts similarity index 95% rename from x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/find_attack_discovery_by_connector_id/find_attack_discovery_by_connector_id.test.ts rename to x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/find_attack_discovery_by_connector_id.test.ts index 53d74e6e92f42..10688ce25b25e 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/find_attack_discovery_by_connector_id/find_attack_discovery_by_connector_id.test.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/find_attack_discovery_by_connector_id.test.ts @@ -9,7 +9,7 @@ import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; import { loggerMock } from '@kbn/logging-mocks'; import { findAttackDiscoveryByConnectorId } from './find_attack_discovery_by_connector_id'; import { AuthenticatedUser } from '@kbn/core-security-common'; -import { getAttackDiscoverySearchEsMock } from '../../../../__mocks__/attack_discovery_schema.mock'; +import { getAttackDiscoverySearchEsMock } from '../../__mocks__/attack_discovery_schema.mock'; const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); const mockLogger = loggerMock.create(); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/find_attack_discovery_by_connector_id/find_attack_discovery_by_connector_id.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/find_attack_discovery_by_connector_id.ts similarity index 93% rename from x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/find_attack_discovery_by_connector_id/find_attack_discovery_by_connector_id.ts rename to x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/find_attack_discovery_by_connector_id.ts index 07fde44080026..532c35ac89c05 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/find_attack_discovery_by_connector_id/find_attack_discovery_by_connector_id.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/find_attack_discovery_by_connector_id.ts @@ -7,8 +7,8 @@ import { AuthenticatedUser, ElasticsearchClient, Logger } from '@kbn/core/server'; import { AttackDiscoveryResponse } from '@kbn/elastic-assistant-common'; -import { EsAttackDiscoverySchema } from '../types'; -import { transformESSearchToAttackDiscovery } from '../transforms/transforms'; +import { EsAttackDiscoverySchema } from './types'; +import { transformESSearchToAttackDiscovery } from './transforms'; export interface FindAttackDiscoveryParams { esClient: ElasticsearchClient; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/get_attack_discovery/get_attack_discovery.test.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/get_attack_discovery.test.ts similarity index 95% rename from x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/get_attack_discovery/get_attack_discovery.test.ts rename to x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/get_attack_discovery.test.ts index af1a1827cbddd..4ee89fb7a3bc0 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/get_attack_discovery/get_attack_discovery.test.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/get_attack_discovery.test.ts @@ -8,7 +8,7 @@ import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; import { loggerMock } from '@kbn/logging-mocks'; import { getAttackDiscovery } from './get_attack_discovery'; -import { getAttackDiscoverySearchEsMock } from '../../../../__mocks__/attack_discovery_schema.mock'; +import { getAttackDiscoverySearchEsMock } from '../../__mocks__/attack_discovery_schema.mock'; import { AuthenticatedUser } from '@kbn/core-security-common'; const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/get_attack_discovery/get_attack_discovery.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/get_attack_discovery.ts similarity index 93% rename from x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/get_attack_discovery/get_attack_discovery.ts rename to x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/get_attack_discovery.ts index ae2051d9e480b..d0cf6fd19ae05 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/get_attack_discovery/get_attack_discovery.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/get_attack_discovery.ts @@ -7,8 +7,8 @@ import { AuthenticatedUser, ElasticsearchClient, Logger } from '@kbn/core/server'; import { AttackDiscoveryResponse } from '@kbn/elastic-assistant-common'; -import { EsAttackDiscoverySchema } from '../types'; -import { transformESSearchToAttackDiscovery } from '../transforms/transforms'; +import { EsAttackDiscoverySchema } from './types'; +import { transformESSearchToAttackDiscovery } from './transforms'; export interface GetAttackDiscoveryParams { esClient: ElasticsearchClient; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/index.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/index.ts similarity index 92% rename from x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/index.ts rename to x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/index.ts index 5aac100f5f52c..ca053743c8035 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/index.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/index.ts @@ -11,15 +11,12 @@ import { AttackDiscoveryResponse, } from '@kbn/elastic-assistant-common'; import { AuthenticatedUser } from '@kbn/core-security-common'; -import { findAllAttackDiscoveries } from './find_all_attack_discoveries/find_all_attack_discoveries'; -import { findAttackDiscoveryByConnectorId } from './find_attack_discovery_by_connector_id/find_attack_discovery_by_connector_id'; -import { updateAttackDiscovery } from './update_attack_discovery/update_attack_discovery'; -import { createAttackDiscovery } from './create_attack_discovery/create_attack_discovery'; -import { getAttackDiscovery } from './get_attack_discovery/get_attack_discovery'; -import { - AIAssistantDataClient, - AIAssistantDataClientParams, -} from '../../../ai_assistant_data_clients'; +import { findAllAttackDiscoveries } from './find_all_attack_discoveries'; +import { findAttackDiscoveryByConnectorId } from './find_attack_discovery_by_connector_id'; +import { updateAttackDiscovery } from './update_attack_discovery'; +import { createAttackDiscovery } from './create_attack_discovery'; +import { getAttackDiscovery } from './get_attack_discovery'; +import { AIAssistantDataClient, AIAssistantDataClientParams } from '..'; type AttackDiscoveryDataClientParams = AIAssistantDataClientParams; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/transforms/transforms.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/transforms.ts similarity index 98% rename from x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/transforms/transforms.ts rename to x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/transforms.ts index 765d40f7a3226..d9a37582f48b0 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/transforms/transforms.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/transforms.ts @@ -7,7 +7,7 @@ import { estypes } from '@elastic/elasticsearch'; import { AttackDiscoveryResponse } from '@kbn/elastic-assistant-common'; -import { EsAttackDiscoverySchema } from '../types'; +import { EsAttackDiscoverySchema } from './types'; export const transformESSearchToAttackDiscovery = ( response: estypes.SearchResponse diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/types.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/types.ts similarity index 93% rename from x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/types.ts rename to x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/types.ts index 08be262fede5a..4a17c50e06af4 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/types.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/types.ts @@ -6,7 +6,7 @@ */ import { AttackDiscoveryStatus, Provider } from '@kbn/elastic-assistant-common'; -import { EsReplacementSchema } from '../../../ai_assistant_data_clients/conversations/types'; +import { EsReplacementSchema } from '../conversations/types'; export interface EsAttackDiscoverySchema { '@timestamp': string; @@ -53,7 +53,7 @@ export interface CreateAttackDiscoverySchema { title: string; timestamp: string; details_markdown: string; - entity_summary_markdown?: string; + entity_summary_markdown: string; mitre_attack_tactics?: string[]; summary_markdown: string; id?: string; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/update_attack_discovery/update_attack_discovery.test.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/update_attack_discovery.test.ts similarity index 97% rename from x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/update_attack_discovery/update_attack_discovery.test.ts rename to x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/update_attack_discovery.test.ts index 8d98839c092aa..24deda445f320 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/update_attack_discovery/update_attack_discovery.test.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/update_attack_discovery.test.ts @@ -7,7 +7,7 @@ import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; import { loggerMock } from '@kbn/logging-mocks'; -import { getAttackDiscovery } from '../get_attack_discovery/get_attack_discovery'; +import { getAttackDiscovery } from './get_attack_discovery'; import { updateAttackDiscovery } from './update_attack_discovery'; import { AttackDiscoveryResponse, @@ -15,7 +15,7 @@ import { AttackDiscoveryUpdateProps, } from '@kbn/elastic-assistant-common'; import { AuthenticatedUser } from '@kbn/core-security-common'; -jest.mock('../get_attack_discovery/get_attack_discovery'); +jest.mock('./get_attack_discovery'); const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); const mockLogger = loggerMock.create(); const user = { diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/update_attack_discovery/update_attack_discovery.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/update_attack_discovery.ts similarity index 95% rename from x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/update_attack_discovery/update_attack_discovery.ts rename to x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/update_attack_discovery.ts index c810a71c5f1a3..73a386bbb4362 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/persistence/update_attack_discovery/update_attack_discovery.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_data_clients/attack_discovery/update_attack_discovery.ts @@ -14,8 +14,8 @@ import { UUID, } from '@kbn/elastic-assistant-common'; import * as uuid from 'uuid'; -import { EsReplacementSchema } from '../../../../ai_assistant_data_clients/conversations/types'; -import { getAttackDiscovery } from '../get_attack_discovery/get_attack_discovery'; +import { EsReplacementSchema } from '../conversations/types'; +import { getAttackDiscovery } from './get_attack_discovery'; export interface UpdateAttackDiscoverySchema { id: UUID; @@ -25,7 +25,7 @@ export interface UpdateAttackDiscoverySchema { title: string; timestamp: string; details_markdown: string; - entity_summary_markdown?: string; + entity_summary_markdown: string; mitre_attack_tactics?: string[]; summary_markdown: string; id?: string; diff --git a/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts b/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts index 4cde64424ed7e..08912f41a8bbc 100644 --- a/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts +++ b/x-pack/plugins/elastic_assistant/server/ai_assistant_service/index.ts @@ -11,7 +11,7 @@ import type { AuthenticatedUser, Logger, ElasticsearchClient } from '@kbn/core/s import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; import type { MlPluginSetup } from '@kbn/ml-plugin/server'; import { Subject } from 'rxjs'; -import { attackDiscoveryFieldMap } from '../lib/attack_discovery/persistence/field_maps_configuration/field_maps_configuration'; +import { attackDiscoveryFieldMap } from '../ai_assistant_data_clients/attack_discovery/field_maps_configuration'; import { getDefaultAnonymizationFields } from '../../common/anonymization'; import { AssistantResourceNames, GetElser } from '../types'; import { AIAssistantConversationsDataClient } from '../ai_assistant_data_clients/conversations'; @@ -34,7 +34,7 @@ import { AIAssistantKnowledgeBaseDataClient, GetAIAssistantKnowledgeBaseDataClientParams, } from '../ai_assistant_data_clients/knowledge_base'; -import { AttackDiscoveryDataClient } from '../lib/attack_discovery/persistence'; +import { AttackDiscoveryDataClient } from '../ai_assistant_data_clients/attack_discovery'; import { createGetElserId, createPipeline, pipelineExists } from './helpers'; const TOTAL_FIELDS_LIMIT = 2500; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/__mocks__/mock_examples.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/__mocks__/mock_examples.ts deleted file mode 100644 index d149b8c4cd44d..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/__mocks__/mock_examples.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Example } from 'langsmith/schemas'; - -export const exampleWithReplacements: Example = { - id: '5D436078-B2CF-487A-A0FA-7CB46696F54E', - created_at: '2024-10-10T23:01:19.350232+00:00', - dataset_id: '0DA3497B-B084-4105-AFC0-2D8E05DE4B7C', - modified_at: '2024-10-10T23:01:19.350232+00:00', - inputs: {}, - outputs: { - attackDiscoveries: [ - { - title: 'Critical Malware and Phishing Alerts on host e1cb3cf0-30f3-4f99-a9c8-518b955c6f90', - alertIds: [ - '4af5689eb58c2420efc0f7fad53c5bf9b8b6797e516d6ea87d6044ce25d54e16', - 'c675d7eb6ee181d788b474117bae8d3ed4bdc2168605c330a93dd342534fb02b', - '021b27d6bee0650a843be1d511119a3b5c7c8fdaeff922471ce0248ad27bd26c', - '6cc8d5f0e1c2b6c75219b001858f1be64194a97334be7a1e3572f8cfe6bae608', - 'f39a4013ed9609584a8a22dca902e896aa5b24d2da03e0eaab5556608fa682ac', - '909968e926e08a974c7df1613d98ebf1e2422afcb58e4e994beb47b063e85080', - '2c25a4dc31cd1ec254c2b19ea663fd0b09a16e239caa1218b4598801fb330da6', - '3bf907becb3a4f8e39a3b673e0d50fc954a7febef30c12891744c603760e4998', - ], - timestamp: '2024-10-10T22:59:52.749Z', - detailsMarkdown: - '- On `2023-06-19T00:28:38.061Z` a critical malware detection alert was triggered on host {{ host.name e1cb3cf0-30f3-4f99-a9c8-518b955c6f90 }} running {{ host.os.name macOS }} version {{ host.os.version 13.4 }}.\n- The malware was identified as {{ file.name unix1 }} with SHA256 hash {{ file.hash.sha256 0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231 }}.\n- The process {{ process.name My Go Application.app }} was executed with command line {{ process.command_line /private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app }}.\n- The process was not trusted as its code signature failed to satisfy specified code requirements.\n- The user involved was {{ user.name 039c15c5-3964-43e7-a891-42fe2ceeb9ff }}.\n- Another critical alert was triggered for potential credentials phishing via {{ process.name osascript }} on the same host.\n- The phishing attempt involved displaying a dialog to capture the user\'s password.\n- The process {{ process.name osascript }} was executed with command line {{ process.command_line osascript -e display dialog "MacOS wants to access System Preferences\\n\\nPlease enter your password." with title "System Preferences" with icon file "System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:ToolbarAdvanced.icns" default answer "" giving up after 30 with hidden answer ¬ }}.\n- The MITRE ATT&CK tactics involved include Credential Access and Input Capture.', - summaryMarkdown: - 'Critical malware and phishing alerts detected on {{ host.name e1cb3cf0-30f3-4f99-a9c8-518b955c6f90 }} involving user {{ user.name 039c15c5-3964-43e7-a891-42fe2ceeb9ff }}. Malware identified as {{ file.name unix1 }} and phishing attempt via {{ process.name osascript }}.', - mitreAttackTactics: ['Credential Access', 'Input Capture'], - entitySummaryMarkdown: - 'Critical malware and phishing alerts detected on {{ host.name e1cb3cf0-30f3-4f99-a9c8-518b955c6f90 }} involving user {{ user.name 039c15c5-3964-43e7-a891-42fe2ceeb9ff }}.', - }, - ], - replacements: { - '039c15c5-3964-43e7-a891-42fe2ceeb9ff': 'james', - '0b53f092-96dd-4282-bfb9-4f75a4530b80': 'root', - '1123bd7b-3afb-45d1-801a-108f04e7cfb7': 'SRVWIN04', - '3b9856bc-2c0d-4f1a-b9ae-32742e15ddd1': 'SRVWIN07', - '5306bcfd-2729-49e3-bdf0-678002778ccf': 'SRVWIN01', - '55af96a7-69b0-47cf-bf11-29be98a59eb0': 'SRVNIX05', - '66919fe3-16a4-4dfe-bc90-713f0b33a2ff': 'Administrator', - '9404361f-53fa-484f-adf8-24508256e70e': 'SRVWIN03', - 'e1cb3cf0-30f3-4f99-a9c8-518b955c6f90': 'SRVMAC08', - 'f59a00e2-f9c4-4069-8390-fd36ecd16918': 'SRVWIN02', - 'fc6d07da-5186-4d59-9b79-9382b0c226b3': 'SRVWIN06', - }, - }, - runs: [], -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/__mocks__/mock_runs.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/__mocks__/mock_runs.ts deleted file mode 100644 index 23c9c08ff5080..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/__mocks__/mock_runs.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Run } from 'langsmith/schemas'; - -export const runWithReplacements: Run = { - id: 'B7B03FEE-9AC4-4823-AEDB-F8EC20EAD5C4', - inputs: {}, - name: 'test', - outputs: { - attackDiscoveries: [ - { - alertIds: [ - '4af5689eb58c2420efc0f7fad53c5bf9b8b6797e516d6ea87d6044ce25d54e16', - 'c675d7eb6ee181d788b474117bae8d3ed4bdc2168605c330a93dd342534fb02b', - '021b27d6bee0650a843be1d511119a3b5c7c8fdaeff922471ce0248ad27bd26c', - '6cc8d5f0e1c2b6c75219b001858f1be64194a97334be7a1e3572f8cfe6bae608', - 'f39a4013ed9609584a8a22dca902e896aa5b24d2da03e0eaab5556608fa682ac', - '909968e926e08a974c7df1613d98ebf1e2422afcb58e4e994beb47b063e85080', - '2c25a4dc31cd1ec254c2b19ea663fd0b09a16e239caa1218b4598801fb330da6', - '3bf907becb3a4f8e39a3b673e0d50fc954a7febef30c12891744c603760e4998', - ], - detailsMarkdown: - '- The attack began with the execution of a malicious file named `unix1` on the host `{{ host.name e1cb3cf0-30f3-4f99-a9c8-518b955c6f90 }}` by the user `{{ user.name 039c15c5-3964-43e7-a891-42fe2ceeb9ff }}`.\n- The file `unix1` was detected at `{{ file.path /Users/james/unix1 }}` with a SHA256 hash of `{{ file.hash.sha256 0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231 }}`.\n- The process `{{ process.name My Go Application.app }}` was executed multiple times with different arguments, indicating potential persistence mechanisms.\n- The process `{{ process.name chmod }}` was used to change permissions of the file `unix1` to 777, making it executable.\n- A phishing attempt was detected via `osascript` on the same host, attempting to capture user credentials.\n- The attack involved multiple critical alerts, all indicating high-risk malware activity.', - entitySummaryMarkdown: - 'The host `{{ host.name e1cb3cf0-30f3-4f99-a9c8-518b955c6f90 }}` and user `{{ user.name 039c15c5-3964-43e7-a891-42fe2ceeb9ff }}` were involved in the attack.', - mitreAttackTactics: ['Initial Access', 'Execution', 'Persistence', 'Credential Access'], - summaryMarkdown: - 'A series of critical malware alerts were detected on the host `{{ host.name e1cb3cf0-30f3-4f99-a9c8-518b955c6f90 }}` involving the user `{{ user.name 039c15c5-3964-43e7-a891-42fe2ceeb9ff }}`. The attack included the execution of a malicious file `unix1`, permission changes, and a phishing attempt via `osascript`.', - title: 'Critical Malware Attack on macOS Host', - timestamp: '2024-10-11T17:55:59.702Z', - }, - ], - replacements: { - '039c15c5-3964-43e7-a891-42fe2ceeb9ff': 'james', - '0b53f092-96dd-4282-bfb9-4f75a4530b80': 'root', - '1123bd7b-3afb-45d1-801a-108f04e7cfb7': 'SRVWIN04', - '3b9856bc-2c0d-4f1a-b9ae-32742e15ddd1': 'SRVWIN07', - '5306bcfd-2729-49e3-bdf0-678002778ccf': 'SRVWIN01', - '55af96a7-69b0-47cf-bf11-29be98a59eb0': 'SRVNIX05', - '66919fe3-16a4-4dfe-bc90-713f0b33a2ff': 'Administrator', - '9404361f-53fa-484f-adf8-24508256e70e': 'SRVWIN03', - 'e1cb3cf0-30f3-4f99-a9c8-518b955c6f90': 'SRVMAC08', - 'f59a00e2-f9c4-4069-8390-fd36ecd16918': 'SRVWIN02', - 'fc6d07da-5186-4d59-9b79-9382b0c226b3': 'SRVWIN06', - }, - }, - run_type: 'evaluation', -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/constants.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/constants.ts deleted file mode 100644 index c6f6f09f1d9ae..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/constants.ts +++ /dev/null @@ -1,911 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen'; - -export const DEFAULT_EVAL_ANONYMIZATION_FIELDS: AnonymizationFieldResponse[] = [ - { - id: 'Mx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: '_id', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'NB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: '@timestamp', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'NR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'cloud.availability_zone', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Nh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'cloud.provider', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Nx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'cloud.region', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'OB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'destination.ip', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'OR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'dns.question.name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Oh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'dns.question.type', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Ox09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'event.category', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'PB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'event.dataset', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'PR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'event.module', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Ph09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'event.outcome', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Px09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'file.Ext.original.path', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'QB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'file.hash.sha256', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'QR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'file.name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Qh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'file.path', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Qx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'group.id', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'RB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'group.name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'RR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'host.asset.criticality', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Rh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'host.name', - allowed: true, - anonymized: true, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Rx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'host.os.name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'SB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'host.os.version', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'SR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'host.risk.calculated_level', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Sh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'host.risk.calculated_score_norm', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Sx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'kibana.alert.original_time', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'TB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'kibana.alert.risk_score', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'TR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'kibana.alert.rule.description', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Th09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'kibana.alert.rule.name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Tx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'kibana.alert.rule.references', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'UB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'kibana.alert.rule.threat.framework', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'UR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'kibana.alert.rule.threat.tactic.id', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Uh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'kibana.alert.rule.threat.tactic.name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Ux09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'kibana.alert.rule.threat.tactic.reference', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'VB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'kibana.alert.rule.threat.technique.id', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'VR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'kibana.alert.rule.threat.technique.name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Vh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'kibana.alert.rule.threat.technique.reference', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Vx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'kibana.alert.rule.threat.technique.subtechnique.id', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'WB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'kibana.alert.rule.threat.technique.subtechnique.name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'WR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'kibana.alert.rule.threat.technique.subtechnique.reference', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Wh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'kibana.alert.severity', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Wx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'kibana.alert.workflow_status', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'XB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'message', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'XR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'network.protocol', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Xh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.args', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Xx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.code_signature.exists', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'YB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.code_signature.signing_id', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'YR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.code_signature.status', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Yh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.code_signature.subject_name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Yx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.code_signature.trusted', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'ZB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.command_line', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'ZR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.executable', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Zh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.exit_code', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'Zx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.Ext.memory_region.bytes_compressed_present', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'aB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.Ext.memory_region.malware_signature.all_names', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'aR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.Ext.memory_region.malware_signature.primary.matches', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'ah09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.Ext.memory_region.malware_signature.primary.signature.name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'ax09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.Ext.token.integrity_level_name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'bB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.hash.md5', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'bR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.hash.sha1', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'bh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.hash.sha256', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'bx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'cB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.parent.args', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'cR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.parent.args_count', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'ch09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.parent.code_signature.exists', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'cx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.parent.code_signature.status', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'dB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.parent.code_signature.subject_name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'dR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.parent.code_signature.trusted', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'dh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.parent.command_line', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'dx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.parent.executable', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'eB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.parent.name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'eR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.pe.original_file_name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'eh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.pid', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'ex09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'process.working_directory', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'fB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'Ransomware.feature', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'fR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'Ransomware.files.data', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'fh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'Ransomware.files.entropy', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'fx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'Ransomware.files.extension', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'gB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'Ransomware.files.metrics', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'gR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'Ransomware.files.operation', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'gh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'Ransomware.files.path', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'gx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'Ransomware.files.score', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'hB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'Ransomware.version', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'hR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'rule.name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'hh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'rule.reference', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'hx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'source.ip', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'iB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'threat.framework', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'iR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'threat.tactic.id', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'ih09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'threat.tactic.name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'ix09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'threat.tactic.reference', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'jB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'threat.technique.id', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'jR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'threat.technique.name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'jh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'threat.technique.reference', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'jx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'threat.technique.subtechnique.id', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'kB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'threat.technique.subtechnique.name', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'kR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'threat.technique.subtechnique.reference', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'kh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'user.asset.criticality', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'kx09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'user.domain', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'lB09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'user.name', - allowed: true, - anonymized: true, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'lR09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'user.risk.calculated_level', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, - { - id: 'lh09VpEBOiz7eA-eF2fb', - timestamp: '2024-08-15T13:32:10.073Z', - field: 'user.risk.calculated_score_norm', - allowed: true, - anonymized: false, - createdAt: '2024-08-15T13:32:10.073Z', - namespace: 'default', - }, -]; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/example_input/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/example_input/index.test.ts deleted file mode 100644 index 93d442bad5e9b..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/example_input/index.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ExampleInput, ExampleInputWithOverrides } from '.'; - -const validInput = { - attackDiscoveries: null, - attackDiscoveryPrompt: 'prompt', - anonymizedAlerts: [{ pageContent: 'content', metadata: { key: 'value' } }], - combinedGenerations: 'gen1gen2', - combinedRefinements: 'ref1ref2', - errors: ['error1', 'error2'], - generationAttempts: 1, - generations: ['gen1', 'gen2'], - hallucinationFailures: 0, - maxGenerationAttempts: 5, - maxHallucinationFailures: 2, - maxRepeatedGenerations: 3, - refinements: ['ref1', 'ref2'], - refinePrompt: 'refine prompt', - replacements: { key: 'replacement' }, - unrefinedResults: null, -}; - -describe('ExampleInput Schema', () => { - it('validates a correct ExampleInput object', () => { - expect(() => ExampleInput.parse(validInput)).not.toThrow(); - }); - - it('throws given an invalid ExampleInput', () => { - const invalidInput = { - attackDiscoveries: 'invalid', // should be an array or null - }; - - expect(() => ExampleInput.parse(invalidInput)).toThrow(); - }); - - it('removes unknown properties', () => { - const hasUnknownProperties = { - ...validInput, - unknownProperty: 'unknown', // <-- should be removed - }; - - const parsed = ExampleInput.parse(hasUnknownProperties); - - expect(parsed).not.toHaveProperty('unknownProperty'); - }); -}); - -describe('ExampleInputWithOverrides Schema', () => { - it('validates a correct ExampleInputWithOverrides object', () => { - const validInputWithOverrides = { - ...validInput, - overrides: { - attackDiscoveryPrompt: 'ad prompt override', - refinePrompt: 'refine prompt override', - }, - }; - - expect(() => ExampleInputWithOverrides.parse(validInputWithOverrides)).not.toThrow(); - }); - - it('throws when given an invalid ExampleInputWithOverrides object', () => { - const invalidInputWithOverrides = { - attackDiscoveries: null, - overrides: 'invalid', // should be an object - }; - - expect(() => ExampleInputWithOverrides.parse(invalidInputWithOverrides)).toThrow(); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/example_input/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/example_input/index.ts deleted file mode 100644 index 8183695fd7d2f..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/example_input/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AttackDiscovery, Replacements } from '@kbn/elastic-assistant-common'; -import { z } from '@kbn/zod'; - -const Document = z.object({ - pageContent: z.string(), - metadata: z.record(z.string(), z.any()), -}); - -type Document = z.infer; - -/** - * Parses the input from an example in a LangSmith dataset - */ -export const ExampleInput = z.object({ - attackDiscoveries: z.array(AttackDiscovery).nullable().optional(), - attackDiscoveryPrompt: z.string().optional(), - anonymizedAlerts: z.array(Document).optional(), - combinedGenerations: z.string().optional(), - combinedRefinements: z.string().optional(), - errors: z.array(z.string()).optional(), - generationAttempts: z.number().optional(), - generations: z.array(z.string()).optional(), - hallucinationFailures: z.number().optional(), - maxGenerationAttempts: z.number().optional(), - maxHallucinationFailures: z.number().optional(), - maxRepeatedGenerations: z.number().optional(), - refinements: z.array(z.string()).optional(), - refinePrompt: z.string().optional(), - replacements: Replacements.optional(), - unrefinedResults: z.array(AttackDiscovery).nullable().optional(), -}); - -export type ExampleInput = z.infer; - -/** - * The optional overrides for an example input - */ -export const ExampleInputWithOverrides = z.intersection( - ExampleInput, - z.object({ - overrides: ExampleInput.optional(), - }) -); - -export type ExampleInputWithOverrides = z.infer; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_default_prompt_template/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_default_prompt_template/index.test.ts deleted file mode 100644 index 8ea30103c0768..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_default_prompt_template/index.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getDefaultPromptTemplate } from '.'; - -describe('getDefaultPromptTemplate', () => { - it('returns the expected prompt template', () => { - const expectedTemplate = `Evaluate based on how well the following submission follows the specified rubric. Grade only based on the rubric and "expected response": - -[BEGIN rubric] -1. Is the submission non-empty and not null? -2. Is the submission well-formed JSON? -3. Evaluate the value of the "detailsMarkdown" field of all the "attackDiscoveries" in the submission json. Do the values of "detailsMarkdown" in the submission capture the essence of the "expected response", regardless of the order in which they appear, and highlight the same incident(s)? -4. Evaluate the value of the "entitySummaryMarkdown" field of all the "attackDiscoveries" in the submission json. Does the value of "entitySummaryMarkdown" in the submission mention at least 50% the same entities as in the "expected response"? -5. Evaluate the value of the "summaryMarkdown" field of all the "attackDiscoveries" in the submission json. Do the values of "summaryMarkdown" in the submission at least partially similar to that of the "expected response", regardless of the order in which they appear, and summarize the same incident(s)? -6. Evaluate the value of the "title" field of all the "attackDiscoveries" in the submission json. Are the "title" values in the submission at least partially similar to the tile(s) of the "expected response", regardless of the order in which they appear, and mention the same incident(s)? -7. Evaluate the value of the "alertIds" field of all the "attackDiscoveries" in the submission json. Do they match at least 100% of the "alertIds" in the submission? -[END rubric] - -[BEGIN DATA] -{input} -[BEGIN submission] -{output} -[END submission] -[BEGIN expected response] -{reference} -[END expected response] -[END DATA] - -{criteria} Base your answer based on all the grading rubric items. If at least 5 of the 7 rubric items are correct, consider the submission correct. Write out your explanation for each criterion in the rubric, first in detail, then as a separate summary on a new line. - -Then finally respond with a single character, 'Y' or 'N', on a new line without any preceding or following characters. It's important that only a single character appears on the last line.`; - - const result = getDefaultPromptTemplate(); - - expect(result).toBe(expectedTemplate); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_default_prompt_template/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_default_prompt_template/index.ts deleted file mode 100644 index 08e10f00e7f77..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_default_prompt_template/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const getDefaultPromptTemplate = - () => `Evaluate based on how well the following submission follows the specified rubric. Grade only based on the rubric and "expected response": - -[BEGIN rubric] -1. Is the submission non-empty and not null? -2. Is the submission well-formed JSON? -3. Evaluate the value of the "detailsMarkdown" field of all the "attackDiscoveries" in the submission json. Do the values of "detailsMarkdown" in the submission capture the essence of the "expected response", regardless of the order in which they appear, and highlight the same incident(s)? -4. Evaluate the value of the "entitySummaryMarkdown" field of all the "attackDiscoveries" in the submission json. Does the value of "entitySummaryMarkdown" in the submission mention at least 50% the same entities as in the "expected response"? -5. Evaluate the value of the "summaryMarkdown" field of all the "attackDiscoveries" in the submission json. Do the values of "summaryMarkdown" in the submission at least partially similar to that of the "expected response", regardless of the order in which they appear, and summarize the same incident(s)? -6. Evaluate the value of the "title" field of all the "attackDiscoveries" in the submission json. Are the "title" values in the submission at least partially similar to the tile(s) of the "expected response", regardless of the order in which they appear, and mention the same incident(s)? -7. Evaluate the value of the "alertIds" field of all the "attackDiscoveries" in the submission json. Do they match at least 100% of the "alertIds" in the submission? -[END rubric] - -[BEGIN DATA] -{input} -[BEGIN submission] -{output} -[END submission] -[BEGIN expected response] -{reference} -[END expected response] -[END DATA] - -{criteria} Base your answer based on all the grading rubric items. If at least 5 of the 7 rubric items are correct, consider the submission correct. Write out your explanation for each criterion in the rubric, first in detail, then as a separate summary on a new line. - -Then finally respond with a single character, 'Y' or 'N', on a new line without any preceding or following characters. It's important that only a single character appears on the last line.`; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_example_attack_discoveries_with_replacements/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_example_attack_discoveries_with_replacements/index.test.ts deleted file mode 100644 index c261f151b99ab..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_example_attack_discoveries_with_replacements/index.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { omit } from 'lodash/fp'; - -import { getExampleAttackDiscoveriesWithReplacements } from '.'; -import { exampleWithReplacements } from '../../../__mocks__/mock_examples'; - -describe('getExampleAttackDiscoveriesWithReplacements', () => { - it('returns attack discoveries with replacements applied to the detailsMarkdown, entitySummaryMarkdown, summaryMarkdown, and title', () => { - const result = getExampleAttackDiscoveriesWithReplacements(exampleWithReplacements); - - expect(result).toEqual([ - { - title: 'Critical Malware and Phishing Alerts on host SRVMAC08', - alertIds: [ - '4af5689eb58c2420efc0f7fad53c5bf9b8b6797e516d6ea87d6044ce25d54e16', - 'c675d7eb6ee181d788b474117bae8d3ed4bdc2168605c330a93dd342534fb02b', - '021b27d6bee0650a843be1d511119a3b5c7c8fdaeff922471ce0248ad27bd26c', - '6cc8d5f0e1c2b6c75219b001858f1be64194a97334be7a1e3572f8cfe6bae608', - 'f39a4013ed9609584a8a22dca902e896aa5b24d2da03e0eaab5556608fa682ac', - '909968e926e08a974c7df1613d98ebf1e2422afcb58e4e994beb47b063e85080', - '2c25a4dc31cd1ec254c2b19ea663fd0b09a16e239caa1218b4598801fb330da6', - '3bf907becb3a4f8e39a3b673e0d50fc954a7febef30c12891744c603760e4998', - ], - timestamp: '2024-10-10T22:59:52.749Z', - detailsMarkdown: - '- On `2023-06-19T00:28:38.061Z` a critical malware detection alert was triggered on host {{ host.name SRVMAC08 }} running {{ host.os.name macOS }} version {{ host.os.version 13.4 }}.\n- The malware was identified as {{ file.name unix1 }} with SHA256 hash {{ file.hash.sha256 0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231 }}.\n- The process {{ process.name My Go Application.app }} was executed with command line {{ process.command_line /private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app }}.\n- The process was not trusted as its code signature failed to satisfy specified code requirements.\n- The user involved was {{ user.name james }}.\n- Another critical alert was triggered for potential credentials phishing via {{ process.name osascript }} on the same host.\n- The phishing attempt involved displaying a dialog to capture the user\'s password.\n- The process {{ process.name osascript }} was executed with command line {{ process.command_line osascript -e display dialog "MacOS wants to access System Preferences\\n\\nPlease enter your password." with title "System Preferences" with icon file "System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:ToolbarAdvanced.icns" default answer "" giving up after 30 with hidden answer ¬ }}.\n- The MITRE ATT&CK tactics involved include Credential Access and Input Capture.', - summaryMarkdown: - 'Critical malware and phishing alerts detected on {{ host.name SRVMAC08 }} involving user {{ user.name james }}. Malware identified as {{ file.name unix1 }} and phishing attempt via {{ process.name osascript }}.', - mitreAttackTactics: ['Credential Access', 'Input Capture'], - entitySummaryMarkdown: - 'Critical malware and phishing alerts detected on {{ host.name SRVMAC08 }} involving user {{ user.name james }}.', - }, - ]); - }); - - it('returns an empty entitySummaryMarkdown when the entitySummaryMarkdown is missing', () => { - const missingEntitySummaryMarkdown = omit( - 'entitySummaryMarkdown', - exampleWithReplacements.outputs?.attackDiscoveries?.[0] - ); - - const exampleWithMissingEntitySummaryMarkdown = { - ...exampleWithReplacements, - outputs: { - ...exampleWithReplacements.outputs, - attackDiscoveries: [missingEntitySummaryMarkdown], - }, - }; - - const result = getExampleAttackDiscoveriesWithReplacements( - exampleWithMissingEntitySummaryMarkdown - ); - - expect(result).toEqual([ - { - title: 'Critical Malware and Phishing Alerts on host SRVMAC08', - alertIds: [ - '4af5689eb58c2420efc0f7fad53c5bf9b8b6797e516d6ea87d6044ce25d54e16', - 'c675d7eb6ee181d788b474117bae8d3ed4bdc2168605c330a93dd342534fb02b', - '021b27d6bee0650a843be1d511119a3b5c7c8fdaeff922471ce0248ad27bd26c', - '6cc8d5f0e1c2b6c75219b001858f1be64194a97334be7a1e3572f8cfe6bae608', - 'f39a4013ed9609584a8a22dca902e896aa5b24d2da03e0eaab5556608fa682ac', - '909968e926e08a974c7df1613d98ebf1e2422afcb58e4e994beb47b063e85080', - '2c25a4dc31cd1ec254c2b19ea663fd0b09a16e239caa1218b4598801fb330da6', - '3bf907becb3a4f8e39a3b673e0d50fc954a7febef30c12891744c603760e4998', - ], - timestamp: '2024-10-10T22:59:52.749Z', - detailsMarkdown: - '- On `2023-06-19T00:28:38.061Z` a critical malware detection alert was triggered on host {{ host.name SRVMAC08 }} running {{ host.os.name macOS }} version {{ host.os.version 13.4 }}.\n- The malware was identified as {{ file.name unix1 }} with SHA256 hash {{ file.hash.sha256 0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231 }}.\n- The process {{ process.name My Go Application.app }} was executed with command line {{ process.command_line /private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app }}.\n- The process was not trusted as its code signature failed to satisfy specified code requirements.\n- The user involved was {{ user.name james }}.\n- Another critical alert was triggered for potential credentials phishing via {{ process.name osascript }} on the same host.\n- The phishing attempt involved displaying a dialog to capture the user\'s password.\n- The process {{ process.name osascript }} was executed with command line {{ process.command_line osascript -e display dialog "MacOS wants to access System Preferences\\n\\nPlease enter your password." with title "System Preferences" with icon file "System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:ToolbarAdvanced.icns" default answer "" giving up after 30 with hidden answer ¬ }}.\n- The MITRE ATT&CK tactics involved include Credential Access and Input Capture.', - summaryMarkdown: - 'Critical malware and phishing alerts detected on {{ host.name SRVMAC08 }} involving user {{ user.name james }}. Malware identified as {{ file.name unix1 }} and phishing attempt via {{ process.name osascript }}.', - mitreAttackTactics: ['Credential Access', 'Input Capture'], - entitySummaryMarkdown: '', - }, - ]); - }); - - it('throws when an example is undefined', () => { - expect(() => getExampleAttackDiscoveriesWithReplacements(undefined)).toThrowError(); - }); - - it('throws when the example is missing attackDiscoveries', () => { - const missingAttackDiscoveries = { - ...exampleWithReplacements, - outputs: { - replacements: { ...exampleWithReplacements.outputs?.replacements }, - }, - }; - - expect(() => - getExampleAttackDiscoveriesWithReplacements(missingAttackDiscoveries) - ).toThrowError(); - }); - - it('throws when attackDiscoveries is null', () => { - const nullAttackDiscoveries = { - ...exampleWithReplacements, - outputs: { - attackDiscoveries: null, - replacements: { ...exampleWithReplacements.outputs?.replacements }, - }, - }; - - expect(() => getExampleAttackDiscoveriesWithReplacements(nullAttackDiscoveries)).toThrowError(); - }); - - it('returns the original attack discoveries when replacements are missing', () => { - const missingReplacements = { - ...exampleWithReplacements, - outputs: { - attackDiscoveries: [...exampleWithReplacements.outputs?.attackDiscoveries], - }, - }; - - const result = getExampleAttackDiscoveriesWithReplacements(missingReplacements); - - expect(result).toEqual(exampleWithReplacements.outputs?.attackDiscoveries); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_example_attack_discoveries_with_replacements/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_example_attack_discoveries_with_replacements/index.ts deleted file mode 100644 index 8fc5de2a08ed1..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_example_attack_discoveries_with_replacements/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AttackDiscoveries, Replacements } from '@kbn/elastic-assistant-common'; -import type { Example } from 'langsmith/schemas'; - -import { getDiscoveriesWithOriginalValues } from '../../get_discoveries_with_original_values'; - -export const getExampleAttackDiscoveriesWithReplacements = ( - example: Example | undefined -): AttackDiscoveries => { - const exampleAttackDiscoveries = example?.outputs?.attackDiscoveries; - const exampleReplacements = example?.outputs?.replacements ?? {}; - - // NOTE: calls to `parse` throw an error if the Example input is invalid - const validatedAttackDiscoveries = AttackDiscoveries.parse(exampleAttackDiscoveries); - const validatedReplacements = Replacements.parse(exampleReplacements); - - const withReplacements = getDiscoveriesWithOriginalValues({ - attackDiscoveries: validatedAttackDiscoveries, - replacements: validatedReplacements, - }); - - return withReplacements; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_run_attack_discoveries_with_replacements/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_run_attack_discoveries_with_replacements/index.test.ts deleted file mode 100644 index bd22e5d952b07..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_run_attack_discoveries_with_replacements/index.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { omit } from 'lodash/fp'; - -import { getRunAttackDiscoveriesWithReplacements } from '.'; -import { runWithReplacements } from '../../../__mocks__/mock_runs'; - -describe('getRunAttackDiscoveriesWithReplacements', () => { - it('returns attack discoveries with replacements applied to the detailsMarkdown, entitySummaryMarkdown, summaryMarkdown, and title', () => { - const result = getRunAttackDiscoveriesWithReplacements(runWithReplacements); - - expect(result).toEqual([ - { - alertIds: [ - '4af5689eb58c2420efc0f7fad53c5bf9b8b6797e516d6ea87d6044ce25d54e16', - 'c675d7eb6ee181d788b474117bae8d3ed4bdc2168605c330a93dd342534fb02b', - '021b27d6bee0650a843be1d511119a3b5c7c8fdaeff922471ce0248ad27bd26c', - '6cc8d5f0e1c2b6c75219b001858f1be64194a97334be7a1e3572f8cfe6bae608', - 'f39a4013ed9609584a8a22dca902e896aa5b24d2da03e0eaab5556608fa682ac', - '909968e926e08a974c7df1613d98ebf1e2422afcb58e4e994beb47b063e85080', - '2c25a4dc31cd1ec254c2b19ea663fd0b09a16e239caa1218b4598801fb330da6', - '3bf907becb3a4f8e39a3b673e0d50fc954a7febef30c12891744c603760e4998', - ], - detailsMarkdown: - '- The attack began with the execution of a malicious file named `unix1` on the host `{{ host.name SRVMAC08 }}` by the user `{{ user.name james }}`.\n- The file `unix1` was detected at `{{ file.path /Users/james/unix1 }}` with a SHA256 hash of `{{ file.hash.sha256 0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231 }}`.\n- The process `{{ process.name My Go Application.app }}` was executed multiple times with different arguments, indicating potential persistence mechanisms.\n- The process `{{ process.name chmod }}` was used to change permissions of the file `unix1` to 777, making it executable.\n- A phishing attempt was detected via `osascript` on the same host, attempting to capture user credentials.\n- The attack involved multiple critical alerts, all indicating high-risk malware activity.', - entitySummaryMarkdown: - 'The host `{{ host.name SRVMAC08 }}` and user `{{ user.name james }}` were involved in the attack.', - mitreAttackTactics: ['Initial Access', 'Execution', 'Persistence', 'Credential Access'], - summaryMarkdown: - 'A series of critical malware alerts were detected on the host `{{ host.name SRVMAC08 }}` involving the user `{{ user.name james }}`. The attack included the execution of a malicious file `unix1`, permission changes, and a phishing attempt via `osascript`.', - title: 'Critical Malware Attack on macOS Host', - timestamp: '2024-10-11T17:55:59.702Z', - }, - ]); - }); - - it("returns an empty entitySummaryMarkdown when it's missing from the attack discovery", () => { - const missingEntitySummaryMarkdown = omit( - 'entitySummaryMarkdown', - runWithReplacements.outputs?.attackDiscoveries?.[0] - ); - - const runWithMissingEntitySummaryMarkdown = { - ...runWithReplacements, - outputs: { - ...runWithReplacements.outputs, - attackDiscoveries: [missingEntitySummaryMarkdown], - }, - }; - - const result = getRunAttackDiscoveriesWithReplacements(runWithMissingEntitySummaryMarkdown); - - expect(result).toEqual([ - { - alertIds: [ - '4af5689eb58c2420efc0f7fad53c5bf9b8b6797e516d6ea87d6044ce25d54e16', - 'c675d7eb6ee181d788b474117bae8d3ed4bdc2168605c330a93dd342534fb02b', - '021b27d6bee0650a843be1d511119a3b5c7c8fdaeff922471ce0248ad27bd26c', - '6cc8d5f0e1c2b6c75219b001858f1be64194a97334be7a1e3572f8cfe6bae608', - 'f39a4013ed9609584a8a22dca902e896aa5b24d2da03e0eaab5556608fa682ac', - '909968e926e08a974c7df1613d98ebf1e2422afcb58e4e994beb47b063e85080', - '2c25a4dc31cd1ec254c2b19ea663fd0b09a16e239caa1218b4598801fb330da6', - '3bf907becb3a4f8e39a3b673e0d50fc954a7febef30c12891744c603760e4998', - ], - detailsMarkdown: - '- The attack began with the execution of a malicious file named `unix1` on the host `{{ host.name SRVMAC08 }}` by the user `{{ user.name james }}`.\n- The file `unix1` was detected at `{{ file.path /Users/james/unix1 }}` with a SHA256 hash of `{{ file.hash.sha256 0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231 }}`.\n- The process `{{ process.name My Go Application.app }}` was executed multiple times with different arguments, indicating potential persistence mechanisms.\n- The process `{{ process.name chmod }}` was used to change permissions of the file `unix1` to 777, making it executable.\n- A phishing attempt was detected via `osascript` on the same host, attempting to capture user credentials.\n- The attack involved multiple critical alerts, all indicating high-risk malware activity.', - entitySummaryMarkdown: '', - mitreAttackTactics: ['Initial Access', 'Execution', 'Persistence', 'Credential Access'], - summaryMarkdown: - 'A series of critical malware alerts were detected on the host `{{ host.name SRVMAC08 }}` involving the user `{{ user.name james }}`. The attack included the execution of a malicious file `unix1`, permission changes, and a phishing attempt via `osascript`.', - title: 'Critical Malware Attack on macOS Host', - timestamp: '2024-10-11T17:55:59.702Z', - }, - ]); - }); - - it('throws when the run is missing attackDiscoveries', () => { - const missingAttackDiscoveries = { - ...runWithReplacements, - outputs: { - replacements: { ...runWithReplacements.outputs?.replacements }, - }, - }; - - expect(() => getRunAttackDiscoveriesWithReplacements(missingAttackDiscoveries)).toThrowError(); - }); - - it('throws when attackDiscoveries is null', () => { - const nullAttackDiscoveries = { - ...runWithReplacements, - outputs: { - attackDiscoveries: null, - replacements: { ...runWithReplacements.outputs?.replacements }, - }, - }; - - expect(() => getRunAttackDiscoveriesWithReplacements(nullAttackDiscoveries)).toThrowError(); - }); - - it('returns the original attack discoveries when replacements are missing', () => { - const missingReplacements = { - ...runWithReplacements, - outputs: { - attackDiscoveries: [...runWithReplacements.outputs?.attackDiscoveries], - }, - }; - - const result = getRunAttackDiscoveriesWithReplacements(missingReplacements); - - expect(result).toEqual(runWithReplacements.outputs?.attackDiscoveries); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_run_attack_discoveries_with_replacements/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_run_attack_discoveries_with_replacements/index.ts deleted file mode 100644 index 01193320f712b..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/get_run_attack_discoveries_with_replacements/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AttackDiscoveries, Replacements } from '@kbn/elastic-assistant-common'; -import type { Run } from 'langsmith/schemas'; - -import { getDiscoveriesWithOriginalValues } from '../../get_discoveries_with_original_values'; - -export const getRunAttackDiscoveriesWithReplacements = (run: Run): AttackDiscoveries => { - const runAttackDiscoveries = run.outputs?.attackDiscoveries; - const runReplacements = run.outputs?.replacements ?? {}; - - // NOTE: calls to `parse` throw an error if the Run Input is invalid - const validatedAttackDiscoveries = AttackDiscoveries.parse(runAttackDiscoveries); - const validatedReplacements = Replacements.parse(runReplacements); - - const withReplacements = getDiscoveriesWithOriginalValues({ - attackDiscoveries: validatedAttackDiscoveries, - replacements: validatedReplacements, - }); - - return withReplacements; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/index.test.ts deleted file mode 100644 index 829e27df73f14..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/index.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { PromptTemplate } from '@langchain/core/prompts'; -import type { ActionsClientLlm } from '@kbn/langchain/server'; -import { loadEvaluator } from 'langchain/evaluation'; - -import { type GetCustomEvaluatorOptions, getCustomEvaluator } from '.'; -import { getDefaultPromptTemplate } from './get_default_prompt_template'; -import { getExampleAttackDiscoveriesWithReplacements } from './get_example_attack_discoveries_with_replacements'; -import { getRunAttackDiscoveriesWithReplacements } from './get_run_attack_discoveries_with_replacements'; -import { exampleWithReplacements } from '../../__mocks__/mock_examples'; -import { runWithReplacements } from '../../__mocks__/mock_runs'; - -const mockLlm = jest.fn() as unknown as ActionsClientLlm; - -jest.mock('langchain/evaluation', () => ({ - ...jest.requireActual('langchain/evaluation'), - loadEvaluator: jest.fn().mockResolvedValue({ - evaluateStrings: jest.fn().mockResolvedValue({ - key: 'correctness', - score: 0.9, - }), - }), -})); - -const options: GetCustomEvaluatorOptions = { - criteria: 'correctness', - key: 'attack_discovery_correctness', - llm: mockLlm, - template: getDefaultPromptTemplate(), -}; - -describe('getCustomEvaluator', () => { - beforeEach(() => jest.clearAllMocks()); - - it('returns an evaluator function', () => { - const evaluator = getCustomEvaluator(options); - - expect(typeof evaluator).toBe('function'); - }); - - it('calls loadEvaluator with the expected arguments', async () => { - const evaluator = getCustomEvaluator(options); - - await evaluator(runWithReplacements, exampleWithReplacements); - - expect(loadEvaluator).toHaveBeenCalledWith('labeled_criteria', { - criteria: options.criteria, - chainOptions: { - prompt: PromptTemplate.fromTemplate(options.template), - }, - llm: mockLlm, - }); - }); - - it('calls evaluateStrings with the expected arguments', async () => { - const mockEvaluateStrings = jest.fn().mockResolvedValue({ - key: 'correctness', - score: 0.9, - }); - - (loadEvaluator as jest.Mock).mockResolvedValue({ - evaluateStrings: mockEvaluateStrings, - }); - - const evaluator = getCustomEvaluator(options); - - await evaluator(runWithReplacements, exampleWithReplacements); - - const prediction = getRunAttackDiscoveriesWithReplacements(runWithReplacements); - const reference = getExampleAttackDiscoveriesWithReplacements(exampleWithReplacements); - - expect(mockEvaluateStrings).toHaveBeenCalledWith({ - input: '', - prediction: JSON.stringify(prediction, null, 2), - reference: JSON.stringify(reference, null, 2), - }); - }); - - it('returns the expected result', async () => { - const evaluator = getCustomEvaluator(options); - - const result = await evaluator(runWithReplacements, exampleWithReplacements); - - expect(result).toEqual({ key: 'attack_discovery_correctness', score: 0.9 }); - }); - - it('throws given an undefined example', async () => { - const evaluator = getCustomEvaluator(options); - - await expect(async () => evaluator(runWithReplacements, undefined)).rejects.toThrow(); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/index.ts deleted file mode 100644 index bcabe410c1b72..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_custom_evaluator/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ActionsClientLlm } from '@kbn/langchain/server'; -import { PromptTemplate } from '@langchain/core/prompts'; -import type { EvaluationResult } from 'langsmith/evaluation'; -import type { Run, Example } from 'langsmith/schemas'; -import { CriteriaLike, loadEvaluator } from 'langchain/evaluation'; - -import { getExampleAttackDiscoveriesWithReplacements } from './get_example_attack_discoveries_with_replacements'; -import { getRunAttackDiscoveriesWithReplacements } from './get_run_attack_discoveries_with_replacements'; - -export interface GetCustomEvaluatorOptions { - /** - * Examples: - * - "conciseness" - * - "relevance" - * - "correctness" - * - "detail" - */ - criteria: CriteriaLike; - /** - * The evaluation score will use this key - */ - key: string; - /** - * LLm to use for evaluation - */ - llm: ActionsClientLlm; - /** - * A prompt template that uses the {input}, {submission}, and {reference} variables - */ - template: string; -} - -export type CustomEvaluator = ( - rootRun: Run, - example: Example | undefined -) => Promise; - -export const getCustomEvaluator = - ({ criteria, key, llm, template }: GetCustomEvaluatorOptions): CustomEvaluator => - async (rootRun, example) => { - const chain = await loadEvaluator('labeled_criteria', { - criteria, - chainOptions: { - prompt: PromptTemplate.fromTemplate(template), - }, - llm, - }); - - const exampleAttackDiscoveriesWithReplacements = - getExampleAttackDiscoveriesWithReplacements(example); - - const runAttackDiscoveriesWithReplacements = getRunAttackDiscoveriesWithReplacements(rootRun); - - // NOTE: res contains a score, as well as the reasoning for the score - const res = await chain.evaluateStrings({ - input: '', // empty for now, but this could be the alerts, i.e. JSON.stringify(rootRun.outputs?.anonymizedAlerts, null, 2), - prediction: JSON.stringify(runAttackDiscoveriesWithReplacements, null, 2), - reference: JSON.stringify(exampleAttackDiscoveriesWithReplacements, null, 2), - }); - - return { key, score: res.score }; - }; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_discoveries_with_original_values/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_discoveries_with_original_values/index.test.ts deleted file mode 100644 index 423248aa5c3d6..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_discoveries_with_original_values/index.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AttackDiscovery } from '@kbn/elastic-assistant-common'; -import { omit } from 'lodash/fp'; - -import { getDiscoveriesWithOriginalValues } from '.'; -import { runWithReplacements } from '../../__mocks__/mock_runs'; - -describe('getDiscoveriesWithOriginalValues', () => { - it('returns attack discoveries with replacements applied to the detailsMarkdown, entitySummaryMarkdown, summaryMarkdown, and title', () => { - const result = getDiscoveriesWithOriginalValues({ - attackDiscoveries: runWithReplacements.outputs?.attackDiscoveries, - replacements: runWithReplacements.outputs?.replacements, - }); - - expect(result).toEqual([ - { - alertIds: [ - '4af5689eb58c2420efc0f7fad53c5bf9b8b6797e516d6ea87d6044ce25d54e16', - 'c675d7eb6ee181d788b474117bae8d3ed4bdc2168605c330a93dd342534fb02b', - '021b27d6bee0650a843be1d511119a3b5c7c8fdaeff922471ce0248ad27bd26c', - '6cc8d5f0e1c2b6c75219b001858f1be64194a97334be7a1e3572f8cfe6bae608', - 'f39a4013ed9609584a8a22dca902e896aa5b24d2da03e0eaab5556608fa682ac', - '909968e926e08a974c7df1613d98ebf1e2422afcb58e4e994beb47b063e85080', - '2c25a4dc31cd1ec254c2b19ea663fd0b09a16e239caa1218b4598801fb330da6', - '3bf907becb3a4f8e39a3b673e0d50fc954a7febef30c12891744c603760e4998', - ], - detailsMarkdown: - '- The attack began with the execution of a malicious file named `unix1` on the host `{{ host.name SRVMAC08 }}` by the user `{{ user.name james }}`.\n- The file `unix1` was detected at `{{ file.path /Users/james/unix1 }}` with a SHA256 hash of `{{ file.hash.sha256 0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231 }}`.\n- The process `{{ process.name My Go Application.app }}` was executed multiple times with different arguments, indicating potential persistence mechanisms.\n- The process `{{ process.name chmod }}` was used to change permissions of the file `unix1` to 777, making it executable.\n- A phishing attempt was detected via `osascript` on the same host, attempting to capture user credentials.\n- The attack involved multiple critical alerts, all indicating high-risk malware activity.', - entitySummaryMarkdown: - 'The host `{{ host.name SRVMAC08 }}` and user `{{ user.name james }}` were involved in the attack.', - mitreAttackTactics: ['Initial Access', 'Execution', 'Persistence', 'Credential Access'], - summaryMarkdown: - 'A series of critical malware alerts were detected on the host `{{ host.name SRVMAC08 }}` involving the user `{{ user.name james }}`. The attack included the execution of a malicious file `unix1`, permission changes, and a phishing attempt via `osascript`.', - title: 'Critical Malware Attack on macOS Host', - timestamp: '2024-10-11T17:55:59.702Z', - }, - ]); - }); - - it("returns an empty entitySummaryMarkdown when it's missing from the attack discovery", () => { - const missingEntitySummaryMarkdown = omit( - 'entitySummaryMarkdown', - runWithReplacements.outputs?.attackDiscoveries?.[0] - ) as unknown as AttackDiscovery; - - const result = getDiscoveriesWithOriginalValues({ - attackDiscoveries: [missingEntitySummaryMarkdown], - replacements: runWithReplacements.outputs?.replacements, - }); - expect(result).toEqual([ - { - alertIds: [ - '4af5689eb58c2420efc0f7fad53c5bf9b8b6797e516d6ea87d6044ce25d54e16', - 'c675d7eb6ee181d788b474117bae8d3ed4bdc2168605c330a93dd342534fb02b', - '021b27d6bee0650a843be1d511119a3b5c7c8fdaeff922471ce0248ad27bd26c', - '6cc8d5f0e1c2b6c75219b001858f1be64194a97334be7a1e3572f8cfe6bae608', - 'f39a4013ed9609584a8a22dca902e896aa5b24d2da03e0eaab5556608fa682ac', - '909968e926e08a974c7df1613d98ebf1e2422afcb58e4e994beb47b063e85080', - '2c25a4dc31cd1ec254c2b19ea663fd0b09a16e239caa1218b4598801fb330da6', - '3bf907becb3a4f8e39a3b673e0d50fc954a7febef30c12891744c603760e4998', - ], - detailsMarkdown: - '- The attack began with the execution of a malicious file named `unix1` on the host `{{ host.name SRVMAC08 }}` by the user `{{ user.name james }}`.\n- The file `unix1` was detected at `{{ file.path /Users/james/unix1 }}` with a SHA256 hash of `{{ file.hash.sha256 0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231 }}`.\n- The process `{{ process.name My Go Application.app }}` was executed multiple times with different arguments, indicating potential persistence mechanisms.\n- The process `{{ process.name chmod }}` was used to change permissions of the file `unix1` to 777, making it executable.\n- A phishing attempt was detected via `osascript` on the same host, attempting to capture user credentials.\n- The attack involved multiple critical alerts, all indicating high-risk malware activity.', - entitySummaryMarkdown: '', - mitreAttackTactics: ['Initial Access', 'Execution', 'Persistence', 'Credential Access'], - summaryMarkdown: - 'A series of critical malware alerts were detected on the host `{{ host.name SRVMAC08 }}` involving the user `{{ user.name james }}`. The attack included the execution of a malicious file `unix1`, permission changes, and a phishing attempt via `osascript`.', - title: 'Critical Malware Attack on macOS Host', - timestamp: '2024-10-11T17:55:59.702Z', - }, - ]); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_discoveries_with_original_values/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_discoveries_with_original_values/index.ts deleted file mode 100644 index 1ef88e2208d1f..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_discoveries_with_original_values/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - type AttackDiscovery, - Replacements, - replaceAnonymizedValuesWithOriginalValues, -} from '@kbn/elastic-assistant-common'; - -export const getDiscoveriesWithOriginalValues = ({ - attackDiscoveries, - replacements, -}: { - attackDiscoveries: AttackDiscovery[]; - replacements: Replacements; -}): AttackDiscovery[] => - attackDiscoveries.map((attackDiscovery) => ({ - ...attackDiscovery, - detailsMarkdown: replaceAnonymizedValuesWithOriginalValues({ - messageContent: attackDiscovery.detailsMarkdown, - replacements, - }), - entitySummaryMarkdown: replaceAnonymizedValuesWithOriginalValues({ - messageContent: attackDiscovery.entitySummaryMarkdown ?? '', - replacements, - }), - summaryMarkdown: replaceAnonymizedValuesWithOriginalValues({ - messageContent: attackDiscovery.summaryMarkdown, - replacements, - }), - title: replaceAnonymizedValuesWithOriginalValues({ - messageContent: attackDiscovery.title, - replacements, - }), - })); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_evaluator_llm/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_evaluator_llm/index.test.ts deleted file mode 100644 index 132a819d44ec8..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_evaluator_llm/index.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ActionsClient } from '@kbn/actions-plugin/server'; -import type { Connector } from '@kbn/actions-plugin/server/application/connector/types'; -import { ActionsClientLlm } from '@kbn/langchain/server'; -import { loggerMock } from '@kbn/logging-mocks'; - -import { getEvaluatorLlm } from '.'; - -jest.mock('@kbn/langchain/server', () => ({ - ...jest.requireActual('@kbn/langchain/server'), - - ActionsClientLlm: jest.fn(), -})); - -const connectorTimeout = 1000; - -const evaluatorConnectorId = 'evaluator-connector-id'; -const evaluatorConnector = { - id: 'evaluatorConnectorId', - actionTypeId: '.gen-ai', - name: 'GPT-4o', - isPreconfigured: true, - isSystemAction: false, - isDeprecated: false, -} as Connector; - -const experimentConnector: Connector = { - name: 'Gemini 1.5 Pro 002', - actionTypeId: '.gemini', - config: { - apiUrl: 'https://example.com', - defaultModel: 'gemini-1.5-pro-002', - gcpRegion: 'test-region', - gcpProjectID: 'test-project-id', - }, - secrets: { - credentialsJson: '{}', - }, - id: 'gemini-1-5-pro-002', - isPreconfigured: true, - isSystemAction: false, - isDeprecated: false, -} as Connector; - -const logger = loggerMock.create(); - -describe('getEvaluatorLlm', () => { - beforeEach(() => jest.clearAllMocks()); - - describe('getting the evaluation connector', () => { - it("calls actionsClient.get with the evaluator connector ID when it's provided", async () => { - const actionsClient = { - get: jest.fn(), - } as unknown as ActionsClient; - - await getEvaluatorLlm({ - actionsClient, - connectorTimeout, - evaluatorConnectorId, - experimentConnector, - langSmithApiKey: undefined, - logger, - }); - - expect(actionsClient.get).toHaveBeenCalledWith({ - id: evaluatorConnectorId, - throwIfSystemAction: false, - }); - }); - - it("calls actionsClient.get with the experiment connector ID when the evaluator connector ID isn't provided", async () => { - const actionsClient = { - get: jest.fn().mockResolvedValue(null), - } as unknown as ActionsClient; - - await getEvaluatorLlm({ - actionsClient, - connectorTimeout, - evaluatorConnectorId: undefined, - experimentConnector, - langSmithApiKey: undefined, - logger, - }); - - expect(actionsClient.get).toHaveBeenCalledWith({ - id: experimentConnector.id, - throwIfSystemAction: false, - }); - }); - - it('falls back to the experiment connector when the evaluator connector is not found', async () => { - const actionsClient = { - get: jest.fn().mockResolvedValue(null), - } as unknown as ActionsClient; - - await getEvaluatorLlm({ - actionsClient, - connectorTimeout, - evaluatorConnectorId, - experimentConnector, - langSmithApiKey: undefined, - logger, - }); - - expect(ActionsClientLlm).toHaveBeenCalledWith( - expect.objectContaining({ - connectorId: experimentConnector.id, - }) - ); - }); - }); - - it('logs the expected connector names and types', async () => { - const actionsClient = { - get: jest.fn().mockResolvedValue(evaluatorConnector), - } as unknown as ActionsClient; - - await getEvaluatorLlm({ - actionsClient, - connectorTimeout, - evaluatorConnectorId, - experimentConnector, - langSmithApiKey: undefined, - logger, - }); - - expect(logger.info).toHaveBeenCalledWith( - `The ${evaluatorConnector.name} (openai) connector will judge output from the ${experimentConnector.name} (gemini) connector` - ); - }); - - it('creates a new ActionsClientLlm instance with the expected traceOptions', async () => { - const actionsClient = { - get: jest.fn().mockResolvedValue(evaluatorConnector), - } as unknown as ActionsClient; - - await getEvaluatorLlm({ - actionsClient, - connectorTimeout, - evaluatorConnectorId, - experimentConnector, - langSmithApiKey: 'test-api-key', - logger, - }); - - expect(ActionsClientLlm).toHaveBeenCalledWith( - expect.objectContaining({ - traceOptions: { - projectName: 'evaluators', - tracers: expect.any(Array), - }, - }) - ); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_evaluator_llm/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_evaluator_llm/index.ts deleted file mode 100644 index 236def9670d07..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_evaluator_llm/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ActionsClient } from '@kbn/actions-plugin/server'; -import type { Connector } from '@kbn/actions-plugin/server/application/connector/types'; -import { Logger } from '@kbn/core/server'; -import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith'; -import { ActionsClientLlm } from '@kbn/langchain/server'; -import { PublicMethodsOf } from '@kbn/utility-types'; - -import { getLlmType } from '../../../../../routes/utils'; - -export const getEvaluatorLlm = async ({ - actionsClient, - connectorTimeout, - evaluatorConnectorId, - experimentConnector, - langSmithApiKey, - logger, -}: { - actionsClient: PublicMethodsOf; - connectorTimeout: number; - evaluatorConnectorId: string | undefined; - experimentConnector: Connector; - langSmithApiKey: string | undefined; - logger: Logger; -}): Promise => { - const evaluatorConnector = - (await actionsClient.get({ - id: evaluatorConnectorId ?? experimentConnector.id, // fallback to the experiment connector if the evaluator connector is not found: - throwIfSystemAction: false, - })) ?? experimentConnector; - - const evaluatorLlmType = getLlmType(evaluatorConnector.actionTypeId); - const experimentLlmType = getLlmType(experimentConnector.actionTypeId); - - logger.info( - `The ${evaluatorConnector.name} (${evaluatorLlmType}) connector will judge output from the ${experimentConnector.name} (${experimentLlmType}) connector` - ); - - const traceOptions = { - projectName: 'evaluators', - tracers: [ - ...getLangSmithTracer({ - apiKey: langSmithApiKey, - projectName: 'evaluators', - logger, - }), - ], - }; - - return new ActionsClientLlm({ - actionsClient, - connectorId: evaluatorConnector.id, - llmType: evaluatorLlmType, - logger, - temperature: 0, // zero temperature for evaluation - timeout: connectorTimeout, - traceOptions, - }); -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_graph_input_overrides/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_graph_input_overrides/index.test.ts deleted file mode 100644 index 47f36bc6fb0e7..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_graph_input_overrides/index.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { omit } from 'lodash/fp'; -import type { Example } from 'langsmith/schemas'; - -import { getGraphInputOverrides } from '.'; -import { exampleWithReplacements } from '../../__mocks__/mock_examples'; - -const exampleWithAlerts: Example = { - ...exampleWithReplacements, - outputs: { - ...exampleWithReplacements.outputs, - anonymizedAlerts: [ - { - metadata: {}, - pageContent: - '@timestamp,2024-10-10T21:01:24.148Z\n' + - '_id,e809ffc5e0c2e731c1f146e0f74250078136a87574534bf8e9ee55445894f7fc\n' + - 'host.name,e1cb3cf0-30f3-4f99-a9c8-518b955c6f90\n' + - 'user.name,039c15c5-3964-43e7-a891-42fe2ceeb9ff', - }, - { - metadata: {}, - pageContent: - '@timestamp,2024-10-10T21:01:24.148Z\n' + - '_id,c675d7eb6ee181d788b474117bae8d3ed4bdc2168605c330a93dd342534fb02b\n' + - 'host.name,e1cb3cf0-30f3-4f99-a9c8-518b955c6f90\n' + - 'user.name,039c15c5-3964-43e7-a891-42fe2ceeb9ff', - }, - ], - }, -}; - -const exampleWithNoReplacements: Example = { - ...exampleWithReplacements, - outputs: { - ...omit('replacements', exampleWithReplacements.outputs), - }, -}; - -describe('getGraphInputOverrides', () => { - describe('root-level outputs overrides', () => { - it('returns the anonymizedAlerts from the root level of the outputs when present', () => { - const overrides = getGraphInputOverrides(exampleWithAlerts.outputs); - - expect(overrides.anonymizedAlerts).toEqual(exampleWithAlerts.outputs?.anonymizedAlerts); - }); - - it('does NOT populate the anonymizedAlerts key when it does NOT exist in the outputs', () => { - const overrides = getGraphInputOverrides(exampleWithReplacements.outputs); - - expect(overrides).not.toHaveProperty('anonymizedAlerts'); - }); - - it('returns replacements from the root level of the outputs when present', () => { - const overrides = getGraphInputOverrides(exampleWithReplacements.outputs); - - expect(overrides.replacements).toEqual(exampleWithReplacements.outputs?.replacements); - }); - - it('does NOT populate the replacements key when it does NOT exist in the outputs', () => { - const overrides = getGraphInputOverrides(exampleWithNoReplacements.outputs); - - expect(overrides).not.toHaveProperty('replacements'); - }); - - it('removes unknown properties', () => { - const withUnknownProperties = { - ...exampleWithReplacements, - outputs: { - ...exampleWithReplacements.outputs, - unknownProperty: 'unknown', - }, - }; - - const overrides = getGraphInputOverrides(withUnknownProperties.outputs); - - expect(overrides).not.toHaveProperty('unknownProperty'); - }); - }); - - describe('overrides', () => { - it('returns all overrides at the root level', () => { - const exampleWithOverrides = { - ...exampleWithAlerts, - outputs: { - ...exampleWithAlerts.outputs, - overrides: { - attackDiscoveries: [], - attackDiscoveryPrompt: 'prompt', - anonymizedAlerts: [], - combinedGenerations: 'combinedGenerations', - combinedRefinements: 'combinedRefinements', - errors: ['error'], - generationAttempts: 1, - generations: ['generation'], - hallucinationFailures: 2, - maxGenerationAttempts: 3, - maxHallucinationFailures: 4, - maxRepeatedGenerations: 5, - refinements: ['refinement'], - refinePrompt: 'refinePrompt', - replacements: {}, - unrefinedResults: [], - }, - }, - }; - - const overrides = getGraphInputOverrides(exampleWithOverrides.outputs); - - expect(overrides).toEqual({ - ...exampleWithOverrides.outputs?.overrides, - }); - }); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_graph_input_overrides/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_graph_input_overrides/index.ts deleted file mode 100644 index 232218f4386f8..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/helpers/get_graph_input_overrides/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pick } from 'lodash/fp'; - -import { ExampleInputWithOverrides } from '../../example_input'; -import { GraphState } from '../../../graphs/default_attack_discovery_graph/types'; - -/** - * Parses input from an LangSmith dataset example to get the graph input overrides - */ -export const getGraphInputOverrides = (outputs: unknown): Partial => { - const validatedInput = ExampleInputWithOverrides.safeParse(outputs).data ?? {}; // safeParse removes unknown properties - - const { overrides } = validatedInput; - - // return all overrides at the root level: - return { - // pick extracts just the anonymizedAlerts and replacements from the root level of the input, - // and only adds the anonymizedAlerts key if it exists in the input - ...pick('anonymizedAlerts', validatedInput), - ...pick('replacements', validatedInput), - ...overrides, // bring all other overrides to the root level - }; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/index.ts deleted file mode 100644 index 40b0f080fe54a..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/index.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ActionsClient } from '@kbn/actions-plugin/server'; -import type { Connector } from '@kbn/actions-plugin/server/application/connector/types'; -import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import { Logger } from '@kbn/core/server'; -import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen'; -import type { LangChainTracer } from '@langchain/core/tracers/tracer_langchain'; -import { ActionsClientLlm } from '@kbn/langchain/server'; -import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith'; -import { asyncForEach } from '@kbn/std'; -import { PublicMethodsOf } from '@kbn/utility-types'; - -import { DEFAULT_EVAL_ANONYMIZATION_FIELDS } from './constants'; -import { AttackDiscoveryGraphMetadata } from '../../langchain/graphs'; -import { DefaultAttackDiscoveryGraph } from '../graphs/default_attack_discovery_graph'; -import { getLlmType } from '../../../routes/utils'; -import { runEvaluations } from './run_evaluations'; - -export const evaluateAttackDiscovery = async ({ - actionsClient, - attackDiscoveryGraphs, - alertsIndexPattern, - anonymizationFields = DEFAULT_EVAL_ANONYMIZATION_FIELDS, // determines which fields are included in the alerts - connectors, - connectorTimeout, - datasetName, - esClient, - evaluationId, - evaluatorConnectorId, - langSmithApiKey, - langSmithProject, - logger, - runName, - size, -}: { - actionsClient: PublicMethodsOf; - attackDiscoveryGraphs: AttackDiscoveryGraphMetadata[]; - alertsIndexPattern: string; - anonymizationFields?: AnonymizationFieldResponse[]; - connectors: Connector[]; - connectorTimeout: number; - datasetName: string; - esClient: ElasticsearchClient; - evaluationId: string; - evaluatorConnectorId: string | undefined; - langSmithApiKey: string | undefined; - langSmithProject: string | undefined; - logger: Logger; - runName: string; - size: number; -}): Promise => { - await asyncForEach(attackDiscoveryGraphs, async ({ getDefaultAttackDiscoveryGraph }) => { - // create a graph for every connector: - const graphs: Array<{ - connector: Connector; - graph: DefaultAttackDiscoveryGraph; - llmType: string | undefined; - name: string; - traceOptions: { - projectName: string | undefined; - tracers: LangChainTracer[]; - }; - }> = connectors.map((connector) => { - const llmType = getLlmType(connector.actionTypeId); - - const traceOptions = { - projectName: langSmithProject, - tracers: [ - ...getLangSmithTracer({ - apiKey: langSmithApiKey, - projectName: langSmithProject, - logger, - }), - ], - }; - - const llm = new ActionsClientLlm({ - actionsClient, - connectorId: connector.id, - llmType, - logger, - temperature: 0, // zero temperature for attack discovery, because we want structured JSON output - timeout: connectorTimeout, - traceOptions, - }); - - const graph = getDefaultAttackDiscoveryGraph({ - alertsIndexPattern, - anonymizationFields, - esClient, - llm, - logger, - size, - }); - - return { - connector, - graph, - llmType, - name: `${runName} - ${connector.name} - ${evaluationId} - Attack discovery`, - traceOptions, - }; - }); - - // run the evaluations for each graph: - await runEvaluations({ - actionsClient, - connectorTimeout, - evaluatorConnectorId, - datasetName, - graphs, - langSmithApiKey, - logger, - }); - }); -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/run_evaluations/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/run_evaluations/index.ts deleted file mode 100644 index 19eb99d57c84c..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/evaluation/run_evaluations/index.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ActionsClient } from '@kbn/actions-plugin/server'; -import type { Connector } from '@kbn/actions-plugin/server/application/connector/types'; -import { Logger } from '@kbn/core/server'; -import type { LangChainTracer } from '@langchain/core/tracers/tracer_langchain'; -import { asyncForEach } from '@kbn/std'; -import { PublicMethodsOf } from '@kbn/utility-types'; -import { Client } from 'langsmith'; -import { evaluate } from 'langsmith/evaluation'; - -import { getEvaluatorLlm } from '../helpers/get_evaluator_llm'; -import { getCustomEvaluator } from '../helpers/get_custom_evaluator'; -import { getDefaultPromptTemplate } from '../helpers/get_custom_evaluator/get_default_prompt_template'; -import { getGraphInputOverrides } from '../helpers/get_graph_input_overrides'; -import { DefaultAttackDiscoveryGraph } from '../../graphs/default_attack_discovery_graph'; -import { GraphState } from '../../graphs/default_attack_discovery_graph/types'; - -/** - * Runs an evaluation for each graph so they show up separately (resulting in - * each dataset run grouped by connector) - */ -export const runEvaluations = async ({ - actionsClient, - connectorTimeout, - evaluatorConnectorId, - datasetName, - graphs, - langSmithApiKey, - logger, -}: { - actionsClient: PublicMethodsOf; - connectorTimeout: number; - evaluatorConnectorId: string | undefined; - datasetName: string; - graphs: Array<{ - connector: Connector; - graph: DefaultAttackDiscoveryGraph; - llmType: string | undefined; - name: string; - traceOptions: { - projectName: string | undefined; - tracers: LangChainTracer[]; - }; - }>; - langSmithApiKey: string | undefined; - logger: Logger; -}): Promise => - asyncForEach(graphs, async ({ connector, graph, llmType, name, traceOptions }) => { - const subject = `connector "${connector.name}" (${llmType}), running experiment "${name}"`; - - try { - logger.info( - () => - `Evaluating ${subject} with dataset "${datasetName}" and evaluator "${evaluatorConnectorId}"` - ); - - const predict = async (input: unknown): Promise => { - logger.debug(() => `Raw example Input for ${subject}":\n ${input}`); - - // The example `Input` may have overrides for the initial state of the graph: - const overrides = getGraphInputOverrides(input); - - return graph.invoke( - { - ...overrides, - }, - { - callbacks: [...(traceOptions.tracers ?? [])], - runName: name, - tags: ['evaluation', llmType ?? ''], - } - ); - }; - - const llm = await getEvaluatorLlm({ - actionsClient, - connectorTimeout, - evaluatorConnectorId, - experimentConnector: connector, - langSmithApiKey, - logger, - }); - - const customEvaluator = getCustomEvaluator({ - criteria: 'correctness', - key: 'attack_discovery_correctness', - llm, - template: getDefaultPromptTemplate(), - }); - - const evalOutput = await evaluate(predict, { - client: new Client({ apiKey: langSmithApiKey }), - data: datasetName ?? '', - evaluators: [customEvaluator], - experimentPrefix: name, - maxConcurrency: 5, // prevents rate limiting - }); - - logger.info(() => `Evaluation complete for ${subject}`); - - logger.debug( - () => `Evaluation output for ${subject}:\n ${JSON.stringify(evalOutput, null, 2)}` - ); - } catch (e) { - logger.error(`Error evaluating ${subject}: ${e}`); - } - }); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/constants.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/constants.ts deleted file mode 100644 index fb5df8f26d0c2..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/constants.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// LangGraph metadata -export const ATTACK_DISCOVERY_GRAPH_RUN_NAME = 'Attack discovery'; -export const ATTACK_DISCOVERY_TAG = 'attack-discovery'; - -// Limits -export const DEFAULT_MAX_GENERATION_ATTEMPTS = 10; -export const DEFAULT_MAX_HALLUCINATION_FAILURES = 5; -export const DEFAULT_MAX_REPEATED_GENERATIONS = 3; - -export const NodeType = { - GENERATE_NODE: 'generate', - REFINE_NODE: 'refine', - RETRIEVE_ANONYMIZED_ALERTS_NODE: 'retrieve_anonymized_alerts', -} as const; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_end/helpers/get_generate_or_end_decision/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_end/helpers/get_generate_or_end_decision/index.test.ts deleted file mode 100644 index 225c4a2b8935c..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_end/helpers/get_generate_or_end_decision/index.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getGenerateOrEndDecision } from '.'; - -describe('getGenerateOrEndDecision', () => { - it('returns "end" when hasZeroAlerts is true', () => { - const result = getGenerateOrEndDecision(true); - - expect(result).toEqual('end'); - }); - - it('returns "generate" when hasZeroAlerts is false', () => { - const result = getGenerateOrEndDecision(false); - - expect(result).toEqual('generate'); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_end/helpers/get_generate_or_end_decision/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_end/helpers/get_generate_or_end_decision/index.ts deleted file mode 100644 index b134b2f3a6118..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_end/helpers/get_generate_or_end_decision/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const getGenerateOrEndDecision = (hasZeroAlerts: boolean): 'end' | 'generate' => - hasZeroAlerts ? 'end' : 'generate'; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_end/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_end/index.test.ts deleted file mode 100644 index 06dd1529179fa..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_end/index.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { loggerMock } from '@kbn/logging-mocks'; - -import { getGenerateOrEndEdge } from '.'; -import type { GraphState } from '../../types'; - -const logger = loggerMock.create(); - -const graphState: GraphState = { - attackDiscoveries: null, - attackDiscoveryPrompt: 'prompt', - anonymizedAlerts: [ - { - metadata: {}, - pageContent: - '@timestamp,2024-10-10T21:01:24.148Z\n' + - '_id,e809ffc5e0c2e731c1f146e0f74250078136a87574534bf8e9ee55445894f7fc\n' + - 'host.name,e1cb3cf0-30f3-4f99-a9c8-518b955c6f90\n' + - 'user.name,039c15c5-3964-43e7-a891-42fe2ceeb9ff', - }, - { - metadata: {}, - pageContent: - '@timestamp,2024-10-10T21:01:24.148Z\n' + - '_id,c675d7eb6ee181d788b474117bae8d3ed4bdc2168605c330a93dd342534fb02b\n' + - 'host.name,e1cb3cf0-30f3-4f99-a9c8-518b955c6f90\n' + - 'user.name,039c15c5-3964-43e7-a891-42fe2ceeb9ff', - }, - ], - combinedGenerations: 'generations', - combinedRefinements: 'refinements', - errors: [], - generationAttempts: 0, - generations: [], - hallucinationFailures: 0, - maxGenerationAttempts: 10, - maxHallucinationFailures: 5, - maxRepeatedGenerations: 10, - refinements: [], - refinePrompt: 'refinePrompt', - replacements: {}, - unrefinedResults: null, -}; - -describe('getGenerateOrEndEdge', () => { - beforeEach(() => jest.clearAllMocks()); - - it("returns 'end' when there are zero alerts", () => { - const state: GraphState = { - ...graphState, - anonymizedAlerts: [], // <-- zero alerts - }; - - const edge = getGenerateOrEndEdge(logger); - const result = edge(state); - - expect(result).toEqual('end'); - }); - - it("returns 'generate' when there are alerts", () => { - const edge = getGenerateOrEndEdge(logger); - const result = edge(graphState); - - expect(result).toEqual('generate'); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_end/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_end/index.ts deleted file mode 100644 index 5bfc4912298eb..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_end/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Logger } from '@kbn/core/server'; - -import { getGenerateOrEndDecision } from './helpers/get_generate_or_end_decision'; -import { getHasZeroAlerts } from '../helpers/get_has_zero_alerts'; -import type { GraphState } from '../../types'; - -export const getGenerateOrEndEdge = (logger?: Logger) => { - const edge = (state: GraphState): 'end' | 'generate' => { - logger?.debug(() => '---GENERATE OR END---'); - const { anonymizedAlerts } = state; - - const hasZeroAlerts = getHasZeroAlerts(anonymizedAlerts); - - const decision = getGenerateOrEndDecision(hasZeroAlerts); - - logger?.debug( - () => `generatOrEndEdge evaluated the following (derived) state:\n${JSON.stringify( - { - anonymizedAlerts: anonymizedAlerts.length, - hasZeroAlerts, - }, - null, - 2 - )} -\n---GENERATE OR END: ${decision}---` - ); - return decision; - }; - - return edge; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/helpers/get_generate_or_refine_or_end_decision/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/helpers/get_generate_or_refine_or_end_decision/index.test.ts deleted file mode 100644 index 42c63b18459ed..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/helpers/get_generate_or_refine_or_end_decision/index.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getGenerateOrRefineOrEndDecision } from '.'; - -describe('getGenerateOrRefineOrEndDecision', () => { - it("returns 'end' if getShouldEnd returns true", () => { - const result = getGenerateOrRefineOrEndDecision({ - hasUnrefinedResults: false, - hasZeroAlerts: true, - maxHallucinationFailuresReached: true, - maxRetriesReached: true, - }); - - expect(result).toEqual('end'); - }); - - it("returns 'refine' if hasUnrefinedResults is true and getShouldEnd returns false", () => { - const result = getGenerateOrRefineOrEndDecision({ - hasUnrefinedResults: true, - hasZeroAlerts: false, - maxHallucinationFailuresReached: false, - maxRetriesReached: false, - }); - - expect(result).toEqual('refine'); - }); - - it("returns 'generate' if hasUnrefinedResults is false and getShouldEnd returns false", () => { - const result = getGenerateOrRefineOrEndDecision({ - hasUnrefinedResults: false, - hasZeroAlerts: false, - maxHallucinationFailuresReached: false, - maxRetriesReached: false, - }); - - expect(result).toEqual('generate'); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/helpers/get_generate_or_refine_or_end_decision/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/helpers/get_generate_or_refine_or_end_decision/index.ts deleted file mode 100644 index b409f63f71a69..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/helpers/get_generate_or_refine_or_end_decision/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getShouldEnd } from '../get_should_end'; - -export const getGenerateOrRefineOrEndDecision = ({ - hasUnrefinedResults, - hasZeroAlerts, - maxHallucinationFailuresReached, - maxRetriesReached, -}: { - hasUnrefinedResults: boolean; - hasZeroAlerts: boolean; - maxHallucinationFailuresReached: boolean; - maxRetriesReached: boolean; -}): 'end' | 'generate' | 'refine' => { - if (getShouldEnd({ hasZeroAlerts, maxHallucinationFailuresReached, maxRetriesReached })) { - return 'end'; - } else if (hasUnrefinedResults) { - return 'refine'; - } else { - return 'generate'; - } -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/helpers/get_should_end/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/helpers/get_should_end/index.test.ts deleted file mode 100644 index 82480a6ad6889..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/helpers/get_should_end/index.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getShouldEnd } from '.'; - -describe('getShouldEnd', () => { - it('returns true if hasZeroAlerts is true', () => { - const result = getShouldEnd({ - hasZeroAlerts: true, // <-- true - maxHallucinationFailuresReached: false, - maxRetriesReached: false, - }); - - expect(result).toBe(true); - }); - - it('returns true if maxHallucinationFailuresReached is true', () => { - const result = getShouldEnd({ - hasZeroAlerts: false, - maxHallucinationFailuresReached: true, // <-- true - maxRetriesReached: false, - }); - - expect(result).toBe(true); - }); - - it('returns true if maxRetriesReached is true', () => { - const result = getShouldEnd({ - hasZeroAlerts: false, - maxHallucinationFailuresReached: false, - maxRetriesReached: true, // <-- true - }); - - expect(result).toBe(true); - }); - - it('returns false if all conditions are false', () => { - const result = getShouldEnd({ - hasZeroAlerts: false, - maxHallucinationFailuresReached: false, - maxRetriesReached: false, - }); - - expect(result).toBe(false); - }); - - it('returns true if all conditions are true', () => { - const result = getShouldEnd({ - hasZeroAlerts: true, - maxHallucinationFailuresReached: true, - maxRetriesReached: true, - }); - - expect(result).toBe(true); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/helpers/get_should_end/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/helpers/get_should_end/index.ts deleted file mode 100644 index 9724ba25886fa..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/helpers/get_should_end/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const getShouldEnd = ({ - hasZeroAlerts, - maxHallucinationFailuresReached, - maxRetriesReached, -}: { - hasZeroAlerts: boolean; - maxHallucinationFailuresReached: boolean; - maxRetriesReached: boolean; -}): boolean => hasZeroAlerts || maxRetriesReached || maxHallucinationFailuresReached; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/index.test.ts deleted file mode 100644 index 585a1bc2dcac3..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/index.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { loggerMock } from '@kbn/logging-mocks'; - -import { getGenerateOrRefineOrEndEdge } from '.'; -import type { GraphState } from '../../types'; - -const logger = loggerMock.create(); - -const graphState: GraphState = { - attackDiscoveries: null, - attackDiscoveryPrompt: 'prompt', - anonymizedAlerts: [ - { - metadata: {}, - pageContent: - '@timestamp,2024-10-10T21:01:24.148Z\n' + - '_id,e809ffc5e0c2e731c1f146e0f74250078136a87574534bf8e9ee55445894f7fc\n' + - 'host.name,e1cb3cf0-30f3-4f99-a9c8-518b955c6f90\n' + - 'user.name,039c15c5-3964-43e7-a891-42fe2ceeb9ff', - }, - { - metadata: {}, - pageContent: - '@timestamp,2024-10-10T21:01:24.148Z\n' + - '_id,c675d7eb6ee181d788b474117bae8d3ed4bdc2168605c330a93dd342534fb02b\n' + - 'host.name,e1cb3cf0-30f3-4f99-a9c8-518b955c6f90\n' + - 'user.name,039c15c5-3964-43e7-a891-42fe2ceeb9ff', - }, - ], - combinedGenerations: '', - combinedRefinements: '', - errors: [], - generationAttempts: 0, - generations: [], - hallucinationFailures: 0, - maxGenerationAttempts: 10, - maxHallucinationFailures: 5, - maxRepeatedGenerations: 3, - refinements: [], - refinePrompt: 'refinePrompt', - replacements: {}, - unrefinedResults: null, -}; - -describe('getGenerateOrRefineOrEndEdge', () => { - beforeEach(() => jest.clearAllMocks()); - - it('returns "end" when there are zero alerts', () => { - const withZeroAlerts: GraphState = { - ...graphState, - anonymizedAlerts: [], // <-- zero alerts - }; - - const edge = getGenerateOrRefineOrEndEdge(logger); - const result = edge(withZeroAlerts); - - expect(result).toEqual('end'); - }); - - it('returns "end" when max hallucination failures are reached', () => { - const withMaxHallucinationFailures: GraphState = { - ...graphState, - hallucinationFailures: 5, - }; - - const edge = getGenerateOrRefineOrEndEdge(logger); - const result = edge(withMaxHallucinationFailures); - - expect(result).toEqual('end'); - }); - - it('returns "end" when max retries are reached', () => { - const withMaxRetries: GraphState = { - ...graphState, - generationAttempts: 10, - }; - - const edge = getGenerateOrRefineOrEndEdge(logger); - const result = edge(withMaxRetries); - - expect(result).toEqual('end'); - }); - - it('returns refine when there are unrefined results', () => { - const withUnrefinedResults: GraphState = { - ...graphState, - unrefinedResults: [ - { - alertIds: [], - id: 'test-id', - detailsMarkdown: 'test-details', - entitySummaryMarkdown: 'test-summary', - summaryMarkdown: 'test-summary', - title: 'test-title', - timestamp: '2024-10-10T21:01:24.148Z', - }, - ], - }; - - const edge = getGenerateOrRefineOrEndEdge(logger); - const result = edge(withUnrefinedResults); - - expect(result).toEqual('refine'); - }); - - it('return generate when there are no unrefined results', () => { - const edge = getGenerateOrRefineOrEndEdge(logger); - const result = edge(graphState); - - expect(result).toEqual('generate'); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/index.ts deleted file mode 100644 index 3368a04ec9204..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/generate_or_refine_or_end/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Logger } from '@kbn/core/server'; - -import { getGenerateOrRefineOrEndDecision } from './helpers/get_generate_or_refine_or_end_decision'; -import { getHasResults } from '../helpers/get_has_results'; -import { getHasZeroAlerts } from '../helpers/get_has_zero_alerts'; -import { getMaxHallucinationFailuresReached } from '../../helpers/get_max_hallucination_failures_reached'; -import { getMaxRetriesReached } from '../../helpers/get_max_retries_reached'; -import type { GraphState } from '../../types'; - -export const getGenerateOrRefineOrEndEdge = (logger?: Logger) => { - const edge = (state: GraphState): 'end' | 'generate' | 'refine' => { - logger?.debug(() => '---GENERATE OR REFINE OR END---'); - const { - anonymizedAlerts, - generationAttempts, - hallucinationFailures, - maxGenerationAttempts, - maxHallucinationFailures, - unrefinedResults, - } = state; - - const hasZeroAlerts = getHasZeroAlerts(anonymizedAlerts); - const hasUnrefinedResults = getHasResults(unrefinedResults); - const maxRetriesReached = getMaxRetriesReached({ generationAttempts, maxGenerationAttempts }); - const maxHallucinationFailuresReached = getMaxHallucinationFailuresReached({ - hallucinationFailures, - maxHallucinationFailures, - }); - - const decision = getGenerateOrRefineOrEndDecision({ - hasUnrefinedResults, - hasZeroAlerts, - maxHallucinationFailuresReached, - maxRetriesReached, - }); - - logger?.debug( - () => - `generatOrRefineOrEndEdge evaluated the following (derived) state:\n${JSON.stringify( - { - anonymizedAlerts: anonymizedAlerts.length, - generationAttempts, - hallucinationFailures, - hasUnrefinedResults, - hasZeroAlerts, - maxHallucinationFailuresReached, - maxRetriesReached, - unrefinedResults: unrefinedResults?.length ?? 0, - }, - null, - 2 - )} - \n---GENERATE OR REFINE OR END: ${decision}---` - ); - return decision; - }; - - return edge; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/helpers/get_has_results/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/helpers/get_has_results/index.ts deleted file mode 100644 index 413f01b74dece..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/helpers/get_has_results/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AttackDiscovery } from '@kbn/elastic-assistant-common'; - -export const getHasResults = (attackDiscoveries: AttackDiscovery[] | null): boolean => - attackDiscoveries !== null; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/helpers/get_has_zero_alerts/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/helpers/get_has_zero_alerts/index.ts deleted file mode 100644 index d768b363f101e..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/helpers/get_has_zero_alerts/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Document } from '@langchain/core/documents'; -import { isEmpty } from 'lodash/fp'; - -export const getHasZeroAlerts = (anonymizedAlerts: Document[]): boolean => - isEmpty(anonymizedAlerts); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/helpers/get_refine_or_end_decision/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/helpers/get_refine_or_end_decision/index.ts deleted file mode 100644 index 7168aa08aeef2..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/helpers/get_refine_or_end_decision/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getShouldEnd } from '../get_should_end'; - -export const getRefineOrEndDecision = ({ - hasFinalResults, - maxHallucinationFailuresReached, - maxRetriesReached, -}: { - hasFinalResults: boolean; - maxHallucinationFailuresReached: boolean; - maxRetriesReached: boolean; -}): 'refine' | 'end' => - getShouldEnd({ - hasFinalResults, - maxHallucinationFailuresReached, - maxRetriesReached, - }) - ? 'end' - : 'refine'; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/helpers/get_should_end/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/helpers/get_should_end/index.ts deleted file mode 100644 index 697f93dd3a02f..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/helpers/get_should_end/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const getShouldEnd = ({ - hasFinalResults, - maxHallucinationFailuresReached, - maxRetriesReached, -}: { - hasFinalResults: boolean; - maxHallucinationFailuresReached: boolean; - maxRetriesReached: boolean; -}): boolean => hasFinalResults || maxRetriesReached || maxHallucinationFailuresReached; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/index.ts deleted file mode 100644 index 85140dceafdcb..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/refine_or_end/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Logger } from '@kbn/core/server'; - -import { getRefineOrEndDecision } from './helpers/get_refine_or_end_decision'; -import { getHasResults } from '../helpers/get_has_results'; -import { getMaxHallucinationFailuresReached } from '../../helpers/get_max_hallucination_failures_reached'; -import { getMaxRetriesReached } from '../../helpers/get_max_retries_reached'; -import type { GraphState } from '../../types'; - -export const getRefineOrEndEdge = (logger?: Logger) => { - const edge = (state: GraphState): 'end' | 'refine' => { - logger?.debug(() => '---REFINE OR END---'); - const { - attackDiscoveries, - generationAttempts, - hallucinationFailures, - maxGenerationAttempts, - maxHallucinationFailures, - } = state; - - const hasFinalResults = getHasResults(attackDiscoveries); - const maxRetriesReached = getMaxRetriesReached({ generationAttempts, maxGenerationAttempts }); - const maxHallucinationFailuresReached = getMaxHallucinationFailuresReached({ - hallucinationFailures, - maxHallucinationFailures, - }); - - const decision = getRefineOrEndDecision({ - hasFinalResults, - maxHallucinationFailuresReached, - maxRetriesReached, - }); - - logger?.debug( - () => - `refineOrEndEdge evaluated the following (derived) state:\n${JSON.stringify( - { - attackDiscoveries: attackDiscoveries?.length ?? 0, - generationAttempts, - hallucinationFailures, - hasFinalResults, - maxHallucinationFailuresReached, - maxRetriesReached, - }, - null, - 2 - )} - \n---REFINE OR END: ${decision}---` - ); - - return decision; - }; - - return edge; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/retrieve_anonymized_alerts_or_generate/get_retrieve_or_generate/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/retrieve_anonymized_alerts_or_generate/get_retrieve_or_generate/index.ts deleted file mode 100644 index 050ca17484185..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/retrieve_anonymized_alerts_or_generate/get_retrieve_or_generate/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Document } from '@langchain/core/documents'; - -export const getRetrieveOrGenerate = ( - anonymizedAlerts: Document[] -): 'retrieve_anonymized_alerts' | 'generate' => - anonymizedAlerts.length === 0 ? 'retrieve_anonymized_alerts' : 'generate'; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/retrieve_anonymized_alerts_or_generate/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/retrieve_anonymized_alerts_or_generate/index.ts deleted file mode 100644 index ad0512497d07d..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/edges/retrieve_anonymized_alerts_or_generate/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Logger } from '@kbn/core/server'; - -import { getRetrieveOrGenerate } from './get_retrieve_or_generate'; -import type { GraphState } from '../../types'; - -export const getRetrieveAnonymizedAlertsOrGenerateEdge = (logger?: Logger) => { - const edge = (state: GraphState): 'retrieve_anonymized_alerts' | 'generate' => { - logger?.debug(() => '---RETRIEVE ANONYMIZED ALERTS OR GENERATE---'); - const { anonymizedAlerts } = state; - - const decision = getRetrieveOrGenerate(anonymizedAlerts); - - logger?.debug( - () => - `retrieveAnonymizedAlertsOrGenerateEdge evaluated the following (derived) state:\n${JSON.stringify( - { - anonymizedAlerts: anonymizedAlerts.length, - }, - null, - 2 - )} - \n---RETRIEVE ANONYMIZED ALERTS OR GENERATE: ${decision}---` - ); - - return decision; - }; - - return edge; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/helpers/get_max_hallucination_failures_reached/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/helpers/get_max_hallucination_failures_reached/index.ts deleted file mode 100644 index 07985381afa73..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/helpers/get_max_hallucination_failures_reached/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const getMaxHallucinationFailuresReached = ({ - hallucinationFailures, - maxHallucinationFailures, -}: { - hallucinationFailures: number; - maxHallucinationFailures: number; -}): boolean => hallucinationFailures >= maxHallucinationFailures; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/helpers/get_max_retries_reached/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/helpers/get_max_retries_reached/index.ts deleted file mode 100644 index c1e36917b45cf..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/helpers/get_max_retries_reached/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const getMaxRetriesReached = ({ - generationAttempts, - maxGenerationAttempts, -}: { - generationAttempts: number; - maxGenerationAttempts: number; -}): boolean => generationAttempts >= maxGenerationAttempts; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/index.ts deleted file mode 100644 index b2c90636ef523..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/index.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ElasticsearchClient, Logger } from '@kbn/core/server'; -import { Replacements } from '@kbn/elastic-assistant-common'; -import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen'; -import type { ActionsClientLlm } from '@kbn/langchain/server'; -import type { CompiledStateGraph } from '@langchain/langgraph'; -import { END, START, StateGraph } from '@langchain/langgraph'; - -import { NodeType } from './constants'; -import { getGenerateOrEndEdge } from './edges/generate_or_end'; -import { getGenerateOrRefineOrEndEdge } from './edges/generate_or_refine_or_end'; -import { getRefineOrEndEdge } from './edges/refine_or_end'; -import { getRetrieveAnonymizedAlertsOrGenerateEdge } from './edges/retrieve_anonymized_alerts_or_generate'; -import { getDefaultGraphState } from './state'; -import { getGenerateNode } from './nodes/generate'; -import { getRefineNode } from './nodes/refine'; -import { getRetrieveAnonymizedAlertsNode } from './nodes/retriever'; -import type { GraphState } from './types'; - -export interface GetDefaultAttackDiscoveryGraphParams { - alertsIndexPattern?: string; - anonymizationFields: AnonymizationFieldResponse[]; - esClient: ElasticsearchClient; - llm: ActionsClientLlm; - logger?: Logger; - onNewReplacements?: (replacements: Replacements) => void; - replacements?: Replacements; - size: number; -} - -export type DefaultAttackDiscoveryGraph = ReturnType; - -/** - * This function returns a compiled state graph that represents the default - * Attack discovery graph. - * - * Refer to the following diagram for this graph: - * x-pack/plugins/elastic_assistant/docs/img/default_attack_discovery_graph.png - */ -export const getDefaultAttackDiscoveryGraph = ({ - alertsIndexPattern, - anonymizationFields, - esClient, - llm, - logger, - onNewReplacements, - replacements, - size, -}: GetDefaultAttackDiscoveryGraphParams): CompiledStateGraph< - GraphState, - Partial, - 'generate' | 'refine' | 'retrieve_anonymized_alerts' | '__start__' -> => { - try { - const graphState = getDefaultGraphState(); - - // get nodes: - const retrieveAnonymizedAlertsNode = getRetrieveAnonymizedAlertsNode({ - alertsIndexPattern, - anonymizationFields, - esClient, - logger, - onNewReplacements, - replacements, - size, - }); - - const generateNode = getGenerateNode({ - llm, - logger, - }); - - const refineNode = getRefineNode({ - llm, - logger, - }); - - // get edges: - const generateOrEndEdge = getGenerateOrEndEdge(logger); - - const generatOrRefineOrEndEdge = getGenerateOrRefineOrEndEdge(logger); - - const refineOrEndEdge = getRefineOrEndEdge(logger); - - const retrieveAnonymizedAlertsOrGenerateEdge = - getRetrieveAnonymizedAlertsOrGenerateEdge(logger); - - // create the graph: - const graph = new StateGraph({ channels: graphState }) - .addNode(NodeType.RETRIEVE_ANONYMIZED_ALERTS_NODE, retrieveAnonymizedAlertsNode) - .addNode(NodeType.GENERATE_NODE, generateNode) - .addNode(NodeType.REFINE_NODE, refineNode) - .addConditionalEdges(START, retrieveAnonymizedAlertsOrGenerateEdge, { - generate: NodeType.GENERATE_NODE, - retrieve_anonymized_alerts: NodeType.RETRIEVE_ANONYMIZED_ALERTS_NODE, - }) - .addConditionalEdges(NodeType.RETRIEVE_ANONYMIZED_ALERTS_NODE, generateOrEndEdge, { - end: END, - generate: NodeType.GENERATE_NODE, - }) - .addConditionalEdges(NodeType.GENERATE_NODE, generatOrRefineOrEndEdge, { - end: END, - generate: NodeType.GENERATE_NODE, - refine: NodeType.REFINE_NODE, - }) - .addConditionalEdges(NodeType.REFINE_NODE, refineOrEndEdge, { - end: END, - refine: NodeType.REFINE_NODE, - }); - - // compile the graph: - return graph.compile(); - } catch (e) { - throw new Error(`Unable to compile AttackDiscoveryGraph\n${e}`); - } -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/mock/mock_empty_open_and_acknowledged_alerts_qery_results.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/mock/mock_empty_open_and_acknowledged_alerts_qery_results.ts deleted file mode 100644 index ed5549acc586a..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/mock/mock_empty_open_and_acknowledged_alerts_qery_results.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const mockEmptyOpenAndAcknowledgedAlertsQueryResults = { - took: 0, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 0, - relation: 'eq', - }, - max_score: null, - hits: [], - }, -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/mock/mock_open_and_acknowledged_alerts_query_results.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/mock/mock_open_and_acknowledged_alerts_query_results.ts deleted file mode 100644 index 3f22f787f54f8..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/mock/mock_open_and_acknowledged_alerts_query_results.ts +++ /dev/null @@ -1,1396 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const mockOpenAndAcknowledgedAlertsQueryResults = { - took: 13, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 31, - relation: 'eq', - }, - max_score: null, - hits: [ - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: 'b6e883c29b32571aaa667fa13e65bbb4f95172a2b84bdfb85d6f16c72b2d2560', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'file.path': ['/Users/james/unix1'], - 'process.hash.md5': ['85caafe3d324e3287b85348fa2fae492'], - 'event.category': ['malware', 'intrusion_detection', 'process'], - 'host.risk.calculated_score_norm': [73.02488], - 'process.parent.command_line': [ - '/Users/james/unix1 /Users/james/library/Keychains/login.keychain-db TempTemp1234!!', - ], - 'process.parent.name': ['unix1'], - 'user.name': ['james'], - 'user.risk.calculated_level': ['Moderate'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - '0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231', - ], - 'process.code_signature.signing_id': ['nans-55554944e5f232edcf023cf68e8e5dac81584f78'], - 'process.pid': [1227], - 'process.code_signature.exists': [true], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': [ - 'code failed to satisfy specified code requirement(s)', - ], - 'event.module': ['endpoint'], - 'process.code_signature.subject_name': [''], - 'host.os.version': ['13.4'], - 'file.hash.sha256': ['0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [66.72442], - 'host.os.name': ['macOS'], - 'kibana.alert.rule.name': ['Malware Detection Alert'], - 'host.name': ['SRVMAC08'], - 'process.executable': ['/Users/james/unix1'], - 'event.outcome': ['success'], - 'process.code_signature.trusted': [false], - 'process.parent.code_signature.subject_name': [''], - 'process.parent.executable': ['/Users/james/unix1'], - 'kibana.alert.workflow_status': ['open'], - 'file.name': ['unix1'], - 'process.args': [ - '/Users/james/unix1', - '/Users/james/library/Keychains/login.keychain-db', - 'TempTemp1234!!', - ], - 'process.code_signature.status': ['code failed to satisfy specified code requirement(s)'], - message: ['Malware Detection Alert'], - 'process.parent.args_count': [3], - 'process.name': ['unix1'], - 'process.parent.args': [ - '/Users/james/unix1', - '/Users/james/library/Keychains/login.keychain-db', - 'TempTemp1234!!', - ], - '@timestamp': ['2024-05-07T12:48:45.032Z'], - 'process.parent.code_signature.trusted': [false], - 'process.command_line': [ - '/Users/james/unix1 /Users/james/library/Keychains/login.keychain-db TempTemp1234!!', - ], - 'host.risk.calculated_level': ['High'], - _id: ['b6e883c29b32571aaa667fa13e65bbb4f95172a2b84bdfb85d6f16c72b2d2560'], - 'process.hash.sha1': ['4ca549355736e4af6434efc4ec9a044ceb2ae3c3'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-06-19T00:28:39.368Z'], - }, - sort: [99, 1715086125032], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: '0215a6c5cc9499dd0290cd69a4947efb87d3ddd8b6385a766d122c2475be7367', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'file.path': ['/Users/james/unix1'], - 'process.hash.md5': ['e62bdd3eaf2be436fca2e67b7eede603'], - 'event.category': ['malware', 'intrusion_detection', 'file'], - 'host.risk.calculated_score_norm': [73.02488], - 'process.parent.command_line': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'process.parent.name': ['My Go Application.app'], - 'user.name': ['james'], - 'user.risk.calculated_level': ['Moderate'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - '2c63ba2b1a5131b80e567b7a1a93997a2de07ea20d0a8f5149701c67b832c097', - ], - 'process.code_signature.signing_id': ['a.out'], - 'process.pid': [1220], - 'process.code_signature.exists': [true], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': [ - 'code failed to satisfy specified code requirement(s)', - ], - 'event.module': ['endpoint'], - 'process.code_signature.subject_name': [''], - 'host.os.version': ['13.4'], - 'file.hash.sha256': ['0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [66.72442], - 'host.os.name': ['macOS'], - 'kibana.alert.rule.name': ['Malware Detection Alert'], - 'host.name': ['SRVMAC08'], - 'process.executable': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'event.outcome': ['success'], - 'process.code_signature.trusted': [false], - 'process.parent.code_signature.subject_name': [''], - 'process.parent.executable': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'kibana.alert.workflow_status': ['open'], - 'file.name': ['unix1'], - 'process.args': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'process.code_signature.status': ['code failed to satisfy specified code requirement(s)'], - message: ['Malware Detection Alert'], - 'process.parent.args_count': [1], - 'process.name': ['My Go Application.app'], - 'process.parent.args': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - '@timestamp': ['2024-05-07T12:48:45.030Z'], - 'process.parent.code_signature.trusted': [false], - 'process.command_line': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'host.risk.calculated_level': ['High'], - _id: ['0215a6c5cc9499dd0290cd69a4947efb87d3ddd8b6385a766d122c2475be7367'], - 'process.hash.sha1': ['58a3bddbc7c45193ecbefa22ad0496b60a29dff2'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-06-19T00:28:38.061Z'], - }, - sort: [99, 1715086125030], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: '600eb9eca925f4c5b544b4e9d3cf95d83b7829f8f74c5bd746369cb4c2968b9a', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'file.path': ['/Users/james/unix1'], - 'process.hash.md5': ['85caafe3d324e3287b85348fa2fae492'], - 'event.category': ['malware', 'intrusion_detection', 'process'], - 'host.risk.calculated_score_norm': [73.02488], - 'process.parent.command_line': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'process.parent.name': ['My Go Application.app'], - 'user.name': ['james'], - 'user.risk.calculated_level': ['Moderate'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - '0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231', - ], - 'process.code_signature.signing_id': ['nans-55554944e5f232edcf023cf68e8e5dac81584f78'], - 'process.pid': [1220], - 'process.code_signature.exists': [true], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': [ - 'code failed to satisfy specified code requirement(s)', - ], - 'event.module': ['endpoint'], - 'process.code_signature.subject_name': [''], - 'host.os.version': ['13.4'], - 'file.hash.sha256': ['0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [66.72442], - 'host.os.name': ['macOS'], - 'kibana.alert.rule.name': ['Malware Detection Alert'], - 'host.name': ['SRVMAC08'], - 'process.executable': ['/Users/james/unix1'], - 'event.outcome': ['success'], - 'process.code_signature.trusted': [false], - 'process.parent.code_signature.subject_name': [''], - 'process.parent.executable': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'kibana.alert.workflow_status': ['open'], - 'file.name': ['unix1'], - 'process.args': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'process.code_signature.status': ['code failed to satisfy specified code requirement(s)'], - message: ['Malware Detection Alert'], - 'process.parent.args_count': [1], - 'process.name': ['unix1'], - 'process.parent.args': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - '@timestamp': ['2024-05-07T12:48:45.029Z'], - 'process.parent.code_signature.trusted': [false], - 'process.command_line': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'host.risk.calculated_level': ['High'], - _id: ['600eb9eca925f4c5b544b4e9d3cf95d83b7829f8f74c5bd746369cb4c2968b9a'], - 'process.hash.sha1': ['4ca549355736e4af6434efc4ec9a044ceb2ae3c3'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-06-19T00:28:37.881Z'], - }, - sort: [99, 1715086125029], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: 'e1f4a4ed70190eb4bd256c813029a6a9101575887cdbfa226ac330fbd3063f0c', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'file.path': ['/Users/james/unix1'], - 'process.hash.md5': ['3f19892ab44eb9bc7bc03f438944301e'], - 'event.category': ['malware', 'intrusion_detection', 'file'], - 'host.risk.calculated_score_norm': [73.02488], - 'process.parent.command_line': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'process.parent.name': ['My Go Application.app'], - 'user.name': ['james'], - 'user.risk.calculated_level': ['Moderate'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - 'f80234ff6fed2c62d23f37443f2412fbe806711b6add2ac126e03e282082c8f5', - ], - 'process.code_signature.signing_id': ['com.apple.chmod'], - 'process.pid': [1219], - 'process.code_signature.exists': [true], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': [ - 'code failed to satisfy specified code requirement(s)', - ], - 'event.module': ['endpoint'], - 'process.code_signature.subject_name': ['Software Signing'], - 'host.os.version': ['13.4'], - 'file.hash.sha256': ['0b18d6880dc9670ab2b955914598c96fc3d0097dc40ea61157b8c79e75edf231'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [66.72442], - 'host.os.name': ['macOS'], - 'kibana.alert.rule.name': ['Malware Detection Alert'], - 'host.name': ['SRVMAC08'], - 'process.executable': ['/bin/chmod'], - 'event.outcome': ['success'], - 'process.code_signature.trusted': [true], - 'process.parent.code_signature.subject_name': [''], - 'process.parent.executable': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'kibana.alert.workflow_status': ['open'], - 'file.name': ['unix1'], - 'process.args': ['chmod', '777', '/Users/james/unix1'], - 'process.code_signature.status': ['No error.'], - message: ['Malware Detection Alert'], - 'process.parent.args_count': [1], - 'process.name': ['chmod'], - 'process.parent.args': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - '@timestamp': ['2024-05-07T12:48:45.028Z'], - 'process.parent.code_signature.trusted': [false], - 'process.command_line': ['chmod 777 /Users/james/unix1'], - 'host.risk.calculated_level': ['High'], - _id: ['e1f4a4ed70190eb4bd256c813029a6a9101575887cdbfa226ac330fbd3063f0c'], - 'process.hash.sha1': ['217490d4f51717aa3b301abec96be08602370d2d'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-06-19T00:28:37.869Z'], - }, - sort: [99, 1715086125028], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: '2a7a4809ca625dfe22ccd35fbef7a7ba8ed07f109e5cbd17250755cfb0bc615f', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'process.hash.md5': ['643dddff1a57cbf70594854b44eb1a1d'], - 'event.category': ['malware', 'intrusion_detection'], - 'host.risk.calculated_score_norm': [73.02488], - 'rule.reference': [ - 'https://github.com/EmpireProject/EmPyre/blob/master/lib/modules/collection/osx/prompt.py', - 'https://ss64.com/osx/osascript.html', - ], - 'process.parent.name': ['My Go Application.app'], - 'user.risk.calculated_level': ['Moderate'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - 'bab17feba710b469e5d96820f0cb7ed511d983e5817f374ec3cb46462ac5b794', - ], - 'process.pid': [1206], - 'process.code_signature.exists': [true], - 'process.code_signature.subject_name': ['Software Signing'], - 'host.os.version': ['13.4'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [66.72442], - 'host.os.name': ['macOS'], - 'kibana.alert.rule.name': [ - 'Malicious Behavior Detection Alert: Potential Credentials Phishing via OSASCRIPT', - ], - 'host.name': ['SRVMAC08'], - 'event.outcome': ['success'], - 'process.code_signature.trusted': [true], - 'group.name': ['staff'], - 'kibana.alert.workflow_status': ['open'], - 'rule.name': ['Potential Credentials Phishing via OSASCRIPT'], - 'threat.tactic.id': ['TA0006'], - 'threat.tactic.name': ['Credential Access'], - 'threat.technique.id': ['T1056'], - 'process.parent.args_count': [0], - 'threat.technique.subtechnique.reference': [ - 'https://attack.mitre.org/techniques/T1056/002/', - ], - 'process.name': ['osascript'], - 'threat.technique.subtechnique.name': ['GUI Input Capture'], - 'process.parent.code_signature.trusted': [false], - _id: ['2a7a4809ca625dfe22ccd35fbef7a7ba8ed07f109e5cbd17250755cfb0bc615f'], - 'threat.technique.name': ['Input Capture'], - 'group.id': ['20'], - 'threat.tactic.reference': ['https://attack.mitre.org/tactics/TA0006/'], - 'user.name': ['james'], - 'threat.framework': ['MITRE ATT&CK'], - 'process.code_signature.signing_id': ['com.apple.osascript'], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': [ - 'code failed to satisfy specified code requirement(s)', - ], - 'event.module': ['endpoint'], - 'process.executable': ['/usr/bin/osascript'], - 'process.parent.executable': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'process.args': [ - 'osascript', - '-e', - 'display dialog "MacOS wants to access System Preferences\n\t\t\nPlease enter your password." with title "System Preferences" with icon file "System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:ToolbarAdvanced.icns" default answer "" giving up after 30 with hidden answer ¬', - ], - 'process.code_signature.status': ['No error.'], - message: [ - 'Malicious Behavior Detection Alert: Potential Credentials Phishing via OSASCRIPT', - ], - '@timestamp': ['2024-05-07T12:48:45.027Z'], - 'threat.technique.subtechnique.id': ['T1056.002'], - 'threat.technique.reference': ['https://attack.mitre.org/techniques/T1056/'], - 'process.command_line': [ - 'osascript -e display dialog "MacOS wants to access System Preferences\n\t\t\nPlease enter your password." with title "System Preferences" with icon file "System:Library:CoreServices:CoreTypes.bundle:Contents:Resources:ToolbarAdvanced.icns" default answer "" giving up after 30 with hidden answer ¬', - ], - 'host.risk.calculated_level': ['High'], - 'process.hash.sha1': ['0568baae15c752208ae56d8f9c737976d6de2e3a'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-06-19T00:28:09.909Z'], - }, - sort: [99, 1715086125027], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: '2a9f7602de8656d30dda0ddcf79e78037ac2929780e13d5b2047b3bedc40bb69', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'file.path': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'process.hash.md5': ['e62bdd3eaf2be436fca2e67b7eede603'], - 'event.category': ['malware', 'intrusion_detection', 'process'], - 'host.risk.calculated_score_norm': [73.02488], - 'process.parent.command_line': ['/sbin/launchd'], - 'process.parent.name': ['launchd'], - 'user.name': ['root'], - 'user.risk.calculated_level': ['Moderate'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - '2c63ba2b1a5131b80e567b7a1a93997a2de07ea20d0a8f5149701c67b832c097', - ], - 'process.code_signature.signing_id': ['a.out'], - 'process.pid': [1200], - 'process.code_signature.exists': [true], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': ['No error.'], - 'event.module': ['endpoint'], - 'process.code_signature.subject_name': [''], - 'host.os.version': ['13.4'], - 'file.hash.sha256': ['2c63ba2b1a5131b80e567b7a1a93997a2de07ea20d0a8f5149701c67b832c097'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [66.491455], - 'host.os.name': ['macOS'], - 'kibana.alert.rule.name': ['Malware Detection Alert'], - 'host.name': ['SRVMAC08'], - 'process.executable': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/6D63F08A-011C-4511-8556-EAEF9AFD6340/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'event.outcome': ['success'], - 'process.code_signature.trusted': [false], - 'process.parent.code_signature.subject_name': ['Software Signing'], - 'process.parent.executable': ['/sbin/launchd'], - 'kibana.alert.workflow_status': ['open'], - 'file.name': ['My Go Application.app'], - 'process.args': ['xpcproxy', 'application.Appify by Machine Box.My Go Application.20.23'], - 'process.code_signature.status': ['code failed to satisfy specified code requirement(s)'], - message: ['Malware Detection Alert'], - 'process.parent.args_count': [1], - 'process.name': ['My Go Application.app'], - 'process.parent.args': ['/sbin/launchd'], - '@timestamp': ['2024-05-07T12:48:45.023Z'], - 'process.parent.code_signature.trusted': [true], - 'process.command_line': [ - 'xpcproxy application.Appify by Machine Box.My Go Application.20.23', - ], - 'host.risk.calculated_level': ['High'], - _id: ['2a9f7602de8656d30dda0ddcf79e78037ac2929780e13d5b2047b3bedc40bb69'], - 'process.hash.sha1': ['58a3bddbc7c45193ecbefa22ad0496b60a29dff2'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-06-19T00:28:06.888Z'], - }, - sort: [99, 1715086125023], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: '4615c3a90e8057ae5cc9b358bbbf4298e346277a2f068dda052b0b43ef6d5bbd', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'file.path': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/3C4D44B9-4838-4613-BACC-BD00A9CE4025/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'process.hash.md5': ['e62bdd3eaf2be436fca2e67b7eede603'], - 'event.category': ['malware', 'intrusion_detection', 'process'], - 'host.risk.calculated_score_norm': [73.02488], - 'process.parent.command_line': ['/sbin/launchd'], - 'process.parent.name': ['launchd'], - 'user.name': ['root'], - 'user.risk.calculated_level': ['Moderate'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - '2c63ba2b1a5131b80e567b7a1a93997a2de07ea20d0a8f5149701c67b832c097', - ], - 'process.code_signature.signing_id': ['a.out'], - 'process.pid': [1169], - 'process.code_signature.exists': [true], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': ['No error.'], - 'event.module': ['endpoint'], - 'process.code_signature.subject_name': [''], - 'host.os.version': ['13.4'], - 'file.hash.sha256': ['2c63ba2b1a5131b80e567b7a1a93997a2de07ea20d0a8f5149701c67b832c097'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [66.491455], - 'host.os.name': ['macOS'], - 'kibana.alert.rule.name': ['Malware Detection Alert'], - 'host.name': ['SRVMAC08'], - 'process.executable': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/3C4D44B9-4838-4613-BACC-BD00A9CE4025/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'event.outcome': ['success'], - 'process.code_signature.trusted': [false], - 'process.parent.code_signature.subject_name': ['Software Signing'], - 'process.parent.executable': ['/sbin/launchd'], - 'kibana.alert.workflow_status': ['open'], - 'file.name': ['My Go Application.app'], - 'process.args': ['xpcproxy', 'application.Appify by Machine Box.My Go Application.20.23'], - 'process.code_signature.status': ['code failed to satisfy specified code requirement(s)'], - message: ['Malware Detection Alert'], - 'process.parent.args_count': [1], - 'process.name': ['My Go Application.app'], - 'process.parent.args': ['/sbin/launchd'], - '@timestamp': ['2024-05-07T12:48:45.022Z'], - 'process.parent.code_signature.trusted': [true], - 'process.command_line': [ - 'xpcproxy application.Appify by Machine Box.My Go Application.20.23', - ], - 'host.risk.calculated_level': ['High'], - _id: ['4615c3a90e8057ae5cc9b358bbbf4298e346277a2f068dda052b0b43ef6d5bbd'], - 'process.hash.sha1': ['58a3bddbc7c45193ecbefa22ad0496b60a29dff2'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-06-19T00:27:47.362Z'], - }, - sort: [99, 1715086125022], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: '449322a72d3f19efbdf983935a1bdd21ebd6b9c761ce31e8b252003017d7e5db', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'file.path': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/37D933EC-334D-410A-A741-0F730D6AE3FD/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'process.hash.md5': ['e62bdd3eaf2be436fca2e67b7eede603'], - 'event.category': ['malware', 'intrusion_detection', 'process'], - 'host.risk.calculated_score_norm': [73.02488], - 'process.parent.command_line': ['/sbin/launchd'], - 'process.parent.name': ['launchd'], - 'user.name': ['root'], - 'user.risk.calculated_level': ['Moderate'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - '2c63ba2b1a5131b80e567b7a1a93997a2de07ea20d0a8f5149701c67b832c097', - ], - 'process.code_signature.signing_id': ['a.out'], - 'process.pid': [1123], - 'process.code_signature.exists': [true], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': ['No error.'], - 'event.module': ['endpoint'], - 'process.code_signature.subject_name': [''], - 'host.os.version': ['13.4'], - 'file.hash.sha256': ['2c63ba2b1a5131b80e567b7a1a93997a2de07ea20d0a8f5149701c67b832c097'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [66.491455], - 'host.os.name': ['macOS'], - 'kibana.alert.rule.name': ['Malware Detection Alert'], - 'host.name': ['SRVMAC08'], - 'process.executable': [ - '/private/var/folders/_b/rmcpc65j6nv11ygrs50ctcjr0000gn/T/AppTranslocation/37D933EC-334D-410A-A741-0F730D6AE3FD/d/Setup.app/Contents/MacOS/My Go Application.app', - ], - 'event.outcome': ['success'], - 'process.code_signature.trusted': [false], - 'process.parent.code_signature.subject_name': ['Software Signing'], - 'process.parent.executable': ['/sbin/launchd'], - 'kibana.alert.workflow_status': ['open'], - 'file.name': ['My Go Application.app'], - 'process.args': ['xpcproxy', 'application.Appify by Machine Box.My Go Application.20.23'], - 'process.code_signature.status': ['code failed to satisfy specified code requirement(s)'], - message: ['Malware Detection Alert'], - 'process.parent.args_count': [1], - 'process.name': ['My Go Application.app'], - 'process.parent.args': ['/sbin/launchd'], - '@timestamp': ['2024-05-07T12:48:45.020Z'], - 'process.parent.code_signature.trusted': [true], - 'process.command_line': [ - 'xpcproxy application.Appify by Machine Box.My Go Application.20.23', - ], - 'host.risk.calculated_level': ['High'], - _id: ['449322a72d3f19efbdf983935a1bdd21ebd6b9c761ce31e8b252003017d7e5db'], - 'process.hash.sha1': ['58a3bddbc7c45193ecbefa22ad0496b60a29dff2'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-06-19T00:25:24.716Z'], - }, - sort: [99, 1715086125020], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: 'f465ca9fbfc8bc3b1871e965c9e111cac76ff3f4076fed6bc9da88d49fb43014', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'process.hash.md5': ['8cc83221870dd07144e63df594c391d9'], - 'event.category': ['malware', 'intrusion_detection'], - 'host.risk.calculated_score_norm': [75.62723], - 'process.parent.command_line': [ - '"C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe" ', - ], - 'process.parent.name': [ - 'd55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'user.name': ['Administrator'], - 'user.risk.calculated_level': ['High'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - '33bc14d231a4afaa18f06513766d5f69d8b88f1e697cd127d24fb4b72ad44c7a', - ], - 'process.pid': [8708], - 'process.code_signature.exists': [true], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': ['errorExpired'], - 'process.pe.original_file_name': ['MsMpEng.exe'], - 'event.module': ['endpoint'], - 'process.code_signature.subject_name': ['Microsoft Corporation'], - 'host.os.version': ['21H2 (10.0.20348.1366)'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [82.16188], - 'host.os.name': ['Windows'], - 'kibana.alert.rule.name': ['Memory Threat Detection Alert: Shellcode Injection'], - 'host.name': ['SRVWIN02'], - 'user.domain': ['OMM-WIN-DETECT'], - 'process.executable': ['C:\\Windows\\MsMpEng.exe'], - 'process.code_signature.trusted': [true], - 'process.Ext.token.integrity_level_name': ['high'], - 'process.parent.code_signature.subject_name': ['PB03 TRANSPORT LTD.'], - 'process.parent.executable': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'kibana.alert.workflow_status': ['open'], - 'process.args': ['C:\\Windows\\MsMpEng.exe'], - 'process.code_signature.status': ['trusted'], - message: ['Memory Threat Detection Alert: Shellcode Injection'], - 'process.parent.args_count': [1], - 'process.name': ['MsMpEng.exe'], - 'process.parent.args': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - '@timestamp': ['2024-05-07T12:48:45.017Z'], - 'process.parent.code_signature.trusted': [false], - 'process.command_line': ['"C:\\Windows\\MsMpEng.exe"'], - 'host.risk.calculated_level': ['High'], - _id: ['f465ca9fbfc8bc3b1871e965c9e111cac76ff3f4076fed6bc9da88d49fb43014'], - 'process.hash.sha1': ['3d409b39b8502fcd23335a878f2cbdaf6d721995'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-01-20T23:38:22.051Z'], - }, - sort: [99, 1715086125017], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: 'aa283e6a13be77b533eceffb09e48254c8f91feeccc39f7eed80fd3881d053f4', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'file.path': ['C:\\Windows\\mpsvc.dll'], - 'process.hash.md5': ['8cc83221870dd07144e63df594c391d9'], - 'event.category': ['malware', 'intrusion_detection', 'library'], - 'host.risk.calculated_score_norm': [75.62723], - 'process.parent.command_line': [ - '"C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe" ', - ], - 'process.parent.name': [ - 'd55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'user.name': ['Administrator'], - 'user.risk.calculated_level': ['High'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - '33bc14d231a4afaa18f06513766d5f69d8b88f1e697cd127d24fb4b72ad44c7a', - ], - 'process.pid': [8708], - 'process.code_signature.exists': [true], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': ['errorExpired'], - 'process.pe.original_file_name': ['MsMpEng.exe'], - 'event.module': ['endpoint'], - 'process.code_signature.subject_name': ['Microsoft Corporation'], - 'host.os.version': ['21H2 (10.0.20348.1366)'], - 'file.hash.sha256': ['8dd620d9aeb35960bb766458c8890ede987c33d239cf730f93fe49d90ae759dd'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [82.16188], - 'host.os.name': ['Windows'], - 'kibana.alert.rule.name': ['Malware Detection Alert'], - 'host.name': ['SRVWIN02'], - 'user.domain': ['OMM-WIN-DETECT'], - 'process.executable': ['C:\\Windows\\MsMpEng.exe'], - 'event.outcome': ['success'], - 'process.code_signature.trusted': [true], - 'process.Ext.token.integrity_level_name': ['high'], - 'process.parent.code_signature.subject_name': ['PB03 TRANSPORT LTD.'], - 'process.parent.executable': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'kibana.alert.workflow_status': ['open'], - 'file.name': ['mpsvc.dll'], - 'process.args': ['C:\\Windows\\MsMpEng.exe'], - 'process.code_signature.status': ['trusted'], - message: ['Malware Detection Alert'], - 'process.parent.args_count': [1], - 'process.name': ['MsMpEng.exe'], - 'process.parent.args': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - '@timestamp': ['2024-05-07T12:48:45.008Z'], - 'process.parent.code_signature.trusted': [false], - 'process.command_line': ['"C:\\Windows\\MsMpEng.exe"'], - 'host.risk.calculated_level': ['High'], - _id: ['aa283e6a13be77b533eceffb09e48254c8f91feeccc39f7eed80fd3881d053f4'], - 'process.hash.sha1': ['3d409b39b8502fcd23335a878f2cbdaf6d721995'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-01-20T23:38:18.093Z'], - }, - sort: [99, 1715086125008], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: 'dd9e4ea23961ccfdb7a9c760ee6bedd19a013beac3b0d38227e7ae77ba4ce515', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'file.path': ['C:\\Windows\\mpsvc.dll'], - 'process.hash.md5': ['561cffbaba71a6e8cc1cdceda990ead4'], - 'event.category': ['malware', 'intrusion_detection', 'file'], - 'host.risk.calculated_score_norm': [75.62723], - 'process.parent.command_line': ['C:\\Windows\\Explorer.EXE'], - 'process.parent.name': ['explorer.exe'], - 'user.name': ['Administrator'], - 'user.risk.calculated_level': ['High'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - 'd55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e', - ], - 'process.pid': [1008], - 'process.code_signature.exists': [true], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': ['trusted'], - 'event.module': ['endpoint'], - 'process.code_signature.subject_name': ['PB03 TRANSPORT LTD.'], - 'host.os.version': ['21H2 (10.0.20348.1366)'], - 'file.hash.sha256': ['8dd620d9aeb35960bb766458c8890ede987c33d239cf730f93fe49d90ae759dd'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [82.16188], - 'host.os.name': ['Windows'], - 'kibana.alert.rule.name': ['Malware Detection Alert'], - 'host.name': ['SRVWIN02'], - 'user.domain': ['OMM-WIN-DETECT'], - 'process.executable': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'event.outcome': ['success'], - 'process.code_signature.trusted': [false], - 'process.Ext.token.integrity_level_name': ['high'], - 'process.parent.code_signature.subject_name': ['Microsoft Windows'], - 'process.parent.executable': ['C:\\Windows\\explorer.exe'], - 'kibana.alert.workflow_status': ['open'], - 'file.name': ['mpsvc.dll'], - 'process.args': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'process.code_signature.status': ['errorExpired'], - message: ['Malware Detection Alert'], - 'process.parent.args_count': [1], - 'process.name': ['d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe'], - 'process.parent.args': ['C:\\Windows\\Explorer.EXE'], - '@timestamp': ['2024-05-07T12:48:45.007Z'], - 'process.parent.code_signature.trusted': [true], - 'process.command_line': [ - '"C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe" ', - ], - 'host.risk.calculated_level': ['High'], - _id: ['dd9e4ea23961ccfdb7a9c760ee6bedd19a013beac3b0d38227e7ae77ba4ce515'], - 'process.hash.sha1': ['5162f14d75e96edb914d1756349d6e11583db0b0'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-01-20T23:38:17.887Z'], - }, - sort: [99, 1715086125007], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: 'f30d55e503b1d848b34ee57741b203d8052360dd873ea34802f3fa7a9ef34d0a', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'file.path': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'process.hash.md5': ['561cffbaba71a6e8cc1cdceda990ead4'], - 'event.category': ['malware', 'intrusion_detection', 'process'], - 'host.risk.calculated_score_norm': [75.62723], - 'process.parent.command_line': ['C:\\Windows\\Explorer.EXE'], - 'process.parent.name': ['explorer.exe'], - 'user.name': ['Administrator'], - 'user.risk.calculated_level': ['High'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - 'd55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e', - ], - 'process.pid': [1008], - 'process.code_signature.exists': [true], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': ['trusted'], - 'event.module': ['endpoint'], - 'process.code_signature.subject_name': ['PB03 TRANSPORT LTD.'], - 'host.os.version': ['21H2 (10.0.20348.1366)'], - 'file.hash.sha256': ['d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [82.16188], - 'host.os.name': ['Windows'], - 'kibana.alert.rule.name': ['Malware Detection Alert'], - 'host.name': ['SRVWIN02'], - 'user.domain': ['OMM-WIN-DETECT'], - 'process.executable': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'event.outcome': ['success'], - 'process.code_signature.trusted': [false], - 'process.Ext.token.integrity_level_name': ['high'], - 'process.parent.code_signature.subject_name': ['Microsoft Windows'], - 'process.parent.executable': ['C:\\Windows\\explorer.exe'], - 'kibana.alert.workflow_status': ['open'], - 'file.name': ['d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe'], - 'process.args': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'process.code_signature.status': ['errorExpired'], - message: ['Malware Detection Alert'], - 'process.parent.args_count': [1], - 'process.name': ['d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe'], - 'process.parent.args': ['C:\\Windows\\Explorer.EXE'], - '@timestamp': ['2024-05-07T12:48:45.006Z'], - 'process.parent.code_signature.trusted': [true], - 'process.command_line': [ - '"C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe" ', - ], - 'host.risk.calculated_level': ['High'], - _id: ['f30d55e503b1d848b34ee57741b203d8052360dd873ea34802f3fa7a9ef34d0a'], - 'process.hash.sha1': ['5162f14d75e96edb914d1756349d6e11583db0b0'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-01-20T23:38:17.544Z'], - }, - sort: [99, 1715086125006], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: '6f8cd5e8021dbb64598f2b7ec56bee21fd00d1e62d4e08905f86bf234873ee66', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'file.path': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'process.hash.md5': ['f070b5cf25febb9a88a168efd87c6112'], - 'event.category': ['malware', 'intrusion_detection', 'file'], - 'host.risk.calculated_score_norm': [75.62723], - 'process.parent.command_line': [''], - 'process.parent.name': ['userinit.exe'], - 'user.name': ['Administrator'], - 'user.risk.calculated_level': ['High'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - '567be4d1e15f4ff96d92e7d28e191076f5813f50be96bf4c3916e4ecf53f66cd', - ], - 'process.pid': [6228], - 'process.code_signature.exists': [true], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': ['trusted'], - 'process.pe.original_file_name': ['EXPLORER.EXE'], - 'event.module': ['endpoint'], - 'process.code_signature.subject_name': ['Microsoft Windows'], - 'host.os.version': ['21H2 (10.0.20348.1366)'], - 'file.hash.sha256': ['d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [82.16188], - 'host.os.name': ['Windows'], - 'kibana.alert.rule.name': ['Malware Detection Alert'], - 'host.name': ['SRVWIN02'], - 'user.domain': ['OMM-WIN-DETECT'], - 'process.executable': ['C:\\Windows\\explorer.exe'], - 'event.outcome': ['success'], - 'process.code_signature.trusted': [true], - 'process.Ext.token.integrity_level_name': ['high'], - 'process.parent.code_signature.subject_name': ['Microsoft Windows'], - 'process.parent.executable': ['C:\\Windows\\System32\\userinit.exe'], - 'kibana.alert.workflow_status': ['open'], - 'file.name': ['d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe'], - 'process.args': ['C:\\Windows\\Explorer.EXE'], - 'process.code_signature.status': ['trusted'], - message: ['Malware Detection Alert'], - 'process.name': ['explorer.exe'], - '@timestamp': ['2024-05-07T12:48:45.004Z'], - 'process.parent.code_signature.trusted': [true], - 'process.command_line': ['C:\\Windows\\Explorer.EXE'], - 'host.risk.calculated_level': ['High'], - _id: ['6f8cd5e8021dbb64598f2b7ec56bee21fd00d1e62d4e08905f86bf234873ee66'], - 'process.hash.sha1': ['94518c310478e494082418ed295466f5aea26eea'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-01-20T23:37:18.152Z'], - }, - sort: [99, 1715086125004], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: 'ce110da958fe0cf0c07599a21c68d90a64c93b7607aa27970a614c7f49598316', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'file.path': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e', - ], - 'process.hash.md5': ['f070b5cf25febb9a88a168efd87c6112'], - 'event.category': ['malware', 'intrusion_detection', 'file'], - 'host.risk.calculated_score_norm': [75.62723], - 'process.parent.command_line': [''], - 'process.parent.name': ['userinit.exe'], - 'user.name': ['Administrator'], - 'user.risk.calculated_level': ['High'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - '567be4d1e15f4ff96d92e7d28e191076f5813f50be96bf4c3916e4ecf53f66cd', - ], - 'process.pid': [6228], - 'process.code_signature.exists': [true], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': ['trusted'], - 'process.pe.original_file_name': ['EXPLORER.EXE'], - 'event.module': ['endpoint'], - 'process.code_signature.subject_name': ['Microsoft Windows'], - 'host.os.version': ['21H2 (10.0.20348.1366)'], - 'file.hash.sha256': ['d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [82.16188], - 'host.os.name': ['Windows'], - 'kibana.alert.rule.name': ['Malware Detection Alert'], - 'host.name': ['SRVWIN02'], - 'user.domain': ['OMM-WIN-DETECT'], - 'process.executable': ['C:\\Windows\\explorer.exe'], - 'event.outcome': ['success'], - 'process.code_signature.trusted': [true], - 'process.Ext.token.integrity_level_name': ['high'], - 'process.parent.code_signature.subject_name': ['Microsoft Windows'], - 'process.parent.executable': ['C:\\Windows\\System32\\userinit.exe'], - 'kibana.alert.workflow_status': ['open'], - 'file.name': ['d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e'], - 'process.args': ['C:\\Windows\\Explorer.EXE'], - 'process.code_signature.status': ['trusted'], - message: ['Malware Detection Alert'], - 'process.name': ['explorer.exe'], - '@timestamp': ['2024-05-07T12:48:45.001Z'], - 'process.parent.code_signature.trusted': [true], - 'process.command_line': ['C:\\Windows\\Explorer.EXE'], - 'host.risk.calculated_level': ['High'], - _id: ['ce110da958fe0cf0c07599a21c68d90a64c93b7607aa27970a614c7f49598316'], - 'process.hash.sha1': ['94518c310478e494082418ed295466f5aea26eea'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-01-20T23:36:43.813Z'], - }, - sort: [99, 1715086125001], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: '0866787b0027b4d908767ac16e35a1da00970c83632ba85be65f2ad371132b4f', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'process.hash.md5': ['8cc83221870dd07144e63df594c391d9'], - 'event.category': ['malware', 'intrusion_detection', 'process', 'file'], - 'host.risk.calculated_score_norm': [75.62723], - 'process.parent.command_line': [ - '"C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe" ', - ], - 'process.parent.name': [ - 'd55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'user.risk.calculated_level': ['High'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - '33bc14d231a4afaa18f06513766d5f69d8b88f1e697cd127d24fb4b72ad44c7a', - ], - 'process.pid': [8708], - 'process.code_signature.exists': [true], - 'process.code_signature.subject_name': ['Microsoft Corporation'], - 'host.os.version': ['21H2 (10.0.20348.1366)'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [82.16188], - 'host.os.name': ['Windows'], - 'kibana.alert.rule.name': ['Ransomware Detection Alert'], - 'host.name': ['SRVWIN02'], - 'Ransomware.files.data': [ - '2D002D002D003D003D003D0020005700', - '2D002D002D003D003D003D0020005700', - '2D002D002D003D003D003D0020005700', - ], - 'process.code_signature.trusted': [true], - 'Ransomware.files.metrics': ['CANARY_ACTIVITY'], - 'kibana.alert.workflow_status': ['open'], - 'process.parent.args_count': [1], - 'process.name': ['MsMpEng.exe'], - 'Ransomware.files.score': [0, 0, 0], - 'process.parent.code_signature.trusted': [false], - _id: ['0866787b0027b4d908767ac16e35a1da00970c83632ba85be65f2ad371132b4f'], - 'Ransomware.version': ['1.6.0'], - 'user.name': ['Administrator'], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': ['errorExpired'], - 'Ransomware.files.operation': ['creation', 'creation', 'creation'], - 'process.pe.original_file_name': ['MsMpEng.exe'], - 'event.module': ['endpoint'], - 'user.domain': ['OMM-WIN-DETECT'], - 'process.executable': ['C:\\Windows\\MsMpEng.exe'], - 'process.Ext.token.integrity_level_name': ['high'], - 'Ransomware.files.path': [ - 'c:\\hd3vuk19y-readme.txt', - 'c:\\$winreagent\\hd3vuk19y-readme.txt', - 'c:\\aaantiransomelastic-do-not-touch-dab6d40c-a6a1-442c-adc4-9d57a47e58d7\\hd3vuk19y-readme.txt', - ], - 'process.parent.code_signature.subject_name': ['PB03 TRANSPORT LTD.'], - 'process.parent.executable': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'Ransomware.files.entropy': [3.629971457026797, 3.629971457026797, 3.629971457026797], - 'Ransomware.feature': ['canary'], - 'Ransomware.files.extension': ['txt', 'txt', 'txt'], - 'process.args': ['C:\\Windows\\MsMpEng.exe'], - 'process.code_signature.status': ['trusted'], - message: ['Ransomware Detection Alert'], - 'process.parent.args': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - '@timestamp': ['2024-05-07T12:48:45.000Z'], - 'process.command_line': ['"C:\\Windows\\MsMpEng.exe"'], - 'host.risk.calculated_level': ['High'], - 'process.hash.sha1': ['3d409b39b8502fcd23335a878f2cbdaf6d721995'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-01-20T23:38:22.964Z'], - }, - sort: [99, 1715086125000], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: 'b0fdf96721e361e1137d49a67e26d92f96b146392d7f44322bddc3d660abaef1', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'process.hash.md5': ['8cc83221870dd07144e63df594c391d9'], - 'event.category': ['malware', 'intrusion_detection'], - 'host.risk.calculated_score_norm': [75.62723], - 'process.parent.command_line': [ - '"C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe" ', - ], - 'process.parent.name': [ - 'd55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'user.name': ['Administrator'], - 'user.risk.calculated_level': ['High'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - '33bc14d231a4afaa18f06513766d5f69d8b88f1e697cd127d24fb4b72ad44c7a', - ], - 'process.pid': [8708], - 'process.code_signature.exists': [true], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': ['errorExpired'], - 'process.pe.original_file_name': ['MsMpEng.exe'], - 'event.module': ['endpoint'], - 'process.code_signature.subject_name': ['Microsoft Corporation'], - 'host.os.version': ['21H2 (10.0.20348.1366)'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [82.16188], - 'host.os.name': ['Windows'], - 'kibana.alert.rule.name': ['Memory Threat Detection Alert: Shellcode Injection'], - 'host.name': ['SRVWIN02'], - 'user.domain': ['OMM-WIN-DETECT'], - 'process.executable': ['C:\\Windows\\MsMpEng.exe'], - 'process.code_signature.trusted': [true], - 'process.Ext.token.integrity_level_name': ['high'], - 'process.parent.code_signature.subject_name': ['PB03 TRANSPORT LTD.'], - 'process.parent.executable': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'kibana.alert.workflow_status': ['open'], - 'process.args': ['C:\\Windows\\MsMpEng.exe'], - 'process.code_signature.status': ['trusted'], - message: ['Memory Threat Detection Alert: Shellcode Injection'], - 'process.parent.args_count': [1], - 'process.name': ['MsMpEng.exe'], - 'process.parent.args': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - '@timestamp': ['2024-05-07T12:48:44.996Z'], - 'process.parent.code_signature.trusted': [false], - 'process.command_line': ['"C:\\Windows\\MsMpEng.exe"'], - 'host.risk.calculated_level': ['High'], - _id: ['b0fdf96721e361e1137d49a67e26d92f96b146392d7f44322bddc3d660abaef1'], - 'process.hash.sha1': ['3d409b39b8502fcd23335a878f2cbdaf6d721995'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-01-20T23:38:22.174Z'], - }, - sort: [99, 1715086124996], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: '7b4f49f21cf141e67856d3207fb4ea069c8035b41f0ea501970694cf8bd43cbe', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'process.hash.md5': ['8cc83221870dd07144e63df594c391d9'], - 'event.category': ['malware', 'intrusion_detection'], - 'host.risk.calculated_score_norm': [75.62723], - 'process.parent.command_line': [ - '"C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe" ', - ], - 'process.parent.name': [ - 'd55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'user.name': ['Administrator'], - 'user.risk.calculated_level': ['High'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - '33bc14d231a4afaa18f06513766d5f69d8b88f1e697cd127d24fb4b72ad44c7a', - ], - 'process.pid': [8708], - 'process.code_signature.exists': [true], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': ['errorExpired'], - 'process.pe.original_file_name': ['MsMpEng.exe'], - 'event.module': ['endpoint'], - 'process.code_signature.subject_name': ['Microsoft Corporation'], - 'host.os.version': ['21H2 (10.0.20348.1366)'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [82.16188], - 'host.os.name': ['Windows'], - 'kibana.alert.rule.name': ['Memory Threat Detection Alert: Shellcode Injection'], - 'host.name': ['SRVWIN02'], - 'user.domain': ['OMM-WIN-DETECT'], - 'process.executable': ['C:\\Windows\\MsMpEng.exe'], - 'process.code_signature.trusted': [true], - 'process.Ext.token.integrity_level_name': ['high'], - 'process.parent.code_signature.subject_name': ['PB03 TRANSPORT LTD.'], - 'process.parent.executable': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'kibana.alert.workflow_status': ['open'], - 'process.args': ['C:\\Windows\\MsMpEng.exe'], - 'process.code_signature.status': ['trusted'], - message: ['Memory Threat Detection Alert: Shellcode Injection'], - 'process.parent.args_count': [1], - 'process.name': ['MsMpEng.exe'], - 'process.parent.args': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - '@timestamp': ['2024-05-07T12:48:44.986Z'], - 'process.parent.code_signature.trusted': [false], - 'process.command_line': ['"C:\\Windows\\MsMpEng.exe"'], - 'host.risk.calculated_level': ['High'], - _id: ['7b4f49f21cf141e67856d3207fb4ea069c8035b41f0ea501970694cf8bd43cbe'], - 'process.hash.sha1': ['3d409b39b8502fcd23335a878f2cbdaf6d721995'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-01-20T23:38:22.066Z'], - }, - sort: [99, 1715086124986], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: 'ea81d79104cbd442236b5bcdb7a3331de897aa4ce1523e622068038d048d0a9e', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'process.hash.md5': ['8cc83221870dd07144e63df594c391d9'], - 'event.category': ['malware', 'intrusion_detection', 'process'], - 'host.risk.calculated_score_norm': [75.62723], - 'process.parent.command_line': [ - '"C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe" ', - ], - 'process.parent.name': [ - 'd55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'user.risk.calculated_level': ['High'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - '33bc14d231a4afaa18f06513766d5f69d8b88f1e697cd127d24fb4b72ad44c7a', - ], - 'process.Ext.memory_region.malware_signature.primary.matches': [ - 'WVmF9nQli1UIg2YEAIk+iwoLSgQ=', - 'dQxy0zPAQF9eW4vlXcMzwOv1VYvsgw==', - 'DIsEsIN4BAV1HP9wCP9wDP91DP8=', - '+4tF/FCLCP9RCF6Lx19bi+Vdw1U=', - 'vAAAADPSi030i/GLRfAPpMEBwe4f', - 'VIvO99GLwiNN3PfQM030I8czReiJ', - 'DIlGDIXAdSozwOtsi0YIhcB0Yms=', - ], - 'process.pid': [8708], - 'process.code_signature.exists': [true], - 'process.code_signature.subject_name': ['Microsoft Corporation'], - 'host.os.version': ['21H2 (10.0.20348.1366)'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [82.16188], - 'host.os.name': ['Windows'], - 'kibana.alert.rule.name': [ - 'Memory Threat Detection Alert: Windows.Ransomware.Sodinokibi', - ], - 'host.name': ['SRVWIN02'], - 'event.outcome': ['success'], - 'process.code_signature.trusted': [true], - 'kibana.alert.workflow_status': ['open'], - 'rule.name': ['Windows.Ransomware.Sodinokibi'], - 'process.parent.args_count': [1], - 'process.Ext.memory_region.bytes_compressed_present': [false], - 'process.name': ['MsMpEng.exe'], - 'process.parent.code_signature.trusted': [false], - _id: ['ea81d79104cbd442236b5bcdb7a3331de897aa4ce1523e622068038d048d0a9e'], - 'user.name': ['Administrator'], - 'process.parent.code_signature.exists': [true], - 'process.parent.code_signature.status': ['errorExpired'], - 'process.pe.original_file_name': ['MsMpEng.exe'], - 'event.module': ['endpoint'], - 'process.Ext.memory_region.malware_signature.all_names': [ - 'Windows.Ransomware.Sodinokibi', - ], - 'user.domain': ['OMM-WIN-DETECT'], - 'process.executable': ['C:\\Windows\\MsMpEng.exe'], - 'process.Ext.memory_region.malware_signature.primary.signature.name': [ - 'Windows.Ransomware.Sodinokibi', - ], - 'process.Ext.token.integrity_level_name': ['high'], - 'process.parent.code_signature.subject_name': ['PB03 TRANSPORT LTD.'], - 'process.parent.executable': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - 'process.args': ['C:\\Windows\\MsMpEng.exe'], - 'process.code_signature.status': ['trusted'], - message: ['Memory Threat Detection Alert: Windows.Ransomware.Sodinokibi'], - 'process.parent.args': [ - 'C:\\Users\\Administrator\\Desktop\\8813719803\\d55f983c994caa160ec63a59f6b4250fe67fb3e8c43a388aec60a4a6978e9f1e.exe', - ], - '@timestamp': ['2024-05-07T12:48:44.975Z'], - 'process.command_line': ['"C:\\Windows\\MsMpEng.exe"'], - 'host.risk.calculated_level': ['High'], - 'process.hash.sha1': ['3d409b39b8502fcd23335a878f2cbdaf6d721995'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-01-20T23:38:25.169Z'], - }, - sort: [99, 1715086124975], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: 'cdf3b5510bb5ed622e8cefd1ce6bedc52bdd99a4c1ead537af0603469e713c8b', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'file.path': ['C:\\Users\\Administrator\\AppData\\Local\\cdnver.dll'], - 'process.hash.md5': ['4bfef0b578515c16b9582e32b78d2594'], - 'event.category': ['malware', 'intrusion_detection', 'library'], - 'host.risk.calculated_score_norm': [73.02488], - 'process.parent.command_line': ['C:\\Programdata\\Q3C7N1V8.exe'], - 'process.parent.name': ['Q3C7N1V8.exe'], - 'user.name': ['Administrator'], - 'user.risk.calculated_level': ['High'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - '70d21cbdc527559c4931421e66aa819b86d5af5535445ace467e74518164c46a', - ], - 'process.pid': [7824], - 'process.code_signature.exists': [true], - 'process.parent.code_signature.exists': [false], - 'process.pe.original_file_name': ['RUNDLL32.EXE'], - 'event.module': ['endpoint'], - 'process.code_signature.subject_name': ['Microsoft Windows'], - 'host.os.version': ['21H2 (10.0.20348.1366)'], - 'file.hash.sha256': ['12e6642cf6413bdf5388bee663080fa299591b2ba023d069286f3be9647547c8'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [82.16188], - 'host.os.name': ['Windows'], - 'kibana.alert.rule.name': ['Malware Detection Alert'], - 'host.name': ['SRVWIN01'], - 'user.domain': ['OMM-WIN-DETECT'], - 'process.executable': ['C:\\Windows\\SysWOW64\\rundll32.exe'], - 'event.outcome': ['success'], - 'process.code_signature.trusted': [true], - 'process.Ext.token.integrity_level_name': ['high'], - 'process.parent.executable': ['C:\\ProgramData\\Q3C7N1V8.exe'], - 'kibana.alert.workflow_status': ['open'], - 'file.name': ['cdnver.dll'], - 'process.args': [ - 'C:\\Windows\\System32\\rundll32.exe', - 'C:\\Users\\Administrator\\AppData\\Local\\cdnver.dll,#1', - ], - 'process.code_signature.status': ['trusted'], - message: ['Malware Detection Alert'], - 'process.parent.args_count': [1], - 'process.name': ['rundll32.exe'], - 'process.parent.args': ['C:\\Programdata\\Q3C7N1V8.exe'], - '@timestamp': ['2024-05-07T12:47:32.838Z'], - 'process.command_line': [ - '"C:\\Windows\\System32\\rundll32.exe" "C:\\Users\\Administrator\\AppData\\Local\\cdnver.dll",#1', - ], - 'host.risk.calculated_level': ['High'], - _id: ['cdf3b5510bb5ed622e8cefd1ce6bedc52bdd99a4c1ead537af0603469e713c8b'], - 'process.hash.sha1': ['9b16507aaf10a0aafa0df2ba83e8eb2708d83a02'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-01-16T01:51:26.472Z'], - }, - sort: [99, 1715086052838], - }, - { - _index: '.internal.alerts-security.alerts-default-000001', - _id: '6abe81eb6350fb08031761be029e7ab19f7e577a7c17a9c5ea1ed010ba1620e3', - _score: null, - fields: { - 'kibana.alert.severity': ['critical'], - 'process.hash.md5': ['4bfef0b578515c16b9582e32b78d2594'], - 'event.category': ['malware', 'intrusion_detection'], - 'host.risk.calculated_score_norm': [73.02488], - 'process.parent.command_line': ['C:\\Programdata\\Q3C7N1V8.exe'], - 'process.parent.name': ['Q3C7N1V8.exe'], - 'user.risk.calculated_level': ['High'], - 'kibana.alert.rule.description': [ - 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', - ], - 'process.hash.sha256': [ - '70d21cbdc527559c4931421e66aa819b86d5af5535445ace467e74518164c46a', - ], - 'process.pid': [7824], - 'process.code_signature.exists': [true], - 'process.code_signature.subject_name': ['Microsoft Windows'], - 'host.os.version': ['21H2 (10.0.20348.1366)'], - 'kibana.alert.risk_score': [99], - 'user.risk.calculated_score_norm': [82.16188], - 'host.os.name': ['Windows'], - 'kibana.alert.rule.name': [ - 'Malicious Behavior Detection Alert: RunDLL32 with Unusual Arguments', - ], - 'host.name': ['SRVWIN01'], - 'event.outcome': ['success'], - 'process.code_signature.trusted': [true], - 'kibana.alert.workflow_status': ['open'], - 'rule.name': ['RunDLL32 with Unusual Arguments'], - 'threat.tactic.id': ['TA0005'], - 'threat.tactic.name': ['Defense Evasion'], - 'threat.technique.id': ['T1218'], - 'process.parent.args_count': [1], - 'threat.technique.subtechnique.reference': [ - 'https://attack.mitre.org/techniques/T1218/011/', - ], - 'process.name': ['rundll32.exe'], - 'threat.technique.subtechnique.name': ['Rundll32'], - _id: ['6abe81eb6350fb08031761be029e7ab19f7e577a7c17a9c5ea1ed010ba1620e3'], - 'threat.technique.name': ['System Binary Proxy Execution'], - 'threat.tactic.reference': ['https://attack.mitre.org/tactics/TA0005/'], - 'user.name': ['Administrator'], - 'threat.framework': ['MITRE ATT&CK'], - 'process.working_directory': ['C:\\Users\\Administrator\\Documents\\'], - 'process.pe.original_file_name': ['RUNDLL32.EXE'], - 'event.module': ['endpoint'], - 'user.domain': ['OMM-WIN-DETECT'], - 'process.executable': ['C:\\Windows\\SysWOW64\\rundll32.exe'], - 'process.Ext.token.integrity_level_name': ['high'], - 'process.parent.executable': ['C:\\ProgramData\\Q3C7N1V8.exe'], - 'process.args': [ - 'C:\\Windows\\System32\\rundll32.exe', - 'C:\\Users\\Administrator\\AppData\\Local\\cdnver.dll,#1', - ], - 'process.code_signature.status': ['trusted'], - message: ['Malicious Behavior Detection Alert: RunDLL32 with Unusual Arguments'], - 'process.parent.args': ['C:\\Programdata\\Q3C7N1V8.exe'], - '@timestamp': ['2024-05-07T12:47:32.836Z'], - 'threat.technique.subtechnique.id': ['T1218.011'], - 'threat.technique.reference': ['https://attack.mitre.org/techniques/T1218/'], - 'process.command_line': [ - '"C:\\Windows\\System32\\rundll32.exe" "C:\\Users\\Administrator\\AppData\\Local\\cdnver.dll",#1', - ], - 'host.risk.calculated_level': ['High'], - 'process.hash.sha1': ['9b16507aaf10a0aafa0df2ba83e8eb2708d83a02'], - 'event.dataset': ['endpoint.alerts'], - 'kibana.alert.original_time': ['2023-01-16T01:51:26.348Z'], - }, - sort: [99, 1715086052836], - }, - ], - }, -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/discard_previous_generations/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/discard_previous_generations/index.ts deleted file mode 100644 index a40dde44f8d67..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/discard_previous_generations/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { GraphState } from '../../../../types'; - -export const discardPreviousGenerations = ({ - generationAttempts, - hallucinationFailures, - isHallucinationDetected, - state, -}: { - generationAttempts: number; - hallucinationFailures: number; - isHallucinationDetected: boolean; - state: GraphState; -}): GraphState => { - return { - ...state, - combinedGenerations: '', // <-- reset the combined generations - generationAttempts: generationAttempts + 1, - generations: [], // <-- reset the generations - hallucinationFailures: isHallucinationDetected - ? hallucinationFailures + 1 - : hallucinationFailures, - }; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_alerts_context_prompt/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_alerts_context_prompt/index.ts deleted file mode 100644 index d92d935053577..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_alerts_context_prompt/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// NOTE: we ask the LLM to `provide insights`. We do NOT use the feature name, `AttackDiscovery`, in the prompt. -export const getAlertsContextPrompt = ({ - anonymizedAlerts, - attackDiscoveryPrompt, -}: { - anonymizedAlerts: string[]; - attackDiscoveryPrompt: string; -}) => `${attackDiscoveryPrompt} - -Use context from the following alerts to provide insights: - -""" -${anonymizedAlerts.join('\n\n')} -""" -`; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_anonymized_alerts_from_state/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_anonymized_alerts_from_state/index.ts deleted file mode 100644 index fb7cf6bd59f98..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_anonymized_alerts_from_state/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { GraphState } from '../../../../types'; - -export const getAnonymizedAlertsFromState = (state: GraphState): string[] => - state.anonymizedAlerts.map((doc) => doc.pageContent); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_use_unrefined_results/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_use_unrefined_results/index.ts deleted file mode 100644 index face2a6afc6bc..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_use_unrefined_results/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AttackDiscovery } from '@kbn/elastic-assistant-common'; - -import { getMaxRetriesReached } from '../../../../helpers/get_max_retries_reached'; - -export const getUseUnrefinedResults = ({ - generationAttempts, - maxGenerationAttempts, - unrefinedResults, -}: { - generationAttempts: number; - maxGenerationAttempts: number; - unrefinedResults: AttackDiscovery[] | null; -}): boolean => { - const nextAttemptWouldExcedLimit = getMaxRetriesReached({ - generationAttempts: generationAttempts + 1, // + 1, because we just used an attempt - maxGenerationAttempts, - }); - - return nextAttemptWouldExcedLimit && unrefinedResults != null && unrefinedResults.length > 0; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/index.ts deleted file mode 100644 index 1fcd81622f0fe..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/index.ts +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ActionsClientLlm } from '@kbn/langchain/server'; -import type { Logger } from '@kbn/core/server'; - -import { discardPreviousGenerations } from './helpers/discard_previous_generations'; -import { extractJson } from '../helpers/extract_json'; -import { getAnonymizedAlertsFromState } from './helpers/get_anonymized_alerts_from_state'; -import { getChainWithFormatInstructions } from '../helpers/get_chain_with_format_instructions'; -import { getCombined } from '../helpers/get_combined'; -import { getCombinedAttackDiscoveryPrompt } from '../helpers/get_combined_attack_discovery_prompt'; -import { generationsAreRepeating } from '../helpers/generations_are_repeating'; -import { getUseUnrefinedResults } from './helpers/get_use_unrefined_results'; -import { parseCombinedOrThrow } from '../helpers/parse_combined_or_throw'; -import { responseIsHallucinated } from '../helpers/response_is_hallucinated'; -import type { GraphState } from '../../types'; - -export const getGenerateNode = ({ - llm, - logger, -}: { - llm: ActionsClientLlm; - logger?: Logger; -}): ((state: GraphState) => Promise) => { - const generate = async (state: GraphState): Promise => { - logger?.debug(() => `---GENERATE---`); - - const anonymizedAlerts: string[] = getAnonymizedAlertsFromState(state); - - const { - attackDiscoveryPrompt, - combinedGenerations, - generationAttempts, - generations, - hallucinationFailures, - maxGenerationAttempts, - maxRepeatedGenerations, - } = state; - - let combinedResponse = ''; // mutable, because it must be accessed in the catch block - let partialResponse = ''; // mutable, because it must be accessed in the catch block - - try { - const query = getCombinedAttackDiscoveryPrompt({ - anonymizedAlerts, - attackDiscoveryPrompt, - combinedMaybePartialResults: combinedGenerations, - }); - - const { chain, formatInstructions, llmType } = getChainWithFormatInstructions(llm); - - logger?.debug( - () => `generate node is invoking the chain (${llmType}), attempt ${generationAttempts}` - ); - - const rawResponse = (await chain.invoke({ - format_instructions: formatInstructions, - query, - })) as unknown as string; - - // LOCAL MUTATION: - partialResponse = extractJson(rawResponse); // remove the surrounding ```json``` - - // if the response is hallucinated, discard previous generations and start over: - if (responseIsHallucinated(partialResponse)) { - logger?.debug( - () => - `generate node detected a hallucination (${llmType}), on attempt ${generationAttempts}; discarding the accumulated generations and starting over` - ); - - return discardPreviousGenerations({ - generationAttempts, - hallucinationFailures, - isHallucinationDetected: true, - state, - }); - } - - // if the generations are repeating, discard previous generations and start over: - if ( - generationsAreRepeating({ - currentGeneration: partialResponse, - previousGenerations: generations, - sampleLastNGenerations: maxRepeatedGenerations, - }) - ) { - logger?.debug( - () => - `generate node detected (${llmType}), detected ${maxRepeatedGenerations} repeated generations on attempt ${generationAttempts}; discarding the accumulated results and starting over` - ); - - // discard the accumulated results and start over: - return discardPreviousGenerations({ - generationAttempts, - hallucinationFailures, - isHallucinationDetected: false, - state, - }); - } - - // LOCAL MUTATION: - combinedResponse = getCombined({ combinedGenerations, partialResponse }); // combine the new response with the previous ones - - const unrefinedResults = parseCombinedOrThrow({ - combinedResponse, - generationAttempts, - llmType, - logger, - nodeName: 'generate', - }); - - // use the unrefined results if we already reached the max number of retries: - const useUnrefinedResults = getUseUnrefinedResults({ - generationAttempts, - maxGenerationAttempts, - unrefinedResults, - }); - - if (useUnrefinedResults) { - logger?.debug( - () => - `generate node is using unrefined results response (${llm._llmType()}) from attempt ${generationAttempts}, because all attempts have been used` - ); - } - - return { - ...state, - attackDiscoveries: useUnrefinedResults ? unrefinedResults : null, // optionally skip the refinement step by returning the final answer - combinedGenerations: combinedResponse, - generationAttempts: generationAttempts + 1, - generations: [...generations, partialResponse], - unrefinedResults, - }; - } catch (error) { - const parsingError = `generate node is unable to parse (${llm._llmType()}) response from attempt ${generationAttempts}; (this may be an incomplete response from the model): ${error}`; - logger?.debug(() => parsingError); // logged at debug level because the error is expected when the model returns an incomplete response - - return { - ...state, - combinedGenerations: combinedResponse, - errors: [...state.errors, parsingError], - generationAttempts: generationAttempts + 1, - generations: [...generations, partialResponse], - }; - } - }; - - return generate; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/schema/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/schema/index.ts deleted file mode 100644 index 05210799f151c..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/schema/index.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod'; - -export const SYNTAX = '{{ field.name fieldValue1 fieldValue2 fieldValueN }}'; -const GOOD_SYNTAX_EXAMPLES = - 'Examples of CORRECT syntax (includes field names and values): {{ host.name hostNameValue }} {{ user.name userNameValue }} {{ source.ip sourceIpValue }}'; - -const BAD_SYNTAX_EXAMPLES = - 'Examples of INCORRECT syntax (bad, because the field names are not included): {{ hostNameValue }} {{ userNameValue }} {{ sourceIpValue }}'; - -const RECONNAISSANCE = 'Reconnaissance'; -const INITIAL_ACCESS = 'Initial Access'; -const EXECUTION = 'Execution'; -const PERSISTENCE = 'Persistence'; -const PRIVILEGE_ESCALATION = 'Privilege Escalation'; -const DISCOVERY = 'Discovery'; -const LATERAL_MOVEMENT = 'Lateral Movement'; -const COMMAND_AND_CONTROL = 'Command and Control'; -const EXFILTRATION = 'Exfiltration'; - -const MITRE_ATTACK_TACTICS = [ - RECONNAISSANCE, - INITIAL_ACCESS, - EXECUTION, - PERSISTENCE, - PRIVILEGE_ESCALATION, - DISCOVERY, - LATERAL_MOVEMENT, - COMMAND_AND_CONTROL, - EXFILTRATION, -] as const; - -export const AttackDiscoveriesGenerationSchema = z.object({ - insights: z - .array( - z.object({ - alertIds: z.string().array().describe(`The alert IDs that the insight is based on.`), - detailsMarkdown: z - .string() - .describe( - `A detailed insight with markdown, where each markdown bullet contains a description of what happened that reads like a story of the attack as it played out and always uses special ${SYNTAX} syntax for field names and values from the source data. ${GOOD_SYNTAX_EXAMPLES} ${BAD_SYNTAX_EXAMPLES}` - ), - entitySummaryMarkdown: z - .string() - .optional() - .describe( - `A short (no more than a sentence) summary of the insight featuring only the host.name and user.name fields (when they are applicable), using the same ${SYNTAX} syntax` - ), - mitreAttackTactics: z - .string() - .array() - .optional() - .describe( - `An array of MITRE ATT&CK tactic for the insight, using one of the following values: ${MITRE_ATTACK_TACTICS.join( - ',' - )}` - ), - summaryMarkdown: z - .string() - .describe(`A markdown summary of insight, using the same ${SYNTAX} syntax`), - title: z - .string() - .describe( - 'A short, no more than 7 words, title for the insight, NOT formatted with special syntax or markdown. This must be as brief as possible.' - ), - }) - ) - .describe( - `Insights with markdown that always uses special ${SYNTAX} syntax for field names and values from the source data. ${GOOD_SYNTAX_EXAMPLES} ${BAD_SYNTAX_EXAMPLES}` - ), -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/add_trailing_backticks_if_necessary/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/add_trailing_backticks_if_necessary/index.ts deleted file mode 100644 index fd824709f5fcf..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/add_trailing_backticks_if_necessary/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const addTrailingBackticksIfNecessary = (text: string): string => { - const leadingJSONpattern = /^\w*```json(.*?)/s; - const trailingBackticksPattern = /(.*?)```\w*$/s; - - const hasLeadingJSONWrapper = leadingJSONpattern.test(text); - const hasTrailingBackticks = trailingBackticksPattern.test(text); - - if (hasLeadingJSONWrapper && !hasTrailingBackticks) { - return `${text}\n\`\`\``; - } - - return text; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/extract_json/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/extract_json/index.test.ts deleted file mode 100644 index 5e13ec9f0dafe..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/extract_json/index.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { extractJson } from '.'; - -describe('extractJson', () => { - it('returns the JSON text surrounded by ```json and ``` with no whitespace or additional text', () => { - const input = '```json{"key": "value"}```'; - - const expected = '{"key": "value"}'; - - expect(extractJson(input)).toBe(expected); - }); - - it('returns the JSON block when surrounded by additional text and whitespace', () => { - const input = - 'You asked for some JSON, here it is:\n```json\n{"key": "value"}\n```\nI hope that works for you.'; - - const expected = '{"key": "value"}'; - - expect(extractJson(input)).toBe(expected); - }); - - it('returns the original text if no JSON block is found', () => { - const input = "There's no JSON here, just some text."; - - expect(extractJson(input)).toBe(input); - }); - - it('trims leading and trailing whitespace from the extracted JSON', () => { - const input = 'Text before\n```json\n {"key": "value"} \n```\nText after'; - - const expected = '{"key": "value"}'; - - expect(extractJson(input)).toBe(expected); - }); - - it('handles incomplete JSON blocks with no trailing ```', () => { - const input = 'Text before\n```json\n{"key": "value"'; // <-- no closing ```, because incomplete generation - - expect(extractJson(input)).toBe('{"key": "value"'); - }); - - it('handles multiline json (real world edge case)', () => { - const input = - '```json\n{\n "insights": [\n {\n "alertIds": [\n "a609473a23b3a66a40f2bba06795c28a0c12863c6931f39e472d069f5600cbae",\n "04a9ded2b4f10ea407711f0010d426ad328eea43ae53e1e0bf166c058947dff6",\n "8d53b9838181299b3c0b1544ea469216d72ad2234a1cce44017dd248a08d78d1",\n "51d0080ffcc1982dbae7c31a9a021f7b51422000dec1f0e0bb58bd61d934c893",\n "d93302956bee58d538f6f7a6cbf944e549e8466dacfb554a302dce46a069eef0",\n "75c89f679397f089716034cde20f5547a2e6bdd1606b1e002e0976ab339c4cd9",\n "5d8e9427c0ecc4daa5809bfe250b9a382c53e81e8f39eec87499d28efdda9300",\n "f18ac1874f510fd3fabb0ae48d0714f4952b294496ef1d993e3eb03f839e2d83",\n "e37cb31213c4c4e80beaf9f75e7966f88cdd86a228c6cb1a28e46356410fa78f",\n "cf70077b8888e8fbe434808fddbaf65d97fff244bb185a595cf0ad487e9c5850",\n "01bea609f0880b10b7b3c6cf6e8245ef0f134386fdcbf2a167e72487e0bda616",\n "289621edc88fd8b4775c541e46bcfdea40538291266179c59a5ca5afbee74cfc",\n "ba121c2045058b62a92e6a3abadd3c78a005b89129630e2271b2f45d5fd995b2",\n "fceb940b252be079df3629550d852bd2793f79071c917227268fa1b805abc8d1",\n "7044589c27bab148cdb97d9e2eeb88bd924fca82a6a05a53ec94dcadf8e56303",\n "1b68be35429f52280456aab17dd94191fe5c47fed9768f00d9f9e9044a08bbb5",\n "52478d4a119bbc44bec67f384f83dfa20b33cf9963177e619cd47a32bababe12",\n "fecbbb8924493b466e8f5744e0875a9ee91f326213b691b576b13da3fb875ebf",\n "c46bbdeb7b59f52c976e7e4f30e3d5c65f417d716cb140096b5edba52b1449a1",\n "f12caebcbda087fc8b49cdced64a8997dd1428f4cf91ebb251434a55126399b3",\n "c7478edbd13af443cfafc57d50e5206c5ae8c0f9c9cabc073fdc2d3946559617",\n "3585ae62651929ef405f9783410d7a94f4254d299205e22f22966178f189bb11",\n "f50f531912af1d31a66a0e37d4c0d9c571c2cca6bef2c7f8453eb3ab67c4d1a0",\n "95a9403f0bb97d03fc3c2eb06386503831766f541b736468088092c5e0e25830",\n "c1292c67f3ccd2cb2651c601f0816122cfa459276fa5fc89b40c62d1a793963e",\n "8911886e1b2964176f70eaee2aa6693ce101ee9c8ec5434acdc7ff18616ec31c",\n "bfbfb02c03c6f69fc2352c48d8fd7c7e4b557c611e16956fbb63e337a513e699",\n "064cbdc1932029fcb34f6ba685211b971afde3b8aa4325054bedaf4c9e4587ed",\n "9fd5d0ca9b9fff6e37f1114ad874103badb2b0570ef143cd4a26a553effdff00",\n "9e2687f26f04b5a8def3266f89fbe7217da2d4355c3b035268df1802f1342c81",\n "64557c4006c52119c01f6e3e582ce1b8207b2e8f64aaaa630ca1fd156c01ea1e",\n "df98d2568c986d101af055f78c7e2a39299627531c28012b5025d10e2ec1b208",\n "10683db11fb21cae36577f83722c686c2fc691d2be6fc4396f2733564f3210d1",\n "f46e7b3266200e3e23b15b5acea7bb934e2c17d23058e10daeed51f036f4932b",\n "3c77d55f912b80b66cc1e4e1df02a22ddee07c50338a409374fb2567d2fb4ca3",\n "8ec169c0fdf558c0d9d9ad8dedad0898b15bb718421b4cab8f5cce4ebcb78254",\n "4119a1705f993588f8d1d576e567ec17f102aeafe535e53bb56ec833418ccd08",\n "b53d06bfd23ab843dba67e2fde0da6364475b0bfb9c40cb8a6641cc4ecadec01",\n "1dcd85c8279fd7152dadecfc547cce06261d23ef4589fe4fdcc92b1ceeb76c0f",\n "d4ed490b1d39925ee612058655030bdb7cecda3e5893e1c40dbbac852b72fbc6",\n "2ecc96c4d51f5338684c08e7c67357e504abfec6fc4f21753a3c941189db68e1",\n "0c9fb123686bc739d117ee4f607ffbcef39f1f72e7eab6d01b70bbb40480b3d6",\n "162be5e04f54a5cd475d2437fe769ee044324b0a32ce83a735f61719b8b5fd63",\n "21eae60b4b29f7f01cc7006372374e1c5d6912858c33397cdbe4470df97fba79",\n "0409539590b6d9b80f7071d3d5658434f982ba7957aa6a5037f8b7a73b70100d",\n "5e8e654df34a9053f8b90e4ade25520dbee5994ebf7da531e1e7255d029ab031",\n "3ef381b2d29d71bc3ac8580d333344948a2664855a89ff037299a8b4aa663293",\n "0aef1fe2506842f9c53549049b47a8166bcc3d6efe2d8fcf1e57f3a634ed137c",\n "c2d12dacd0cd6ef4a7386c8d0146d3eb91a7e1e9f2d8d47bffaab07a92577993",\n "45e6663c65172e225e2531df3dce58096ed6e9a7d0fd7819e5b6f094a41731a0",\n "f2af064d46f1db1d96c7c9508a462993851e42f29566f2101ea3a1c51e5e451c",\n "b75c046d06f86eea41826999211ab5e6c9cb5fe067ade561fb5dc5f0b52d4584",\n "1fb9fbb26b78c2e9c56abf8e39e4cb278a5a382d53115dcb1624fdefca762865",\n "d78c4d12f6d50278be6320df1fe10beeef8723558cdb12d9d6c7d1aa8180498b",\n "c8fa7d3a31906893c47df234318e94bc4371b55ac54edc60b2c09afd8a9291c6",\n "5236dc9c55f19d8aed50078cc6ecd1de85041afa65003276fc311c14d5a74d0a",\n "efb9d548ff94246a22cfa8e06b70689d8f3edf69c8ad45c3811e0d340b4b10ff",\n "842c8d78d995f49b569934cf5e8316ba1d93a1d73e757210d5f0eb7e1ed52049",\n "b95dcfba35d31ab263bfab939280c71893bdb39e3a744c2f3cc38612ebcbb42a",\n "d6387171a203c64fd1c09514a028cf813d2ffccf968831c92cdf22287992e004",\n "b8d098f358ce5e8fa2900ac18435078652353a32a19ef2fd038bf82eee3a0731"\n ],\n "detailsMarkdown": "### Attack Progression\\n- **Initial Access**: The attack began with a spearphishing attachment delivered via Microsoft Office documents. The documents contained malicious macros that executed upon opening.\\n- **Execution**: The malicious macros executed various commands, including the use of `certutil` to decode and execute payloads, and `regsvr32` to register malicious DLLs.\\n- **Persistence**: The attackers established persistence by modifying registry run keys and creating scheduled tasks.\\n- **Credential Access**: The attackers attempted to capture credentials using `osascript` on macOS systems.\\n- **Defense Evasion**: The attackers used code signing with invalid or expired certificates to evade detection.\\n- **Command and Control**: The attackers established command and control channels using various techniques, including the use of `mshta` and `powershell` scripts.\\n- **Exfiltration**: The attackers exfiltrated data using tools like `curl` to transfer data to remote servers.\\n- **Impact**: The attackers deployed ransomware, including `Sodinokibi` and `Bumblebee`, to encrypt files and demand ransom payments.\\n\\n### Affected Hosts and Users\\n- **Hosts**: Multiple hosts across different operating systems (Windows, macOS, Linux) were affected.\\n- **Users**: The attacks targeted various users, including administrators and regular users.\\n\\n### Known Threat Groups\\n- The attack patterns and techniques used in this campaign are consistent with those employed by known threat groups such as `Emotet`, `Qbot`, and `Sodinokibi`.\\n\\n### Recommendations\\n- **Immediate Actions**: Isolate affected systems, reset passwords, and review network traffic for signs of command and control communications.\\n- **Long-term Actions**: Implement multi-factor authentication, conduct regular security awareness training, and deploy advanced endpoint protection solutions.",\n "entitySummaryMarkdown": "{{ host.name 9ed6a9db-da4d-4877-a2b4-f7a22cc55e9a }} {{ user.name c45d8d76-bff6-4c4b-aa5a-62eb15d68adb }}",\n "mitreAttackTactics": [\n "Initial Access",\n "Execution",\n "Persistence",\n "Credential Access",\n "Defense Evasion",\n "Command and Control",\n "Exfiltration",\n "Impact"\n ],\n "summaryMarkdown": "A sophisticated multi-stage attack was detected, involving spearphishing, credential access, and ransomware deployment. The attack targeted multiple hosts and users across different operating systems.",\n "title": "Multi-Stage Cyber Attack Detected"\n }\n ]\n}\n```'; - - const expected = - '{\n "insights": [\n {\n "alertIds": [\n "a609473a23b3a66a40f2bba06795c28a0c12863c6931f39e472d069f5600cbae",\n "04a9ded2b4f10ea407711f0010d426ad328eea43ae53e1e0bf166c058947dff6",\n "8d53b9838181299b3c0b1544ea469216d72ad2234a1cce44017dd248a08d78d1",\n "51d0080ffcc1982dbae7c31a9a021f7b51422000dec1f0e0bb58bd61d934c893",\n "d93302956bee58d538f6f7a6cbf944e549e8466dacfb554a302dce46a069eef0",\n "75c89f679397f089716034cde20f5547a2e6bdd1606b1e002e0976ab339c4cd9",\n "5d8e9427c0ecc4daa5809bfe250b9a382c53e81e8f39eec87499d28efdda9300",\n "f18ac1874f510fd3fabb0ae48d0714f4952b294496ef1d993e3eb03f839e2d83",\n "e37cb31213c4c4e80beaf9f75e7966f88cdd86a228c6cb1a28e46356410fa78f",\n "cf70077b8888e8fbe434808fddbaf65d97fff244bb185a595cf0ad487e9c5850",\n "01bea609f0880b10b7b3c6cf6e8245ef0f134386fdcbf2a167e72487e0bda616",\n "289621edc88fd8b4775c541e46bcfdea40538291266179c59a5ca5afbee74cfc",\n "ba121c2045058b62a92e6a3abadd3c78a005b89129630e2271b2f45d5fd995b2",\n "fceb940b252be079df3629550d852bd2793f79071c917227268fa1b805abc8d1",\n "7044589c27bab148cdb97d9e2eeb88bd924fca82a6a05a53ec94dcadf8e56303",\n "1b68be35429f52280456aab17dd94191fe5c47fed9768f00d9f9e9044a08bbb5",\n "52478d4a119bbc44bec67f384f83dfa20b33cf9963177e619cd47a32bababe12",\n "fecbbb8924493b466e8f5744e0875a9ee91f326213b691b576b13da3fb875ebf",\n "c46bbdeb7b59f52c976e7e4f30e3d5c65f417d716cb140096b5edba52b1449a1",\n "f12caebcbda087fc8b49cdced64a8997dd1428f4cf91ebb251434a55126399b3",\n "c7478edbd13af443cfafc57d50e5206c5ae8c0f9c9cabc073fdc2d3946559617",\n "3585ae62651929ef405f9783410d7a94f4254d299205e22f22966178f189bb11",\n "f50f531912af1d31a66a0e37d4c0d9c571c2cca6bef2c7f8453eb3ab67c4d1a0",\n "95a9403f0bb97d03fc3c2eb06386503831766f541b736468088092c5e0e25830",\n "c1292c67f3ccd2cb2651c601f0816122cfa459276fa5fc89b40c62d1a793963e",\n "8911886e1b2964176f70eaee2aa6693ce101ee9c8ec5434acdc7ff18616ec31c",\n "bfbfb02c03c6f69fc2352c48d8fd7c7e4b557c611e16956fbb63e337a513e699",\n "064cbdc1932029fcb34f6ba685211b971afde3b8aa4325054bedaf4c9e4587ed",\n "9fd5d0ca9b9fff6e37f1114ad874103badb2b0570ef143cd4a26a553effdff00",\n "9e2687f26f04b5a8def3266f89fbe7217da2d4355c3b035268df1802f1342c81",\n "64557c4006c52119c01f6e3e582ce1b8207b2e8f64aaaa630ca1fd156c01ea1e",\n "df98d2568c986d101af055f78c7e2a39299627531c28012b5025d10e2ec1b208",\n "10683db11fb21cae36577f83722c686c2fc691d2be6fc4396f2733564f3210d1",\n "f46e7b3266200e3e23b15b5acea7bb934e2c17d23058e10daeed51f036f4932b",\n "3c77d55f912b80b66cc1e4e1df02a22ddee07c50338a409374fb2567d2fb4ca3",\n "8ec169c0fdf558c0d9d9ad8dedad0898b15bb718421b4cab8f5cce4ebcb78254",\n "4119a1705f993588f8d1d576e567ec17f102aeafe535e53bb56ec833418ccd08",\n "b53d06bfd23ab843dba67e2fde0da6364475b0bfb9c40cb8a6641cc4ecadec01",\n "1dcd85c8279fd7152dadecfc547cce06261d23ef4589fe4fdcc92b1ceeb76c0f",\n "d4ed490b1d39925ee612058655030bdb7cecda3e5893e1c40dbbac852b72fbc6",\n "2ecc96c4d51f5338684c08e7c67357e504abfec6fc4f21753a3c941189db68e1",\n "0c9fb123686bc739d117ee4f607ffbcef39f1f72e7eab6d01b70bbb40480b3d6",\n "162be5e04f54a5cd475d2437fe769ee044324b0a32ce83a735f61719b8b5fd63",\n "21eae60b4b29f7f01cc7006372374e1c5d6912858c33397cdbe4470df97fba79",\n "0409539590b6d9b80f7071d3d5658434f982ba7957aa6a5037f8b7a73b70100d",\n "5e8e654df34a9053f8b90e4ade25520dbee5994ebf7da531e1e7255d029ab031",\n "3ef381b2d29d71bc3ac8580d333344948a2664855a89ff037299a8b4aa663293",\n "0aef1fe2506842f9c53549049b47a8166bcc3d6efe2d8fcf1e57f3a634ed137c",\n "c2d12dacd0cd6ef4a7386c8d0146d3eb91a7e1e9f2d8d47bffaab07a92577993",\n "45e6663c65172e225e2531df3dce58096ed6e9a7d0fd7819e5b6f094a41731a0",\n "f2af064d46f1db1d96c7c9508a462993851e42f29566f2101ea3a1c51e5e451c",\n "b75c046d06f86eea41826999211ab5e6c9cb5fe067ade561fb5dc5f0b52d4584",\n "1fb9fbb26b78c2e9c56abf8e39e4cb278a5a382d53115dcb1624fdefca762865",\n "d78c4d12f6d50278be6320df1fe10beeef8723558cdb12d9d6c7d1aa8180498b",\n "c8fa7d3a31906893c47df234318e94bc4371b55ac54edc60b2c09afd8a9291c6",\n "5236dc9c55f19d8aed50078cc6ecd1de85041afa65003276fc311c14d5a74d0a",\n "efb9d548ff94246a22cfa8e06b70689d8f3edf69c8ad45c3811e0d340b4b10ff",\n "842c8d78d995f49b569934cf5e8316ba1d93a1d73e757210d5f0eb7e1ed52049",\n "b95dcfba35d31ab263bfab939280c71893bdb39e3a744c2f3cc38612ebcbb42a",\n "d6387171a203c64fd1c09514a028cf813d2ffccf968831c92cdf22287992e004",\n "b8d098f358ce5e8fa2900ac18435078652353a32a19ef2fd038bf82eee3a0731"\n ],\n "detailsMarkdown": "### Attack Progression\\n- **Initial Access**: The attack began with a spearphishing attachment delivered via Microsoft Office documents. The documents contained malicious macros that executed upon opening.\\n- **Execution**: The malicious macros executed various commands, including the use of `certutil` to decode and execute payloads, and `regsvr32` to register malicious DLLs.\\n- **Persistence**: The attackers established persistence by modifying registry run keys and creating scheduled tasks.\\n- **Credential Access**: The attackers attempted to capture credentials using `osascript` on macOS systems.\\n- **Defense Evasion**: The attackers used code signing with invalid or expired certificates to evade detection.\\n- **Command and Control**: The attackers established command and control channels using various techniques, including the use of `mshta` and `powershell` scripts.\\n- **Exfiltration**: The attackers exfiltrated data using tools like `curl` to transfer data to remote servers.\\n- **Impact**: The attackers deployed ransomware, including `Sodinokibi` and `Bumblebee`, to encrypt files and demand ransom payments.\\n\\n### Affected Hosts and Users\\n- **Hosts**: Multiple hosts across different operating systems (Windows, macOS, Linux) were affected.\\n- **Users**: The attacks targeted various users, including administrators and regular users.\\n\\n### Known Threat Groups\\n- The attack patterns and techniques used in this campaign are consistent with those employed by known threat groups such as `Emotet`, `Qbot`, and `Sodinokibi`.\\n\\n### Recommendations\\n- **Immediate Actions**: Isolate affected systems, reset passwords, and review network traffic for signs of command and control communications.\\n- **Long-term Actions**: Implement multi-factor authentication, conduct regular security awareness training, and deploy advanced endpoint protection solutions.",\n "entitySummaryMarkdown": "{{ host.name 9ed6a9db-da4d-4877-a2b4-f7a22cc55e9a }} {{ user.name c45d8d76-bff6-4c4b-aa5a-62eb15d68adb }}",\n "mitreAttackTactics": [\n "Initial Access",\n "Execution",\n "Persistence",\n "Credential Access",\n "Defense Evasion",\n "Command and Control",\n "Exfiltration",\n "Impact"\n ],\n "summaryMarkdown": "A sophisticated multi-stage attack was detected, involving spearphishing, credential access, and ransomware deployment. The attack targeted multiple hosts and users across different operating systems.",\n "title": "Multi-Stage Cyber Attack Detected"\n }\n ]\n}'; - - expect(extractJson(input)).toBe(expected); - }); - - it('handles "Here is my analysis of the security events in JSON format" (real world edge case)', () => { - const input = - 'Here is my analysis of the security events in JSON format:\n\n```json\n{\n "insights": [\n {\n "alertIds": [\n "d776c8406fd81427b1f166550ac1b949017da7a13dc734594e4b05f24622b26e",\n "504c012054cfe91986311b4e6bc8523914434fab590e5c07c0328fab6566753c",\n "b706b8c19e68cc4f54b69f0a93e32b10f4102b610213b7826fb1d303b90a0536",\n "7763ebe716c47f64987362a9fb120d73873c77d26ad915f2c3d57c5dd3b7eed0",\n "25c61e0423a9bfd7f268ca6e9b67d4f507207c0cb1e1b4701aa5248cb3866f1f",\n "ea99e1633177f0c82e5126d4c999db2128c3adac6af4c7f4f183abc44486f070"\n ],\n "detailsMarkdown": "- At {{ kibana.alert.original_time 2024-05-16T18:50:17.566Z }}, a malicious file with SHA256 hash {{ file.hash.sha256 74ef6cc38f5a1a80148752b63c117e6846984debd2af806c65887195a8eccc56 }} was detected on {{ host.name SRVNIX05 }}\\n- The file was initially downloaded as a zip archive and extracted to /home/ubuntu/\\n- The malware, identified as Linux.Trojan.BPFDoor, was then copied to /dev/shm/kdmtmpflush and executed\\n- This trojan allows remote attackers to gain backdoor access to the compromised Linux system\\n- The malware was executed with root privileges, indicating a serious compromise\\n- Network connections and other malicious activities from this backdoor should be investigated",\n "entitySummaryMarkdown": "{{ host.name SRVNIX05 }} compromised by Linux.Trojan.BPFDoor malware executed as {{ user.name root }}",\n "mitreAttackTactics": [\n "Initial Access",\n "Execution",\n "Persistence"\n ],\n "summaryMarkdown": "Linux.Trojan.BPFDoor malware detected and executed on {{ host.name SRVNIX05 }} with root privileges, allowing remote backdoor access",\n "title": "Linux Trojan BPFDoor Backdoor Detected"\n },\n {\n "alertIds": [\n "5946b409f49b0983de53e575db0874ef11b0544766f816dc702941a69a9b0dd1",\n "aa0ba23872c48a8ee761591c5bb0a9ed8258c51b27111cc72dbe8624a0b7da96",\n "b60a5c344b579cab9406becdec14a11d56f4eccc2bf6caaf6eb72ddf1707124c",\n "4920ca19a22968e4ab0cf299974234699d9cce15545c401a2b8fd09d71f6e106",\n "26302b2afbe58c8dcfde950c7164262c626af0b85f0808f3d8632b1d6a406d16",\n "3aba59cd449be763e5b50ab954e39936ab3035be36010810e340e277b5670017",\n "41564c953dd101b942537110d175d2b269959c24dbf5b7c482e32851ab6f5dc1",\n "12e102970920f5f938b21effb09394c00540075fc4057ec79e221046a8b6ba0f"\n ],\n "detailsMarkdown": "- At {{ kibana.alert.original_time 2024-05-16T18:50:33.570Z }}, suspicious activity was detected on {{ host.name SRVMAC08 }}\\n- A malicious application \\"My Go Application.app\\" was executed, likely masquerading as a legitimate program\\n- The malware attempted to access the user\'s keychain to steal credentials\\n- It executed a file named {{ file.name unix1 }} which tried to access {{ file.path /Users/james/library/Keychains/login.keychain-db }}\\n- The malware also attempted to display a fake system preferences dialog to phish the user\'s password\\n- This attack targeted {{ user.name james }}, who has high user criticality",\n "entitySummaryMarkdown": "{{ host.name SRVMAC08 }} infected with malware targeting {{ user.name james }}\'s credentials",\n "mitreAttackTactics": [\n "Initial Access",\n "Execution",\n "Credential Access" \n ],\n "summaryMarkdown": "Malware on {{ host.name SRVMAC08 }} attempted to steal keychain credentials and phish password from {{ user.name james }}",\n "title": "macOS Credential Theft Attempt"\n },\n {\n "alertIds": [\n "a492cd3202717d0c86f9b44623b12ac4d19855722e0fadb2f84a547afb45871a",\n "7fdf3a399b0a6df74784f478c2712a0e47ff997f73701593b3a5a56fa452056f",\n "bf33e5f004b6f6f41e362f929b3fa16b5ea9ecbb0f6389acd17dfcfb67ff3ae9",\n "b6559664247c438f9cd15022feb87855253c3cef882cc52d2e064f2693977f1c",\n "636a5a24b810bf2dbc5e2417858ac218b1fadb598fa55676745f88c0509f3e48",\n "fc0f6f9939277cc4f526148c15813f5d48094e557fdcf0ba9e773b2a16ec8c2e",\n "0029a93e8f72dce05a22ca0cc5a5cd1ca8a29b93b3c8864f7623f10b98d79084",\n "67f41b973f82fc141d75fbbd1d6caba11066c19b2a1c720fcec9e681e1cfa60c",\n "79774ae772225e94b6183f5ea394572ebe24452be99100bab145173c57c73d3b"\n ],\n "detailsMarkdown": "- At {{ kibana.alert.original_time 2024-05-16T18:49:54.836Z }}, malicious activity was detected on {{ host.name SRVWIN01 }}\\n- An Excel file was used to drop and execute malware\\n- The malware used certutil.exe to decode a malicious payload\\n- A suspicious executable {{ file.name Q3C7N1V8.exe }} was created in C:\\\\ProgramData\\\\\\n- The malware established persistence by modifying registry run keys\\n- It then executed a DLL {{ file.name cdnver.dll }} using rundll32.exe\\n- This attack chain indicates a sophisticated malware infection, likely part of an ongoing attack campaign",\n "entitySummaryMarkdown": "{{ host.name SRVWIN01 }} infected via malicious Excel file executed by {{ user.name Administrator }}",\n "mitreAttackTactics": [\n "Initial Access", \n "Execution",\n "Persistence",\n "Defense Evasion"\n ],\n "summaryMarkdown": "Sophisticated malware infection on {{ host.name SRVWIN01 }} via malicious Excel file, establishing persistence and executing additional payloads",\n "title": "Excel-based Malware Infection Chain"\n },\n {\n "alertIds": [\n "801ec41afa5f05a7cafefe4eaff87be1f9eb7ecbfcfc501bd83a12f19e742be0",\n "eafd7577e1d88b2c4fc3d0e3eb54b2a315f79996f075ba3c57d6f2ae7181c53b",\n "eb8fee0ceacc8caec4757e95ec132a42bae4ba7841126ce9616873e01e806ddf",\n "69dcd5e48424cc8a04a965f5bec7539c8221ac556a7b93c531cdc7e02b58c191",\n "6c81da91ad4ec313c5a4aa970e1fdf7c3ee6dbfa8536c734bd12c72f1abe3a09",\n "584d904ea196623eb794df40565797656e24d05a707638447b5e53c05d520510",\n "46d05beb516dae1ad2f168084cdeb5bfd35ac1b1194bd65aa1c837fb3b77c21d",\n "c79fe367d985d9a5d9ee723ce94977b88fe1bbb3ec8e2ffbb7b3ee134d6b49ef",\n "3ef6baa7c7c99cad5b7832e6a778a7d1ea2d88729a3e50fbf2b821d0e57f2740",\n "1fbe36af64b587d7604812f6a248754cfe8c1d80b0551046c1fc95640d0ba538",\n "4451f6a45edc2d90f85717925071457e88dd41d0ee3d3c377f5721a254651513",\n "7ec9f53a2c4571325476ad2f4de3d2ecb49609b35a4a30a33d8d57e815d09f52",\n "ca57fd3a83e06419ce8299eefd3c783bd3d33b46ce47ffd27e2abdcb2b3e0955"\n ],\n "detailsMarkdown": "- At {{ kibana.alert.original_time 2024-05-16T18:50:14.847Z }}, a malicious OneNote file was opened on {{ host.name SRVWIN04 }}\\n- The OneNote file executed an embedded HTA file using mshta.exe\\n- The HTA file then downloaded additional malware using curl.exe\\n- A suspicious DLL {{ file.path C:\\\\ProgramData\\\\121.png }} was loaded using rundll32.exe\\n- The malware injected shellcode into legitimate Windows processes like AtBroker.exe\\n- Memory scans detected signatures matching the Qbot banking trojan\\n- The malware established persistence by modifying registry run keys\\n- It also performed domain trust enumeration, indicating potential lateral movement preparation\\n- This sophisticated attack chain suggests a targeted intrusion by an advanced threat actor",\n "entitySummaryMarkdown": "{{ host.name SRVWIN04 }} compromised via malicious OneNote file opened by {{ user.name Administrator }}",\n "mitreAttackTactics": [\n "Initial Access",\n "Execution", \n "Persistence",\n "Defense Evasion",\n "Discovery"\n ],\n "summaryMarkdown": "Sophisticated malware infection on {{ host.name SRVWIN04 }} via OneNote file, downloading Qbot trojan and preparing for potential lateral movement",\n "title": "OneNote-based Qbot Infection Chain"\n },\n {\n "alertIds": [\n "7150ee5a9571c6028573bf7d9c2ed0da15c3387ee3c8f668741799496f7b4ae9",\n "6053ca3481a9307d3a8626fe055357541bb53d97f5deb1b7b346ec86441c335b",\n "d9c3908a4ac46b90270e6aab8217ab6385a114574931026f1df8cfc930260ff6",\n "ea99e1633177f0c82e5126d4c999db2128c3adac6af4c7f4f183abc44486f070",\n "f045dc2a57582944b6e198e685e98bf02f86b5eb23ddbbdbb015c8568867122c",\n "171fe0490d48e9cac6f5b46aec7bfa67f3ecb96af308027018ca881bae1ce5d7",\n "0e22ea9514fd663a3841a212b19736fd1579c301d80f4838f25adeec24de4cf6",\n "9d8fdb59213e5a950d93253f9f986c730c877a70493c4f47ad0de52ef50c42f1"\n ],\n "detailsMarkdown": "- At {{ kibana.alert.original_time 2024-05-16T18:49:58.609Z }}, a malicious executable was run on {{ host.name SRVWIN02 }}\\n- The malware injected shellcode into the legitimate MsMpEng.exe (Windows Defender) process\\n- Memory scans detected signatures matching the Sodinokibi (REvil) ransomware\\n- The malware created ransom notes and began encrypting files\\n- It also attempted to enable network discovery, likely to spread to other systems\\n- This indicates an active ransomware infection that could quickly spread across the network",\n "entitySummaryMarkdown": "{{ host.name SRVWIN02 }} infected with Sodinokibi ransomware executed by {{ user.name Administrator }}",\n "mitreAttackTactics": [\n "Execution",\n "Defense Evasion",\n "Impact"\n ],\n "summaryMarkdown": "Sodinokibi (REvil) ransomware detected on {{ host.name SRVWIN02 }}, actively encrypting files and attempting to spread",\n "title": "Active Sodinokibi Ransomware Infection"\n },\n {\n "alertIds": [\n "6f8e71d59956c6dbed5c88986cdafd4386684e3879085b2742e1f2d38b282066",\n "c13b78fbfef05ddc81c73b436ccb5288d8cd52a46175638b1b3b0d311f8b53e8",\n "b0f3d3f5bfc0b1d1f3c7e219ee44dc225fa26cafd40697073a636b44cf6054ad"\n ],\n "detailsMarkdown": "- At {{ kibana.alert.original_time 2024-05-16T18:50:22.077Z }}, suspicious activity was detected on {{ host.name SRVWIN06 }}\\n- The msiexec.exe process spawned an unusual PowerShell child process\\n- The PowerShell process executed a script from a suspicious temporary directory\\n- Memory scans of the PowerShell process detected signatures matching the Bumblebee malware loader\\n- Bumblebee is known to be used by multiple ransomware groups as an initial access vector\\n- This indicates a likely ongoing attack attempting to deploy additional malware or ransomware",\n "entitySummaryMarkdown": "{{ host.name SRVWIN06 }} infected with Bumblebee malware loader via {{ user.name Administrator }}",\n "mitreAttackTactics": [\n "Execution",\n "Defense Evasion"\n ],\n "summaryMarkdown": "Bumblebee malware loader detected on {{ host.name SRVWIN06 }}, likely attempting to deploy additional payloads",\n "title": "Bumblebee Malware Loader Detected"\n },\n {\n "alertIds": [\n "f629babc51c3628517d8a7e1f0662124ee41e4328b1dbcf72dc3fc6f2e410d33",\n "627d00600f803366edb83700b546a4bf486e2990ac7140d842e898eb6e298e83",\n "6181847506974ed4458f03b60919c4a306197b5cb040ab324d2d1f6d0ca5bde1",\n "3aba59cd449be763e5b50ab954e39936ab3035be36010810e340e277b5670017",\n "df26b2d23068b77fdc001ea44f46505a259f02ceccc9fa0b2401c5e35190e710",\n "9c038ff779bd0ff514a1ff2b55caa359189d8bcebc48c6ac14a789946e87eaed"\n ],\n "detailsMarkdown": "- At {{ kibana.alert.original_time 2024-05-16T18:50:27.839Z }}, a malicious Word document was opened on {{ host.name SRVWIN07 }}\\n- The document spawned wscript.exe to execute a malicious VBS script\\n- The VBS script then launched a PowerShell process with suspicious arguments\\n- PowerShell was used to create a scheduled task for persistence\\n- This attack chain indicates a likely attempt to establish a foothold for further malicious activities",\n "entitySummaryMarkdown": "{{ host.name SRVWIN07 }} compromised via malicious Word document opened by {{ user.name Administrator }}",\n "mitreAttackTactics": [\n "Initial Access",\n "Execution",\n "Persistence"\n ],\n "summaryMarkdown": "Malicious Word document on {{ host.name SRVWIN07 }} led to execution of VBS and PowerShell scripts, establishing persistence via scheduled task",\n "title": "Malicious Document Leads to Persistence"\n }\n ]\n}'; - - const expected = - '{\n "insights": [\n {\n "alertIds": [\n "d776c8406fd81427b1f166550ac1b949017da7a13dc734594e4b05f24622b26e",\n "504c012054cfe91986311b4e6bc8523914434fab590e5c07c0328fab6566753c",\n "b706b8c19e68cc4f54b69f0a93e32b10f4102b610213b7826fb1d303b90a0536",\n "7763ebe716c47f64987362a9fb120d73873c77d26ad915f2c3d57c5dd3b7eed0",\n "25c61e0423a9bfd7f268ca6e9b67d4f507207c0cb1e1b4701aa5248cb3866f1f",\n "ea99e1633177f0c82e5126d4c999db2128c3adac6af4c7f4f183abc44486f070"\n ],\n "detailsMarkdown": "- At {{ kibana.alert.original_time 2024-05-16T18:50:17.566Z }}, a malicious file with SHA256 hash {{ file.hash.sha256 74ef6cc38f5a1a80148752b63c117e6846984debd2af806c65887195a8eccc56 }} was detected on {{ host.name SRVNIX05 }}\\n- The file was initially downloaded as a zip archive and extracted to /home/ubuntu/\\n- The malware, identified as Linux.Trojan.BPFDoor, was then copied to /dev/shm/kdmtmpflush and executed\\n- This trojan allows remote attackers to gain backdoor access to the compromised Linux system\\n- The malware was executed with root privileges, indicating a serious compromise\\n- Network connections and other malicious activities from this backdoor should be investigated",\n "entitySummaryMarkdown": "{{ host.name SRVNIX05 }} compromised by Linux.Trojan.BPFDoor malware executed as {{ user.name root }}",\n "mitreAttackTactics": [\n "Initial Access",\n "Execution",\n "Persistence"\n ],\n "summaryMarkdown": "Linux.Trojan.BPFDoor malware detected and executed on {{ host.name SRVNIX05 }} with root privileges, allowing remote backdoor access",\n "title": "Linux Trojan BPFDoor Backdoor Detected"\n },\n {\n "alertIds": [\n "5946b409f49b0983de53e575db0874ef11b0544766f816dc702941a69a9b0dd1",\n "aa0ba23872c48a8ee761591c5bb0a9ed8258c51b27111cc72dbe8624a0b7da96",\n "b60a5c344b579cab9406becdec14a11d56f4eccc2bf6caaf6eb72ddf1707124c",\n "4920ca19a22968e4ab0cf299974234699d9cce15545c401a2b8fd09d71f6e106",\n "26302b2afbe58c8dcfde950c7164262c626af0b85f0808f3d8632b1d6a406d16",\n "3aba59cd449be763e5b50ab954e39936ab3035be36010810e340e277b5670017",\n "41564c953dd101b942537110d175d2b269959c24dbf5b7c482e32851ab6f5dc1",\n "12e102970920f5f938b21effb09394c00540075fc4057ec79e221046a8b6ba0f"\n ],\n "detailsMarkdown": "- At {{ kibana.alert.original_time 2024-05-16T18:50:33.570Z }}, suspicious activity was detected on {{ host.name SRVMAC08 }}\\n- A malicious application \\"My Go Application.app\\" was executed, likely masquerading as a legitimate program\\n- The malware attempted to access the user\'s keychain to steal credentials\\n- It executed a file named {{ file.name unix1 }} which tried to access {{ file.path /Users/james/library/Keychains/login.keychain-db }}\\n- The malware also attempted to display a fake system preferences dialog to phish the user\'s password\\n- This attack targeted {{ user.name james }}, who has high user criticality",\n "entitySummaryMarkdown": "{{ host.name SRVMAC08 }} infected with malware targeting {{ user.name james }}\'s credentials",\n "mitreAttackTactics": [\n "Initial Access",\n "Execution",\n "Credential Access" \n ],\n "summaryMarkdown": "Malware on {{ host.name SRVMAC08 }} attempted to steal keychain credentials and phish password from {{ user.name james }}",\n "title": "macOS Credential Theft Attempt"\n },\n {\n "alertIds": [\n "a492cd3202717d0c86f9b44623b12ac4d19855722e0fadb2f84a547afb45871a",\n "7fdf3a399b0a6df74784f478c2712a0e47ff997f73701593b3a5a56fa452056f",\n "bf33e5f004b6f6f41e362f929b3fa16b5ea9ecbb0f6389acd17dfcfb67ff3ae9",\n "b6559664247c438f9cd15022feb87855253c3cef882cc52d2e064f2693977f1c",\n "636a5a24b810bf2dbc5e2417858ac218b1fadb598fa55676745f88c0509f3e48",\n "fc0f6f9939277cc4f526148c15813f5d48094e557fdcf0ba9e773b2a16ec8c2e",\n "0029a93e8f72dce05a22ca0cc5a5cd1ca8a29b93b3c8864f7623f10b98d79084",\n "67f41b973f82fc141d75fbbd1d6caba11066c19b2a1c720fcec9e681e1cfa60c",\n "79774ae772225e94b6183f5ea394572ebe24452be99100bab145173c57c73d3b"\n ],\n "detailsMarkdown": "- At {{ kibana.alert.original_time 2024-05-16T18:49:54.836Z }}, malicious activity was detected on {{ host.name SRVWIN01 }}\\n- An Excel file was used to drop and execute malware\\n- The malware used certutil.exe to decode a malicious payload\\n- A suspicious executable {{ file.name Q3C7N1V8.exe }} was created in C:\\\\ProgramData\\\\\\n- The malware established persistence by modifying registry run keys\\n- It then executed a DLL {{ file.name cdnver.dll }} using rundll32.exe\\n- This attack chain indicates a sophisticated malware infection, likely part of an ongoing attack campaign",\n "entitySummaryMarkdown": "{{ host.name SRVWIN01 }} infected via malicious Excel file executed by {{ user.name Administrator }}",\n "mitreAttackTactics": [\n "Initial Access", \n "Execution",\n "Persistence",\n "Defense Evasion"\n ],\n "summaryMarkdown": "Sophisticated malware infection on {{ host.name SRVWIN01 }} via malicious Excel file, establishing persistence and executing additional payloads",\n "title": "Excel-based Malware Infection Chain"\n },\n {\n "alertIds": [\n "801ec41afa5f05a7cafefe4eaff87be1f9eb7ecbfcfc501bd83a12f19e742be0",\n "eafd7577e1d88b2c4fc3d0e3eb54b2a315f79996f075ba3c57d6f2ae7181c53b",\n "eb8fee0ceacc8caec4757e95ec132a42bae4ba7841126ce9616873e01e806ddf",\n "69dcd5e48424cc8a04a965f5bec7539c8221ac556a7b93c531cdc7e02b58c191",\n "6c81da91ad4ec313c5a4aa970e1fdf7c3ee6dbfa8536c734bd12c72f1abe3a09",\n "584d904ea196623eb794df40565797656e24d05a707638447b5e53c05d520510",\n "46d05beb516dae1ad2f168084cdeb5bfd35ac1b1194bd65aa1c837fb3b77c21d",\n "c79fe367d985d9a5d9ee723ce94977b88fe1bbb3ec8e2ffbb7b3ee134d6b49ef",\n "3ef6baa7c7c99cad5b7832e6a778a7d1ea2d88729a3e50fbf2b821d0e57f2740",\n "1fbe36af64b587d7604812f6a248754cfe8c1d80b0551046c1fc95640d0ba538",\n "4451f6a45edc2d90f85717925071457e88dd41d0ee3d3c377f5721a254651513",\n "7ec9f53a2c4571325476ad2f4de3d2ecb49609b35a4a30a33d8d57e815d09f52",\n "ca57fd3a83e06419ce8299eefd3c783bd3d33b46ce47ffd27e2abdcb2b3e0955"\n ],\n "detailsMarkdown": "- At {{ kibana.alert.original_time 2024-05-16T18:50:14.847Z }}, a malicious OneNote file was opened on {{ host.name SRVWIN04 }}\\n- The OneNote file executed an embedded HTA file using mshta.exe\\n- The HTA file then downloaded additional malware using curl.exe\\n- A suspicious DLL {{ file.path C:\\\\ProgramData\\\\121.png }} was loaded using rundll32.exe\\n- The malware injected shellcode into legitimate Windows processes like AtBroker.exe\\n- Memory scans detected signatures matching the Qbot banking trojan\\n- The malware established persistence by modifying registry run keys\\n- It also performed domain trust enumeration, indicating potential lateral movement preparation\\n- This sophisticated attack chain suggests a targeted intrusion by an advanced threat actor",\n "entitySummaryMarkdown": "{{ host.name SRVWIN04 }} compromised via malicious OneNote file opened by {{ user.name Administrator }}",\n "mitreAttackTactics": [\n "Initial Access",\n "Execution", \n "Persistence",\n "Defense Evasion",\n "Discovery"\n ],\n "summaryMarkdown": "Sophisticated malware infection on {{ host.name SRVWIN04 }} via OneNote file, downloading Qbot trojan and preparing for potential lateral movement",\n "title": "OneNote-based Qbot Infection Chain"\n },\n {\n "alertIds": [\n "7150ee5a9571c6028573bf7d9c2ed0da15c3387ee3c8f668741799496f7b4ae9",\n "6053ca3481a9307d3a8626fe055357541bb53d97f5deb1b7b346ec86441c335b",\n "d9c3908a4ac46b90270e6aab8217ab6385a114574931026f1df8cfc930260ff6",\n "ea99e1633177f0c82e5126d4c999db2128c3adac6af4c7f4f183abc44486f070",\n "f045dc2a57582944b6e198e685e98bf02f86b5eb23ddbbdbb015c8568867122c",\n "171fe0490d48e9cac6f5b46aec7bfa67f3ecb96af308027018ca881bae1ce5d7",\n "0e22ea9514fd663a3841a212b19736fd1579c301d80f4838f25adeec24de4cf6",\n "9d8fdb59213e5a950d93253f9f986c730c877a70493c4f47ad0de52ef50c42f1"\n ],\n "detailsMarkdown": "- At {{ kibana.alert.original_time 2024-05-16T18:49:58.609Z }}, a malicious executable was run on {{ host.name SRVWIN02 }}\\n- The malware injected shellcode into the legitimate MsMpEng.exe (Windows Defender) process\\n- Memory scans detected signatures matching the Sodinokibi (REvil) ransomware\\n- The malware created ransom notes and began encrypting files\\n- It also attempted to enable network discovery, likely to spread to other systems\\n- This indicates an active ransomware infection that could quickly spread across the network",\n "entitySummaryMarkdown": "{{ host.name SRVWIN02 }} infected with Sodinokibi ransomware executed by {{ user.name Administrator }}",\n "mitreAttackTactics": [\n "Execution",\n "Defense Evasion",\n "Impact"\n ],\n "summaryMarkdown": "Sodinokibi (REvil) ransomware detected on {{ host.name SRVWIN02 }}, actively encrypting files and attempting to spread",\n "title": "Active Sodinokibi Ransomware Infection"\n },\n {\n "alertIds": [\n "6f8e71d59956c6dbed5c88986cdafd4386684e3879085b2742e1f2d38b282066",\n "c13b78fbfef05ddc81c73b436ccb5288d8cd52a46175638b1b3b0d311f8b53e8",\n "b0f3d3f5bfc0b1d1f3c7e219ee44dc225fa26cafd40697073a636b44cf6054ad"\n ],\n "detailsMarkdown": "- At {{ kibana.alert.original_time 2024-05-16T18:50:22.077Z }}, suspicious activity was detected on {{ host.name SRVWIN06 }}\\n- The msiexec.exe process spawned an unusual PowerShell child process\\n- The PowerShell process executed a script from a suspicious temporary directory\\n- Memory scans of the PowerShell process detected signatures matching the Bumblebee malware loader\\n- Bumblebee is known to be used by multiple ransomware groups as an initial access vector\\n- This indicates a likely ongoing attack attempting to deploy additional malware or ransomware",\n "entitySummaryMarkdown": "{{ host.name SRVWIN06 }} infected with Bumblebee malware loader via {{ user.name Administrator }}",\n "mitreAttackTactics": [\n "Execution",\n "Defense Evasion"\n ],\n "summaryMarkdown": "Bumblebee malware loader detected on {{ host.name SRVWIN06 }}, likely attempting to deploy additional payloads",\n "title": "Bumblebee Malware Loader Detected"\n },\n {\n "alertIds": [\n "f629babc51c3628517d8a7e1f0662124ee41e4328b1dbcf72dc3fc6f2e410d33",\n "627d00600f803366edb83700b546a4bf486e2990ac7140d842e898eb6e298e83",\n "6181847506974ed4458f03b60919c4a306197b5cb040ab324d2d1f6d0ca5bde1",\n "3aba59cd449be763e5b50ab954e39936ab3035be36010810e340e277b5670017",\n "df26b2d23068b77fdc001ea44f46505a259f02ceccc9fa0b2401c5e35190e710",\n "9c038ff779bd0ff514a1ff2b55caa359189d8bcebc48c6ac14a789946e87eaed"\n ],\n "detailsMarkdown": "- At {{ kibana.alert.original_time 2024-05-16T18:50:27.839Z }}, a malicious Word document was opened on {{ host.name SRVWIN07 }}\\n- The document spawned wscript.exe to execute a malicious VBS script\\n- The VBS script then launched a PowerShell process with suspicious arguments\\n- PowerShell was used to create a scheduled task for persistence\\n- This attack chain indicates a likely attempt to establish a foothold for further malicious activities",\n "entitySummaryMarkdown": "{{ host.name SRVWIN07 }} compromised via malicious Word document opened by {{ user.name Administrator }}",\n "mitreAttackTactics": [\n "Initial Access",\n "Execution",\n "Persistence"\n ],\n "summaryMarkdown": "Malicious Word document on {{ host.name SRVWIN07 }} led to execution of VBS and PowerShell scripts, establishing persistence via scheduled task",\n "title": "Malicious Document Leads to Persistence"\n }\n ]\n}'; - - expect(extractJson(input)).toBe(expected); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/extract_json/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/extract_json/index.ts deleted file mode 100644 index 79d3f9c0d0599..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/extract_json/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const extractJson = (input: string): string => { - const regex = /```json\s*([\s\S]*?)(?:\s*```|$)/; - const match = input.match(regex); - - if (match && match[1]) { - return match[1].trim(); - } - - return input; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/generations_are_repeating/index.test.tsx b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/generations_are_repeating/index.test.tsx deleted file mode 100644 index 7d6db4dd72dfd..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/generations_are_repeating/index.test.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { generationsAreRepeating } from '.'; - -describe('getIsGenerationRepeating', () => { - it('returns true when all previous generations are the same as the current generation', () => { - const result = generationsAreRepeating({ - currentGeneration: 'gen1', - previousGenerations: ['gen1', 'gen1', 'gen1'], // <-- all the same, length 3 - sampleLastNGenerations: 3, - }); - - expect(result).toBe(true); - }); - - it('returns false when some of the previous generations are NOT the same as the current generation', () => { - const result = generationsAreRepeating({ - currentGeneration: 'gen1', - previousGenerations: ['gen1', 'gen2', 'gen1'], // <-- some are different, length 3 - sampleLastNGenerations: 3, - }); - - expect(result).toBe(false); - }); - - it('returns true when all *sampled* generations are the same as the current generation, and there are older samples past the last N', () => { - const result = generationsAreRepeating({ - currentGeneration: 'gen1', - previousGenerations: [ - 'gen2', // <-- older sample will be ignored - 'gen1', - 'gen1', - 'gen1', - ], - sampleLastNGenerations: 3, - }); - - expect(result).toBe(true); - }); - - it('returns false when some of the *sampled* generations are NOT the same as the current generation, and there are additional samples past the last N', () => { - const result = generationsAreRepeating({ - currentGeneration: 'gen1', - previousGenerations: [ - 'gen1', // <-- older sample will be ignored - 'gen1', - 'gen1', - 'gen2', - ], - sampleLastNGenerations: 3, - }); - - expect(result).toBe(false); - }); - - it('returns false when sampling fewer generations than sampleLastNGenerations, and all are the same as the current generation', () => { - const result = generationsAreRepeating({ - currentGeneration: 'gen1', - previousGenerations: ['gen1', 'gen1'], // <-- same, but only 2 generations - sampleLastNGenerations: 3, - }); - - expect(result).toBe(false); - }); - - it('returns false when sampling fewer generations than sampleLastNGenerations, and some are different from the current generation', () => { - const result = generationsAreRepeating({ - currentGeneration: 'gen1', - previousGenerations: ['gen1', 'gen2'], // <-- different, but only 2 generations - sampleLastNGenerations: 3, - }); - - expect(result).toBe(false); - }); - - it('returns false when there are no previous generations to sample', () => { - const result = generationsAreRepeating({ - currentGeneration: 'gen1', - previousGenerations: [], - sampleLastNGenerations: 3, - }); - - expect(result).toBe(false); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/generations_are_repeating/index.tsx b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/generations_are_repeating/index.tsx deleted file mode 100644 index 6cc9cd86c9d2f..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/generations_are_repeating/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** Returns true if the last n generations are repeating the same output */ -export const generationsAreRepeating = ({ - currentGeneration, - previousGenerations, - sampleLastNGenerations, -}: { - currentGeneration: string; - previousGenerations: string[]; - sampleLastNGenerations: number; -}): boolean => { - const generationsToSample = previousGenerations.slice(-sampleLastNGenerations); - - if (generationsToSample.length < sampleLastNGenerations) { - return false; // Not enough generations to sample - } - - return generationsToSample.every((generation) => generation === currentGeneration); -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_chain_with_format_instructions/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_chain_with_format_instructions/index.ts deleted file mode 100644 index 7eacaad1d7e39..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_chain_with_format_instructions/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ActionsClientLlm } from '@kbn/langchain/server'; -import { ChatPromptTemplate } from '@langchain/core/prompts'; -import { Runnable } from '@langchain/core/runnables'; - -import { getOutputParser } from '../get_output_parser'; - -interface GetChainWithFormatInstructions { - chain: Runnable; - formatInstructions: string; - llmType: string; -} - -export const getChainWithFormatInstructions = ( - llm: ActionsClientLlm -): GetChainWithFormatInstructions => { - const outputParser = getOutputParser(); - const formatInstructions = outputParser.getFormatInstructions(); - - const prompt = ChatPromptTemplate.fromTemplate( - `Answer the user's question as best you can:\n{format_instructions}\n{query}` - ); - - const chain = prompt.pipe(llm); - const llmType = llm._llmType(); - - return { chain, formatInstructions, llmType }; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_combined/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_combined/index.ts deleted file mode 100644 index 10b5c323891a1..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_combined/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const getCombined = ({ - combinedGenerations, - partialResponse, -}: { - combinedGenerations: string; - partialResponse: string; -}): string => `${combinedGenerations}${partialResponse}`; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_combined_attack_discovery_prompt/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_combined_attack_discovery_prompt/index.ts deleted file mode 100644 index 4c9ac71f8310c..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_combined_attack_discovery_prompt/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isEmpty } from 'lodash/fp'; - -import { getAlertsContextPrompt } from '../../generate/helpers/get_alerts_context_prompt'; -import { getContinuePrompt } from '../get_continue_prompt'; - -/** - * Returns the the initial query, or the initial query combined with a - * continuation prompt and partial results - */ -export const getCombinedAttackDiscoveryPrompt = ({ - anonymizedAlerts, - attackDiscoveryPrompt, - combinedMaybePartialResults, -}: { - anonymizedAlerts: string[]; - attackDiscoveryPrompt: string; - /** combined results that may contain incomplete JSON */ - combinedMaybePartialResults: string; -}): string => { - const alertsContextPrompt = getAlertsContextPrompt({ - anonymizedAlerts, - attackDiscoveryPrompt, - }); - - return isEmpty(combinedMaybePartialResults) - ? alertsContextPrompt // no partial results yet - : `${alertsContextPrompt} - -${getContinuePrompt()} - -""" -${combinedMaybePartialResults} -""" - -`; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_continue_prompt/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_continue_prompt/index.ts deleted file mode 100644 index 628ba0531332c..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_continue_prompt/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const getContinuePrompt = - (): string => `Continue exactly where you left off in the JSON output below, generating only the additional JSON output when it's required to complete your work. The additional JSON output MUST ALWAYS follow these rules: -1) it MUST conform to the schema above, because it will be checked against the JSON schema -2) it MUST escape all JSON special characters (i.e. backslashes, double quotes, newlines, tabs, carriage returns, backspaces, and form feeds), because it will be parsed as JSON -3) it MUST NOT repeat any the previous output, because that would prevent partial results from being combined -4) it MUST NOT restart from the beginning, because that would prevent partial results from being combined -5) it MUST NOT be prefixed or suffixed with additional text outside of the JSON, because that would prevent it from being combined and parsed as JSON: -`; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_default_attack_discovery_prompt/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_default_attack_discovery_prompt/index.ts deleted file mode 100644 index 25bace13d40c8..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_default_attack_discovery_prompt/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const getDefaultAttackDiscoveryPrompt = (): string => - "You are a cyber security analyst tasked with analyzing security events from Elastic Security to identify and report on potential cyber attacks or progressions. Your report should focus on high-risk incidents that could severely impact the organization, rather than isolated alerts. Present your findings in a way that can be easily understood by anyone, regardless of their technical expertise, as if you were briefing the CISO. Break down your response into sections based on timing, hosts, and users involved. When correlating alerts, use kibana.alert.original_time when it's available, otherwise use @timestamp. Include appropriate context about the affected hosts and users. Describe how the attack progression might have occurred and, if feasible, attribute it to known threat groups. Prioritize high and critical alerts, but include lower-severity alerts if desired. In the description field, provide as much detail as possible, in a bulleted list explaining any attack progressions. Accuracy is of utmost importance. You MUST escape all JSON special characters (i.e. backslashes, double quotes, newlines, tabs, carriage returns, backspaces, and form feeds)."; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_output_parser/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_output_parser/index.test.ts deleted file mode 100644 index 569c8cf4e04a5..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_output_parser/index.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { getOutputParser } from '.'; - -describe('getOutputParser', () => { - it('returns a structured output parser with the expected format instructions', () => { - const outputParser = getOutputParser(); - - const expected = `You must format your output as a JSON value that adheres to a given \"JSON Schema\" instance. - -\"JSON Schema\" is a declarative language that allows you to annotate and validate JSON documents. - -For example, the example \"JSON Schema\" instance {{\"properties\": {{\"foo\": {{\"description\": \"a list of test words\", \"type\": \"array\", \"items\": {{\"type\": \"string\"}}}}}}, \"required\": [\"foo\"]}}}} -would match an object with one required property, \"foo\". The \"type\" property specifies \"foo\" must be an \"array\", and the \"description\" property semantically describes it as \"a list of test words\". The items within \"foo\" must be strings. -Thus, the object {{\"foo\": [\"bar\", \"baz\"]}} is a well-formatted instance of this example \"JSON Schema\". The object {{\"properties\": {{\"foo\": [\"bar\", \"baz\"]}}}} is not well-formatted. - -Your output will be parsed and type-checked according to the provided schema instance, so make sure all fields in your output match the schema exactly and there are no trailing commas! - -Here is the JSON Schema instance your output must adhere to. Include the enclosing markdown codeblock: -\`\`\`json -{"type":"object","properties":{"insights":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"alertIds\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"The alert IDs that the insight is based on.\"},\"detailsMarkdown\":{\"type\":\"string\",\"description\":\"A detailed insight with markdown, where each markdown bullet contains a description of what happened that reads like a story of the attack as it played out and always uses special {{ field.name fieldValue1 fieldValue2 fieldValueN }} syntax for field names and values from the source data. Examples of CORRECT syntax (includes field names and values): {{ host.name hostNameValue }} {{ user.name userNameValue }} {{ source.ip sourceIpValue }} Examples of INCORRECT syntax (bad, because the field names are not included): {{ hostNameValue }} {{ userNameValue }} {{ sourceIpValue }}\"},\"entitySummaryMarkdown\":{\"type\":\"string\",\"description\":\"A short (no more than a sentence) summary of the insight featuring only the host.name and user.name fields (when they are applicable), using the same {{ field.name fieldValue1 fieldValue2 fieldValueN }} syntax\"},\"mitreAttackTactics\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"An array of MITRE ATT&CK tactic for the insight, using one of the following values: Reconnaissance,Initial Access,Execution,Persistence,Privilege Escalation,Discovery,Lateral Movement,Command and Control,Exfiltration\"},\"summaryMarkdown\":{\"type\":\"string\",\"description\":\"A markdown summary of insight, using the same {{ field.name fieldValue1 fieldValue2 fieldValueN }} syntax\"},\"title\":{\"type\":\"string\",\"description\":\"A short, no more than 7 words, title for the insight, NOT formatted with special syntax or markdown. This must be as brief as possible.\"}},\"required\":[\"alertIds\",\"detailsMarkdown\",\"summaryMarkdown\",\"title\"],\"additionalProperties\":false},\"description\":\"Insights with markdown that always uses special {{ field.name fieldValue1 fieldValue2 fieldValueN }} syntax for field names and values from the source data. Examples of CORRECT syntax (includes field names and values): {{ host.name hostNameValue }} {{ user.name userNameValue }} {{ source.ip sourceIpValue }} Examples of INCORRECT syntax (bad, because the field names are not included): {{ hostNameValue }} {{ userNameValue }} {{ sourceIpValue }}\"}},\"required\":[\"insights\"],\"additionalProperties":false,\"$schema\":\"http://json-schema.org/draft-07/schema#\"} -\`\`\` -`; - - expect(outputParser.getFormatInstructions()).toEqual(expected); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_output_parser/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_output_parser/index.ts deleted file mode 100644 index 2ca0d72b63eb4..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/get_output_parser/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { StructuredOutputParser } from 'langchain/output_parsers'; - -import { AttackDiscoveriesGenerationSchema } from '../../generate/schema'; - -export const getOutputParser = () => - StructuredOutputParser.fromZodSchema(AttackDiscoveriesGenerationSchema); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/parse_combined_or_throw/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/parse_combined_or_throw/index.ts deleted file mode 100644 index 3f7a0a9d802b3..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/parse_combined_or_throw/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Logger } from '@kbn/core/server'; -import type { AttackDiscovery } from '@kbn/elastic-assistant-common'; - -import { addTrailingBackticksIfNecessary } from '../add_trailing_backticks_if_necessary'; -import { extractJson } from '../extract_json'; -import { AttackDiscoveriesGenerationSchema } from '../../generate/schema'; - -export const parseCombinedOrThrow = ({ - combinedResponse, - generationAttempts, - llmType, - logger, - nodeName, -}: { - /** combined responses that maybe valid JSON */ - combinedResponse: string; - generationAttempts: number; - nodeName: string; - llmType: string; - logger?: Logger; -}): AttackDiscovery[] => { - const timestamp = new Date().toISOString(); - - const extractedJson = extractJson(addTrailingBackticksIfNecessary(combinedResponse)); - - logger?.debug( - () => - `${nodeName} node is parsing extractedJson (${llmType}) from attempt ${generationAttempts}` - ); - - const unvalidatedParsed = JSON.parse(extractedJson); - - logger?.debug( - () => - `${nodeName} node is validating combined response (${llmType}) from attempt ${generationAttempts}` - ); - - const validatedResponse = AttackDiscoveriesGenerationSchema.parse(unvalidatedParsed); - - logger?.debug( - () => - `${nodeName} node successfully validated Attack discoveries response (${llmType}) from attempt ${generationAttempts}` - ); - - return [...validatedResponse.insights.map((insight) => ({ ...insight, timestamp }))]; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/response_is_hallucinated/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/response_is_hallucinated/index.ts deleted file mode 100644 index f938f6436db98..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/helpers/response_is_hallucinated/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const responseIsHallucinated = (result: string): boolean => - result.includes('{{ host.name hostNameValue }}'); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/refine/helpers/discard_previous_refinements/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/refine/helpers/discard_previous_refinements/index.ts deleted file mode 100644 index e642e598e73f0..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/refine/helpers/discard_previous_refinements/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { GraphState } from '../../../../types'; - -export const discardPreviousRefinements = ({ - generationAttempts, - hallucinationFailures, - isHallucinationDetected, - state, -}: { - generationAttempts: number; - hallucinationFailures: number; - isHallucinationDetected: boolean; - state: GraphState; -}): GraphState => { - return { - ...state, - combinedRefinements: '', // <-- reset the combined refinements - generationAttempts: generationAttempts + 1, - refinements: [], // <-- reset the refinements - hallucinationFailures: isHallucinationDetected - ? hallucinationFailures + 1 - : hallucinationFailures, - }; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/refine/helpers/get_combined_refine_prompt/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/refine/helpers/get_combined_refine_prompt/index.ts deleted file mode 100644 index 11ea40a48ae55..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/refine/helpers/get_combined_refine_prompt/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { AttackDiscovery } from '@kbn/elastic-assistant-common'; -import { isEmpty } from 'lodash/fp'; - -import { getContinuePrompt } from '../../../helpers/get_continue_prompt'; - -/** - * Returns a prompt that combines the initial query, a refine prompt, and partial results - */ -export const getCombinedRefinePrompt = ({ - attackDiscoveryPrompt, - combinedRefinements, - refinePrompt, - unrefinedResults, -}: { - attackDiscoveryPrompt: string; - combinedRefinements: string; - refinePrompt: string; - unrefinedResults: AttackDiscovery[] | null; -}): string => { - const baseQuery = `${attackDiscoveryPrompt} - -${refinePrompt} - -""" -${JSON.stringify(unrefinedResults, null, 2)} -""" - -`; - - return isEmpty(combinedRefinements) - ? baseQuery // no partial results yet - : `${baseQuery} - -${getContinuePrompt()} - -""" -${combinedRefinements} -""" - -`; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/refine/helpers/get_default_refine_prompt/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/refine/helpers/get_default_refine_prompt/index.ts deleted file mode 100644 index 5743316669785..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/refine/helpers/get_default_refine_prompt/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const getDefaultRefinePrompt = - (): string => `You previously generated the following insights, but sometimes they represent the same attack. - -Combine the insights below, when they represent the same attack; leave any insights that are not combined unchanged:`; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/refine/helpers/get_use_unrefined_results/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/refine/helpers/get_use_unrefined_results/index.ts deleted file mode 100644 index 13d0a2228a3ee..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/refine/helpers/get_use_unrefined_results/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** - * Note: the conditions tested here are different than the generate node - */ -export const getUseUnrefinedResults = ({ - maxHallucinationFailuresReached, - maxRetriesReached, -}: { - maxHallucinationFailuresReached: boolean; - maxRetriesReached: boolean; -}): boolean => maxRetriesReached || maxHallucinationFailuresReached; // we may have reached max halucination failures, but we still want to use the unrefined results diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/refine/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/refine/index.ts deleted file mode 100644 index 0c7987eef92bc..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/refine/index.ts +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ActionsClientLlm } from '@kbn/langchain/server'; -import type { Logger } from '@kbn/core/server'; - -import { discardPreviousRefinements } from './helpers/discard_previous_refinements'; -import { extractJson } from '../helpers/extract_json'; -import { getChainWithFormatInstructions } from '../helpers/get_chain_with_format_instructions'; -import { getCombined } from '../helpers/get_combined'; -import { getCombinedRefinePrompt } from './helpers/get_combined_refine_prompt'; -import { generationsAreRepeating } from '../helpers/generations_are_repeating'; -import { getMaxHallucinationFailuresReached } from '../../helpers/get_max_hallucination_failures_reached'; -import { getMaxRetriesReached } from '../../helpers/get_max_retries_reached'; -import { getUseUnrefinedResults } from './helpers/get_use_unrefined_results'; -import { parseCombinedOrThrow } from '../helpers/parse_combined_or_throw'; -import { responseIsHallucinated } from '../helpers/response_is_hallucinated'; -import type { GraphState } from '../../types'; - -export const getRefineNode = ({ - llm, - logger, -}: { - llm: ActionsClientLlm; - logger?: Logger; -}): ((state: GraphState) => Promise) => { - const refine = async (state: GraphState): Promise => { - logger?.debug(() => '---REFINE---'); - - const { - attackDiscoveryPrompt, - combinedRefinements, - generationAttempts, - hallucinationFailures, - maxGenerationAttempts, - maxHallucinationFailures, - maxRepeatedGenerations, - refinements, - refinePrompt, - unrefinedResults, - } = state; - - let combinedResponse = ''; // mutable, because it must be accessed in the catch block - let partialResponse = ''; // mutable, because it must be accessed in the catch block - - try { - const query = getCombinedRefinePrompt({ - attackDiscoveryPrompt, - combinedRefinements, - refinePrompt, - unrefinedResults, - }); - - const { chain, formatInstructions, llmType } = getChainWithFormatInstructions(llm); - - logger?.debug( - () => `refine node is invoking the chain (${llmType}), attempt ${generationAttempts}` - ); - - const rawResponse = (await chain.invoke({ - format_instructions: formatInstructions, - query, - })) as unknown as string; - - // LOCAL MUTATION: - partialResponse = extractJson(rawResponse); // remove the surrounding ```json``` - - // if the response is hallucinated, discard it: - if (responseIsHallucinated(partialResponse)) { - logger?.debug( - () => - `refine node detected a hallucination (${llmType}), on attempt ${generationAttempts}; discarding the accumulated refinements and starting over` - ); - - return discardPreviousRefinements({ - generationAttempts, - hallucinationFailures, - isHallucinationDetected: true, - state, - }); - } - - // if the refinements are repeating, discard previous refinements and start over: - if ( - generationsAreRepeating({ - currentGeneration: partialResponse, - previousGenerations: refinements, - sampleLastNGenerations: maxRepeatedGenerations, - }) - ) { - logger?.debug( - () => - `refine node detected (${llmType}), detected ${maxRepeatedGenerations} repeated generations on attempt ${generationAttempts}; discarding the accumulated results and starting over` - ); - - // discard the accumulated results and start over: - return discardPreviousRefinements({ - generationAttempts, - hallucinationFailures, - isHallucinationDetected: false, - state, - }); - } - - // LOCAL MUTATION: - combinedResponse = getCombined({ combinedGenerations: combinedRefinements, partialResponse }); // combine the new response with the previous ones - - const attackDiscoveries = parseCombinedOrThrow({ - combinedResponse, - generationAttempts, - llmType, - logger, - nodeName: 'refine', - }); - - return { - ...state, - attackDiscoveries, // the final, refined answer - generationAttempts: generationAttempts + 1, - combinedRefinements: combinedResponse, - refinements: [...refinements, partialResponse], - }; - } catch (error) { - const parsingError = `refine node is unable to parse (${llm._llmType()}) response from attempt ${generationAttempts}; (this may be an incomplete response from the model): ${error}`; - logger?.debug(() => parsingError); // logged at debug level because the error is expected when the model returns an incomplete response - - const maxRetriesReached = getMaxRetriesReached({ - generationAttempts: generationAttempts + 1, - maxGenerationAttempts, - }); - - const maxHallucinationFailuresReached = getMaxHallucinationFailuresReached({ - hallucinationFailures, - maxHallucinationFailures, - }); - - // we will use the unrefined results if we have reached the maximum number of retries or hallucination failures: - const useUnrefinedResults = getUseUnrefinedResults({ - maxHallucinationFailuresReached, - maxRetriesReached, - }); - - if (useUnrefinedResults) { - logger?.debug( - () => - `refine node is using unrefined results response (${llm._llmType()}) from attempt ${generationAttempts}, because all attempts have been used` - ); - } - - return { - ...state, - attackDiscoveries: useUnrefinedResults ? unrefinedResults : null, - combinedRefinements: combinedResponse, - errors: [...state.errors, parsingError], - generationAttempts: generationAttempts + 1, - refinements: [...refinements, partialResponse], - }; - } - }; - - return refine; -}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/retriever/anonymized_alerts_retriever/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/retriever/anonymized_alerts_retriever/index.ts deleted file mode 100644 index 3a8b7ed3a6b94..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/retriever/anonymized_alerts_retriever/index.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ElasticsearchClient } from '@kbn/core/server'; -import { Replacements } from '@kbn/elastic-assistant-common'; -import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen'; -import type { CallbackManagerForRetrieverRun } from '@langchain/core/callbacks/manager'; -import type { Document } from '@langchain/core/documents'; -import { BaseRetriever, type BaseRetrieverInput } from '@langchain/core/retrievers'; - -import { getAnonymizedAlerts } from '../helpers/get_anonymized_alerts'; - -export type CustomRetrieverInput = BaseRetrieverInput; - -export class AnonymizedAlertsRetriever extends BaseRetriever { - lc_namespace = ['langchain', 'retrievers']; - - #alertsIndexPattern?: string; - #anonymizationFields?: AnonymizationFieldResponse[]; - #esClient: ElasticsearchClient; - #onNewReplacements?: (newReplacements: Replacements) => void; - #replacements?: Replacements; - #size?: number; - - constructor({ - alertsIndexPattern, - anonymizationFields, - fields, - esClient, - onNewReplacements, - replacements, - size, - }: { - alertsIndexPattern?: string; - anonymizationFields?: AnonymizationFieldResponse[]; - fields?: CustomRetrieverInput; - esClient: ElasticsearchClient; - onNewReplacements?: (newReplacements: Replacements) => void; - replacements?: Replacements; - size?: number; - }) { - super(fields); - - this.#alertsIndexPattern = alertsIndexPattern; - this.#anonymizationFields = anonymizationFields; - this.#esClient = esClient; - this.#onNewReplacements = onNewReplacements; - this.#replacements = replacements; - this.#size = size; - } - - async _getRelevantDocuments( - query: string, - runManager?: CallbackManagerForRetrieverRun - ): Promise { - const anonymizedAlerts = await getAnonymizedAlerts({ - alertsIndexPattern: this.#alertsIndexPattern, - anonymizationFields: this.#anonymizationFields, - esClient: this.#esClient, - onNewReplacements: this.#onNewReplacements, - replacements: this.#replacements, - size: this.#size, - }); - - return anonymizedAlerts.map((alert) => ({ - pageContent: alert, - metadata: {}, - })); - } -} diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/retriever/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/retriever/index.ts deleted file mode 100644 index 951ae3bca8854..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/retriever/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ElasticsearchClient, Logger } from '@kbn/core/server'; -import { Replacements } from '@kbn/elastic-assistant-common'; -import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen'; - -import { AnonymizedAlertsRetriever } from './anonymized_alerts_retriever'; -import type { GraphState } from '../../types'; - -export const getRetrieveAnonymizedAlertsNode = ({ - alertsIndexPattern, - anonymizationFields, - esClient, - logger, - onNewReplacements, - replacements, - size, -}: { - alertsIndexPattern?: string; - anonymizationFields?: AnonymizationFieldResponse[]; - esClient: ElasticsearchClient; - logger?: Logger; - onNewReplacements?: (replacements: Replacements) => void; - replacements?: Replacements; - size?: number; -}): ((state: GraphState) => Promise) => { - let localReplacements = { ...(replacements ?? {}) }; - const localOnNewReplacements = (newReplacements: Replacements) => { - localReplacements = { ...localReplacements, ...newReplacements }; - - onNewReplacements?.(localReplacements); // invoke the callback with the latest replacements - }; - - const retriever = new AnonymizedAlertsRetriever({ - alertsIndexPattern, - anonymizationFields, - esClient, - onNewReplacements: localOnNewReplacements, - replacements, - size, - }); - - const retrieveAnonymizedAlerts = async (state: GraphState): Promise => { - logger?.debug(() => '---RETRIEVE ANONYMIZED ALERTS---'); - const documents = await retriever - .withConfig({ runName: 'runAnonymizedAlertsRetriever' }) - .invoke(''); - - return { - ...state, - anonymizedAlerts: documents, - replacements: localReplacements, - }; - }; - - return retrieveAnonymizedAlerts; -}; - -/** - * Retrieve documents - * - * @param {GraphState} state The current state of the graph. - * @param {RunnableConfig | undefined} config The configuration object for tracing. - * @returns {Promise} The new state object. - */ diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/state/index.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/state/index.ts deleted file mode 100644 index 4229155cc2e25..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/state/index.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AttackDiscovery, Replacements } from '@kbn/elastic-assistant-common'; -import type { Document } from '@langchain/core/documents'; -import type { StateGraphArgs } from '@langchain/langgraph'; - -import { - DEFAULT_MAX_GENERATION_ATTEMPTS, - DEFAULT_MAX_HALLUCINATION_FAILURES, - DEFAULT_MAX_REPEATED_GENERATIONS, -} from '../constants'; -import { getDefaultAttackDiscoveryPrompt } from '../nodes/helpers/get_default_attack_discovery_prompt'; -import { getDefaultRefinePrompt } from '../nodes/refine/helpers/get_default_refine_prompt'; -import type { GraphState } from '../types'; - -export const getDefaultGraphState = (): StateGraphArgs['channels'] => ({ - attackDiscoveries: { - value: (x: AttackDiscovery[] | null, y?: AttackDiscovery[] | null) => y ?? x, - default: () => null, - }, - attackDiscoveryPrompt: { - value: (x: string, y?: string) => y ?? x, - default: () => getDefaultAttackDiscoveryPrompt(), - }, - anonymizedAlerts: { - value: (x: Document[], y?: Document[]) => y ?? x, - default: () => [], - }, - combinedGenerations: { - value: (x: string, y?: string) => y ?? x, - default: () => '', - }, - combinedRefinements: { - value: (x: string, y?: string) => y ?? x, - default: () => '', - }, - errors: { - value: (x: string[], y?: string[]) => y ?? x, - default: () => [], - }, - generationAttempts: { - value: (x: number, y?: number) => y ?? x, - default: () => 0, - }, - generations: { - value: (x: string[], y?: string[]) => y ?? x, - default: () => [], - }, - hallucinationFailures: { - value: (x: number, y?: number) => y ?? x, - default: () => 0, - }, - refinePrompt: { - value: (x: string, y?: string) => y ?? x, - default: () => getDefaultRefinePrompt(), - }, - maxGenerationAttempts: { - value: (x: number, y?: number) => y ?? x, - default: () => DEFAULT_MAX_GENERATION_ATTEMPTS, - }, - maxHallucinationFailures: { - value: (x: number, y?: number) => y ?? x, - default: () => DEFAULT_MAX_HALLUCINATION_FAILURES, - }, - maxRepeatedGenerations: { - value: (x: number, y?: number) => y ?? x, - default: () => DEFAULT_MAX_REPEATED_GENERATIONS, - }, - refinements: { - value: (x: string[], y?: string[]) => y ?? x, - default: () => [], - }, - replacements: { - value: (x: Replacements, y?: Replacements) => y ?? x, - default: () => ({}), - }, - unrefinedResults: { - value: (x: AttackDiscovery[] | null, y?: AttackDiscovery[] | null) => y ?? x, - default: () => null, - }, -}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/types.ts b/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/types.ts deleted file mode 100644 index b4473a02b82ae..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { AttackDiscovery, Replacements } from '@kbn/elastic-assistant-common'; -import type { Document } from '@langchain/core/documents'; - -export interface GraphState { - attackDiscoveries: AttackDiscovery[] | null; - attackDiscoveryPrompt: string; - anonymizedAlerts: Document[]; - combinedGenerations: string; - combinedRefinements: string; - errors: string[]; - generationAttempts: number; - generations: string[]; - hallucinationFailures: number; - maxGenerationAttempts: number; - maxHallucinationFailures: number; - maxRepeatedGenerations: number; - refinements: string[]; - refinePrompt: string; - replacements: Replacements; - unrefinedResults: AttackDiscovery[] | null; -} diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/index.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/index.ts index b9e4f85a800a0..706da7197f31a 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/index.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/index.ts @@ -10,41 +10,14 @@ import { GetDefaultAssistantGraphParams, DefaultAssistantGraph, } from './default_assistant_graph/graph'; -import { - DefaultAttackDiscoveryGraph, - GetDefaultAttackDiscoveryGraphParams, - getDefaultAttackDiscoveryGraph, -} from '../../attack_discovery/graphs/default_attack_discovery_graph'; export type GetAssistantGraph = (params: GetDefaultAssistantGraphParams) => DefaultAssistantGraph; -export type GetAttackDiscoveryGraph = ( - params: GetDefaultAttackDiscoveryGraphParams -) => DefaultAttackDiscoveryGraph; - -export type GraphType = 'assistant' | 'attack-discovery'; - -export interface AssistantGraphMetadata { - getDefaultAssistantGraph: GetAssistantGraph; - graphType: 'assistant'; -} - -export interface AttackDiscoveryGraphMetadata { - getDefaultAttackDiscoveryGraph: GetAttackDiscoveryGraph; - graphType: 'attack-discovery'; -} - -export type GraphMetadata = AssistantGraphMetadata | AttackDiscoveryGraphMetadata; /** * Map of the different Assistant Graphs. Useful for running evaluations. */ -export const ASSISTANT_GRAPH_MAP: Record = { - DefaultAssistantGraph: { - getDefaultAssistantGraph, - graphType: 'assistant', - }, - DefaultAttackDiscoveryGraph: { - getDefaultAttackDiscoveryGraph, - graphType: 'attack-discovery', - }, +export const ASSISTANT_GRAPH_MAP: Record = { + DefaultAssistantGraph: getDefaultAssistantGraph, + // TODO: Support additional graphs + // AttackDiscoveryGraph: getDefaultAssistantGraph, }; diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/cancel/cancel_attack_discovery.test.ts b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/cancel_attack_discovery.test.ts similarity index 80% rename from x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/cancel/cancel_attack_discovery.test.ts rename to x-pack/plugins/elastic_assistant/server/routes/attack_discovery/cancel_attack_discovery.test.ts index 9f5efbe5041d5..66aca77f1eb8b 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/cancel/cancel_attack_discovery.test.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/cancel_attack_discovery.test.ts @@ -8,23 +8,15 @@ import { cancelAttackDiscoveryRoute } from './cancel_attack_discovery'; import { AuthenticatedUser } from '@kbn/core-security-common'; -import { serverMock } from '../../../../__mocks__/server'; -import { requestContextMock } from '../../../../__mocks__/request_context'; +import { serverMock } from '../../__mocks__/server'; +import { requestContextMock } from '../../__mocks__/request_context'; import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; -import { AttackDiscoveryDataClient } from '../../../../lib/attack_discovery/persistence'; -import { transformESSearchToAttackDiscovery } from '../../../../lib/attack_discovery/persistence/transforms/transforms'; -import { getAttackDiscoverySearchEsMock } from '../../../../__mocks__/attack_discovery_schema.mock'; -import { getCancelAttackDiscoveryRequest } from '../../../../__mocks__/request'; -import { updateAttackDiscoveryStatusToCanceled } from '../../helpers/helpers'; - -jest.mock('../../helpers/helpers', () => { - const original = jest.requireActual('../../helpers/helpers'); - - return { - ...original, - updateAttackDiscoveryStatusToCanceled: jest.fn(), - }; -}); +import { AttackDiscoveryDataClient } from '../../ai_assistant_data_clients/attack_discovery'; +import { transformESSearchToAttackDiscovery } from '../../ai_assistant_data_clients/attack_discovery/transforms'; +import { getAttackDiscoverySearchEsMock } from '../../__mocks__/attack_discovery_schema.mock'; +import { getCancelAttackDiscoveryRequest } from '../../__mocks__/request'; +import { updateAttackDiscoveryStatusToCanceled } from './helpers'; +jest.mock('./helpers'); const { clients, context } = requestContextMock.createTools(); const server: ReturnType = serverMock.create(); diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/cancel/cancel_attack_discovery.ts b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/cancel_attack_discovery.ts similarity index 91% rename from x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/cancel/cancel_attack_discovery.ts rename to x-pack/plugins/elastic_assistant/server/routes/attack_discovery/cancel_attack_discovery.ts index 86631708b1cf7..47b748c9c432a 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/cancel/cancel_attack_discovery.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/cancel_attack_discovery.ts @@ -14,16 +14,16 @@ import { } from '@kbn/elastic-assistant-common'; import { transformError } from '@kbn/securitysolution-es-utils'; -import { updateAttackDiscoveryStatusToCanceled } from '../../helpers/helpers'; -import { ATTACK_DISCOVERY_CANCEL_BY_CONNECTOR_ID } from '../../../../../common/constants'; -import { buildResponse } from '../../../../lib/build_response'; -import { ElasticAssistantRequestHandlerContext } from '../../../../types'; +import { updateAttackDiscoveryStatusToCanceled } from './helpers'; +import { ATTACK_DISCOVERY_CANCEL_BY_CONNECTOR_ID } from '../../../common/constants'; +import { buildResponse } from '../../lib/build_response'; +import { ElasticAssistantRequestHandlerContext } from '../../types'; export const cancelAttackDiscoveryRoute = ( router: IRouter ) => { router.versioned - .post({ + .put({ access: 'internal', path: ATTACK_DISCOVERY_CANCEL_BY_CONNECTOR_ID, options: { diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery.test.ts b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/get_attack_discovery.test.ts similarity index 85% rename from x-pack/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery.test.ts rename to x-pack/plugins/elastic_assistant/server/routes/attack_discovery/get_attack_discovery.test.ts index ce07d66b9606e..74cf160c43ffe 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery.test.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/get_attack_discovery.test.ts @@ -8,24 +8,15 @@ import { getAttackDiscoveryRoute } from './get_attack_discovery'; import { AuthenticatedUser } from '@kbn/core-security-common'; -import { serverMock } from '../../../__mocks__/server'; -import { requestContextMock } from '../../../__mocks__/request_context'; +import { serverMock } from '../../__mocks__/server'; +import { requestContextMock } from '../../__mocks__/request_context'; import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; -import { AttackDiscoveryDataClient } from '../../../lib/attack_discovery/persistence'; -import { transformESSearchToAttackDiscovery } from '../../../lib/attack_discovery/persistence/transforms/transforms'; -import { getAttackDiscoverySearchEsMock } from '../../../__mocks__/attack_discovery_schema.mock'; -import { getAttackDiscoveryRequest } from '../../../__mocks__/request'; -import { getAttackDiscoveryStats, updateAttackDiscoveryLastViewedAt } from '../helpers/helpers'; - -jest.mock('../helpers/helpers', () => { - const original = jest.requireActual('../helpers/helpers'); - - return { - ...original, - getAttackDiscoveryStats: jest.fn(), - updateAttackDiscoveryLastViewedAt: jest.fn(), - }; -}); +import { AttackDiscoveryDataClient } from '../../ai_assistant_data_clients/attack_discovery'; +import { transformESSearchToAttackDiscovery } from '../../ai_assistant_data_clients/attack_discovery/transforms'; +import { getAttackDiscoverySearchEsMock } from '../../__mocks__/attack_discovery_schema.mock'; +import { getAttackDiscoveryRequest } from '../../__mocks__/request'; +import { getAttackDiscoveryStats, updateAttackDiscoveryLastViewedAt } from './helpers'; +jest.mock('./helpers'); const mockStats = { newConnectorResultsCount: 2, diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery.ts b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/get_attack_discovery.ts similarity index 92% rename from x-pack/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery.ts rename to x-pack/plugins/elastic_assistant/server/routes/attack_discovery/get_attack_discovery.ts index e3756b10a3fb3..09b2df98fe090 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/get/get_attack_discovery.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/get_attack_discovery.ts @@ -14,10 +14,10 @@ import { } from '@kbn/elastic-assistant-common'; import { transformError } from '@kbn/securitysolution-es-utils'; -import { updateAttackDiscoveryLastViewedAt, getAttackDiscoveryStats } from '../helpers/helpers'; -import { ATTACK_DISCOVERY_BY_CONNECTOR_ID } from '../../../../common/constants'; -import { buildResponse } from '../../../lib/build_response'; -import { ElasticAssistantRequestHandlerContext } from '../../../types'; +import { updateAttackDiscoveryLastViewedAt, getAttackDiscoveryStats } from './helpers'; +import { ATTACK_DISCOVERY_BY_CONNECTOR_ID } from '../../../common/constants'; +import { buildResponse } from '../../lib/build_response'; +import { ElasticAssistantRequestHandlerContext } from '../../types'; export const getAttackDiscoveryRoute = (router: IRouter) => { router.versioned diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.test.ts b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.test.ts new file mode 100644 index 0000000000000..d5eaf7d159618 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.test.ts @@ -0,0 +1,805 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AuthenticatedUser } from '@kbn/core-security-common'; +import moment from 'moment'; +import { actionsClientMock } from '@kbn/actions-plugin/server/actions_client/actions_client.mock'; + +import { + REQUIRED_FOR_ATTACK_DISCOVERY, + addGenerationInterval, + attackDiscoveryStatus, + getAssistantToolParams, + handleToolError, + updateAttackDiscoveryStatusToCanceled, + updateAttackDiscoveryStatusToRunning, + updateAttackDiscoveries, + getAttackDiscoveryStats, +} from './helpers'; +import { ActionsClientLlm } from '@kbn/langchain/server'; +import { AttackDiscoveryDataClient } from '../../ai_assistant_data_clients/attack_discovery'; +import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants'; +import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import { loggerMock } from '@kbn/logging-mocks'; +import { KibanaRequest } from '@kbn/core-http-server'; +import { + AttackDiscoveryPostRequestBody, + ExecuteConnectorRequestBody, +} from '@kbn/elastic-assistant-common'; +import { coreMock } from '@kbn/core/server/mocks'; +import { transformESSearchToAttackDiscovery } from '../../ai_assistant_data_clients/attack_discovery/transforms'; +import { getAttackDiscoverySearchEsMock } from '../../__mocks__/attack_discovery_schema.mock'; +import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; + +import { + getAnonymizationFieldMock, + getUpdateAnonymizationFieldSchemaMock, +} from '../../__mocks__/anonymization_fields_schema.mock'; + +jest.mock('lodash/fp', () => ({ + uniq: jest.fn((arr) => Array.from(new Set(arr))), +})); + +jest.mock('@kbn/securitysolution-es-utils', () => ({ + transformError: jest.fn((err) => err), +})); +jest.mock('@kbn/langchain/server', () => ({ + ActionsClientLlm: jest.fn(), +})); +jest.mock('../evaluate/utils', () => ({ + getLangSmithTracer: jest.fn().mockReturnValue([]), +})); +jest.mock('../utils', () => ({ + getLlmType: jest.fn().mockReturnValue('llm-type'), +})); +const findAttackDiscoveryByConnectorId = jest.fn(); +const updateAttackDiscovery = jest.fn(); +const createAttackDiscovery = jest.fn(); +const getAttackDiscovery = jest.fn(); +const findAllAttackDiscoveries = jest.fn(); +const mockDataClient = { + findAttackDiscoveryByConnectorId, + updateAttackDiscovery, + createAttackDiscovery, + getAttackDiscovery, + findAllAttackDiscoveries, +} as unknown as AttackDiscoveryDataClient; +const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); +const mockLogger = loggerMock.create(); +const mockTelemetry = coreMock.createSetup().analytics; +const mockError = new Error('Test error'); + +const mockAuthenticatedUser = { + username: 'user', + profile_uid: '1234', + authentication_realm: { + type: 'my_realm_type', + name: 'my_realm_name', + }, +} as AuthenticatedUser; + +const mockApiConfig = { + connectorId: 'connector-id', + actionTypeId: '.bedrock', + model: 'model', + provider: OpenAiProviderType.OpenAi, +}; + +const mockCurrentAd = transformESSearchToAttackDiscovery(getAttackDiscoverySearchEsMock())[0]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const mockRequest: KibanaRequest = {} as unknown as KibanaRequest< + unknown, + unknown, + any, // eslint-disable-line @typescript-eslint/no-explicit-any + any // eslint-disable-line @typescript-eslint/no-explicit-any +>; + +describe('helpers', () => { + const date = '2024-03-28T22:27:28.000Z'; + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + beforeEach(() => { + jest.clearAllMocks(); + jest.setSystemTime(new Date(date)); + getAttackDiscovery.mockResolvedValue(mockCurrentAd); + updateAttackDiscovery.mockResolvedValue({}); + }); + describe('getAssistantToolParams', () => { + const alertsIndexPattern = '.alerts-security.alerts-default'; + const esClient = elasticsearchClientMock.createElasticsearchClient(); + const actionsClient = actionsClientMock.create(); + const langChainTimeout = 1000; + const latestReplacements = {}; + const llm = new ActionsClientLlm({ + actionsClient, + connectorId: 'test-connecter-id', + llmType: 'bedrock', + logger: mockLogger, + temperature: 0, + timeout: 580000, + }); + const onNewReplacements = jest.fn(); + const size = 20; + + const mockParams = { + actionsClient, + alertsIndexPattern: 'alerts-*', + anonymizationFields: [{ id: '1', field: 'field1', allowed: true, anonymized: true }], + apiConfig: mockApiConfig, + esClient: mockEsClient, + connectorTimeout: 1000, + langChainTimeout: 2000, + langSmithProject: 'project', + langSmithApiKey: 'api-key', + logger: mockLogger, + latestReplacements: {}, + onNewReplacements: jest.fn(), + request: {} as KibanaRequest< + unknown, + unknown, + ExecuteConnectorRequestBody | AttackDiscoveryPostRequestBody + >, + size: 10, + }; + + it('should return formatted assistant tool params', () => { + const result = getAssistantToolParams(mockParams); + + expect(ActionsClientLlm).toHaveBeenCalledWith( + expect.objectContaining({ + connectorId: 'connector-id', + llmType: 'llm-type', + }) + ); + expect(result.anonymizationFields).toEqual([ + ...mockParams.anonymizationFields, + ...REQUIRED_FOR_ATTACK_DISCOVERY, + ]); + }); + + it('returns the expected AssistantToolParams when anonymizationFields are provided', () => { + const anonymizationFields = [ + getAnonymizationFieldMock(getUpdateAnonymizationFieldSchemaMock()), + ]; + + const result = getAssistantToolParams({ + actionsClient, + alertsIndexPattern, + apiConfig: mockApiConfig, + anonymizationFields, + connectorTimeout: 1000, + latestReplacements, + esClient, + langChainTimeout, + logger: mockLogger, + onNewReplacements, + request: mockRequest, + size, + }); + + expect(result).toEqual({ + alertsIndexPattern, + anonymizationFields: [...anonymizationFields, ...REQUIRED_FOR_ATTACK_DISCOVERY], + isEnabledKnowledgeBase: false, + chain: undefined, + esClient, + langChainTimeout, + llm, + logger: mockLogger, + onNewReplacements, + replacements: latestReplacements, + request: mockRequest, + size, + }); + }); + + it('returns the expected AssistantToolParams when anonymizationFields is undefined', () => { + const anonymizationFields = undefined; + + const result = getAssistantToolParams({ + actionsClient, + alertsIndexPattern, + apiConfig: mockApiConfig, + anonymizationFields, + connectorTimeout: 1000, + latestReplacements, + esClient, + langChainTimeout, + logger: mockLogger, + onNewReplacements, + request: mockRequest, + size, + }); + + expect(result).toEqual({ + alertsIndexPattern, + anonymizationFields: [...REQUIRED_FOR_ATTACK_DISCOVERY], + isEnabledKnowledgeBase: false, + chain: undefined, + esClient, + langChainTimeout, + llm, + logger: mockLogger, + onNewReplacements, + replacements: latestReplacements, + request: mockRequest, + size, + }); + }); + + describe('addGenerationInterval', () => { + const generationInterval = { date: '2024-01-01T00:00:00Z', durationMs: 1000 }; + const existingIntervals = [ + { date: '2024-01-02T00:00:00Z', durationMs: 2000 }, + { date: '2024-01-03T00:00:00Z', durationMs: 3000 }, + ]; + + it('should add new interval and maintain length within MAX_GENERATION_INTERVALS', () => { + const result = addGenerationInterval(existingIntervals, generationInterval); + expect(result.length).toBeLessThanOrEqual(5); + expect(result).toContain(generationInterval); + }); + + it('should remove the oldest interval if exceeding MAX_GENERATION_INTERVALS', () => { + const longExistingIntervals = [...Array(5)].map((_, i) => ({ + date: `2024-01-0${i + 2}T00:00:00Z`, + durationMs: (i + 2) * 1000, + })); + const result = addGenerationInterval(longExistingIntervals, generationInterval); + expect(result.length).toBe(5); + expect(result).not.toContain(longExistingIntervals[4]); + }); + }); + + describe('updateAttackDiscoveryStatusToRunning', () => { + it('should update existing attack discovery to running', async () => { + const existingAd = { id: 'existing-id', backingIndex: 'index' }; + findAttackDiscoveryByConnectorId.mockResolvedValue(existingAd); + updateAttackDiscovery.mockResolvedValue(existingAd); + + const result = await updateAttackDiscoveryStatusToRunning( + mockDataClient, + mockAuthenticatedUser, + mockApiConfig + ); + + expect(findAttackDiscoveryByConnectorId).toHaveBeenCalledWith({ + connectorId: mockApiConfig.connectorId, + authenticatedUser: mockAuthenticatedUser, + }); + expect(updateAttackDiscovery).toHaveBeenCalledWith({ + attackDiscoveryUpdateProps: expect.objectContaining({ + status: attackDiscoveryStatus.running, + }), + authenticatedUser: mockAuthenticatedUser, + }); + expect(result).toEqual({ attackDiscoveryId: existingAd.id, currentAd: existingAd }); + }); + + it('should create a new attack discovery if none exists', async () => { + const newAd = { id: 'new-id', backingIndex: 'index' }; + findAttackDiscoveryByConnectorId.mockResolvedValue(null); + createAttackDiscovery.mockResolvedValue(newAd); + + const result = await updateAttackDiscoveryStatusToRunning( + mockDataClient, + mockAuthenticatedUser, + mockApiConfig + ); + + expect(createAttackDiscovery).toHaveBeenCalledWith({ + attackDiscoveryCreate: expect.objectContaining({ + status: attackDiscoveryStatus.running, + }), + authenticatedUser: mockAuthenticatedUser, + }); + expect(result).toEqual({ attackDiscoveryId: newAd.id, currentAd: newAd }); + }); + + it('should throw an error if updating or creating attack discovery fails', async () => { + findAttackDiscoveryByConnectorId.mockResolvedValue(null); + createAttackDiscovery.mockResolvedValue(null); + + await expect( + updateAttackDiscoveryStatusToRunning(mockDataClient, mockAuthenticatedUser, mockApiConfig) + ).rejects.toThrow('Could not create attack discovery for connectorId: connector-id'); + }); + }); + + describe('updateAttackDiscoveryStatusToCanceled', () => { + const existingAd = { + id: 'existing-id', + backingIndex: 'index', + status: attackDiscoveryStatus.running, + }; + it('should update existing attack discovery to canceled', async () => { + findAttackDiscoveryByConnectorId.mockResolvedValue(existingAd); + updateAttackDiscovery.mockResolvedValue(existingAd); + + const result = await updateAttackDiscoveryStatusToCanceled( + mockDataClient, + mockAuthenticatedUser, + mockApiConfig.connectorId + ); + + expect(findAttackDiscoveryByConnectorId).toHaveBeenCalledWith({ + connectorId: mockApiConfig.connectorId, + authenticatedUser: mockAuthenticatedUser, + }); + expect(updateAttackDiscovery).toHaveBeenCalledWith({ + attackDiscoveryUpdateProps: expect.objectContaining({ + status: attackDiscoveryStatus.canceled, + }), + authenticatedUser: mockAuthenticatedUser, + }); + expect(result).toEqual(existingAd); + }); + + it('should throw an error if attack discovery is not running', async () => { + findAttackDiscoveryByConnectorId.mockResolvedValue({ + ...existingAd, + status: attackDiscoveryStatus.succeeded, + }); + await expect( + updateAttackDiscoveryStatusToCanceled( + mockDataClient, + mockAuthenticatedUser, + mockApiConfig.connectorId + ) + ).rejects.toThrow( + 'Connector id connector-id does not have a running attack discovery, and therefore cannot be canceled.' + ); + }); + + it('should throw an error if attack discovery does not exist', async () => { + findAttackDiscoveryByConnectorId.mockResolvedValue(null); + await expect( + updateAttackDiscoveryStatusToCanceled( + mockDataClient, + mockAuthenticatedUser, + mockApiConfig.connectorId + ) + ).rejects.toThrow('Could not find attack discovery for connector id: connector-id'); + }); + it('should throw error if updateAttackDiscovery returns null', async () => { + findAttackDiscoveryByConnectorId.mockResolvedValue(existingAd); + updateAttackDiscovery.mockResolvedValue(null); + + await expect( + updateAttackDiscoveryStatusToCanceled( + mockDataClient, + mockAuthenticatedUser, + mockApiConfig.connectorId + ) + ).rejects.toThrow('Could not update attack discovery for connector id: connector-id'); + }); + }); + + describe('updateAttackDiscoveries', () => { + const mockAttackDiscoveryId = 'attack-discovery-id'; + const mockLatestReplacements = {}; + const mockRawAttackDiscoveries = JSON.stringify({ + alertsContextCount: 5, + attackDiscoveries: [{ alertIds: ['alert-1', 'alert-2'] }, { alertIds: ['alert-3'] }], + }); + const mockSize = 10; + const mockStartTime = moment('2024-03-28T22:25:28.000Z'); + + const mockArgs = { + apiConfig: mockApiConfig, + attackDiscoveryId: mockAttackDiscoveryId, + authenticatedUser: mockAuthenticatedUser, + dataClient: mockDataClient, + latestReplacements: mockLatestReplacements, + logger: mockLogger, + rawAttackDiscoveries: mockRawAttackDiscoveries, + size: mockSize, + startTime: mockStartTime, + telemetry: mockTelemetry, + }; + + it('should update attack discoveries and report success telemetry', async () => { + await updateAttackDiscoveries(mockArgs); + + expect(updateAttackDiscovery).toHaveBeenCalledWith({ + attackDiscoveryUpdateProps: { + alertsContextCount: 5, + attackDiscoveries: [{ alertIds: ['alert-1', 'alert-2'] }, { alertIds: ['alert-3'] }], + status: attackDiscoveryStatus.succeeded, + id: mockAttackDiscoveryId, + replacements: mockLatestReplacements, + backingIndex: mockCurrentAd.backingIndex, + generationIntervals: [ + { date, durationMs: 120000 }, + ...mockCurrentAd.generationIntervals, + ], + }, + authenticatedUser: mockAuthenticatedUser, + }); + + expect(mockTelemetry.reportEvent).toHaveBeenCalledWith('attack_discovery_success', { + actionTypeId: mockApiConfig.actionTypeId, + alertsContextCount: 5, + alertsCount: 3, + configuredAlertsCount: mockSize, + discoveriesGenerated: 2, + durationMs: 120000, + model: mockApiConfig.model, + provider: mockApiConfig.provider, + }); + }); + + it('should update attack discoveries without generation interval if no discoveries are found', async () => { + const noDiscoveriesRaw = JSON.stringify({ + alertsContextCount: 0, + attackDiscoveries: [], + }); + + await updateAttackDiscoveries({ + ...mockArgs, + rawAttackDiscoveries: noDiscoveriesRaw, + }); + + expect(updateAttackDiscovery).toHaveBeenCalledWith({ + attackDiscoveryUpdateProps: { + alertsContextCount: 0, + attackDiscoveries: [], + status: attackDiscoveryStatus.succeeded, + id: mockAttackDiscoveryId, + replacements: mockLatestReplacements, + backingIndex: mockCurrentAd.backingIndex, + }, + authenticatedUser: mockAuthenticatedUser, + }); + + expect(mockTelemetry.reportEvent).toHaveBeenCalledWith('attack_discovery_success', { + actionTypeId: mockApiConfig.actionTypeId, + alertsContextCount: 0, + alertsCount: 0, + configuredAlertsCount: mockSize, + discoveriesGenerated: 0, + durationMs: 120000, + model: mockApiConfig.model, + provider: mockApiConfig.provider, + }); + }); + + it('should catch and log an error if raw attack discoveries is null', async () => { + await updateAttackDiscoveries({ + ...mockArgs, + rawAttackDiscoveries: null, + }); + expect(mockLogger.error).toHaveBeenCalledTimes(1); + expect(mockTelemetry.reportEvent).toHaveBeenCalledWith('attack_discovery_error', { + actionTypeId: mockArgs.apiConfig.actionTypeId, + errorMessage: 'tool returned no attack discoveries', + model: mockArgs.apiConfig.model, + provider: mockArgs.apiConfig.provider, + }); + }); + + it('should return and not call updateAttackDiscovery when getAttackDiscovery returns a canceled response', async () => { + getAttackDiscovery.mockResolvedValue({ + ...mockCurrentAd, + status: attackDiscoveryStatus.canceled, + }); + await updateAttackDiscoveries(mockArgs); + + expect(mockLogger.error).not.toHaveBeenCalled(); + expect(updateAttackDiscovery).not.toHaveBeenCalled(); + }); + + it('should log the error and report telemetry when getAttackDiscovery rejects', async () => { + getAttackDiscovery.mockRejectedValue(mockError); + await updateAttackDiscoveries(mockArgs); + + expect(mockLogger.error).toHaveBeenCalledWith(mockError); + expect(updateAttackDiscovery).not.toHaveBeenCalled(); + expect(mockTelemetry.reportEvent).toHaveBeenCalledWith('attack_discovery_error', { + actionTypeId: mockArgs.apiConfig.actionTypeId, + errorMessage: mockError.message, + model: mockArgs.apiConfig.model, + provider: mockArgs.apiConfig.provider, + }); + }); + }); + + describe('handleToolError', () => { + const mockArgs = { + apiConfig: mockApiConfig, + attackDiscoveryId: 'discovery-id', + authenticatedUser: mockAuthenticatedUser, + backingIndex: 'backing-index', + dataClient: mockDataClient, + err: mockError, + latestReplacements: {}, + logger: mockLogger, + telemetry: mockTelemetry, + }; + + it('should log the error and update attack discovery status to failed', async () => { + await handleToolError(mockArgs); + + expect(mockLogger.error).toHaveBeenCalledWith(mockError); + expect(updateAttackDiscovery).toHaveBeenCalledWith({ + attackDiscoveryUpdateProps: { + status: attackDiscoveryStatus.failed, + attackDiscoveries: [], + backingIndex: 'foo', + failureReason: 'Test error', + id: 'discovery-id', + replacements: {}, + }, + authenticatedUser: mockArgs.authenticatedUser, + }); + expect(mockTelemetry.reportEvent).toHaveBeenCalledWith('attack_discovery_error', { + actionTypeId: mockArgs.apiConfig.actionTypeId, + errorMessage: mockError.message, + model: mockArgs.apiConfig.model, + provider: mockArgs.apiConfig.provider, + }); + }); + + it('should log the error and report telemetry when updateAttackDiscovery rejects', async () => { + updateAttackDiscovery.mockRejectedValue(mockError); + await handleToolError(mockArgs); + + expect(mockLogger.error).toHaveBeenCalledWith(mockError); + expect(updateAttackDiscovery).toHaveBeenCalledWith({ + attackDiscoveryUpdateProps: { + status: attackDiscoveryStatus.failed, + attackDiscoveries: [], + backingIndex: 'foo', + failureReason: 'Test error', + id: 'discovery-id', + replacements: {}, + }, + authenticatedUser: mockArgs.authenticatedUser, + }); + expect(mockTelemetry.reportEvent).toHaveBeenCalledWith('attack_discovery_error', { + actionTypeId: mockArgs.apiConfig.actionTypeId, + errorMessage: mockError.message, + model: mockArgs.apiConfig.model, + provider: mockArgs.apiConfig.provider, + }); + }); + + it('should return and not call updateAttackDiscovery when getAttackDiscovery returns a canceled response', async () => { + getAttackDiscovery.mockResolvedValue({ + ...mockCurrentAd, + status: attackDiscoveryStatus.canceled, + }); + await handleToolError(mockArgs); + + expect(mockTelemetry.reportEvent).not.toHaveBeenCalled(); + expect(updateAttackDiscovery).not.toHaveBeenCalled(); + }); + + it('should log the error and report telemetry when getAttackDiscovery rejects', async () => { + getAttackDiscovery.mockRejectedValue(mockError); + await handleToolError(mockArgs); + + expect(mockLogger.error).toHaveBeenCalledWith(mockError); + expect(updateAttackDiscovery).not.toHaveBeenCalled(); + expect(mockTelemetry.reportEvent).toHaveBeenCalledWith('attack_discovery_error', { + actionTypeId: mockArgs.apiConfig.actionTypeId, + errorMessage: mockError.message, + model: mockArgs.apiConfig.model, + provider: mockArgs.apiConfig.provider, + }); + }); + }); + }); + describe('getAttackDiscoveryStats', () => { + const mockDiscoveries = [ + { + timestamp: '2024-06-13T17:55:11.360Z', + id: '8abb49bd-2f5d-43d2-bc2f-dd3c3cab25ad', + backingIndex: '.ds-.kibana-elastic-ai-assistant-attack-discovery-default-2024.06.12-000001', + createdAt: '2024-06-13T17:55:11.360Z', + updatedAt: '2024-06-17T20:47:57.556Z', + lastViewedAt: '2024-06-17T20:47:57.556Z', + users: [mockAuthenticatedUser], + namespace: 'default', + status: 'failed', + alertsContextCount: undefined, + apiConfig: { + connectorId: 'my-bedrock-old', + actionTypeId: '.bedrock', + defaultSystemPromptId: undefined, + model: undefined, + provider: undefined, + }, + attackDiscoveries: [], + replacements: {}, + generationIntervals: mockCurrentAd.generationIntervals, + averageIntervalMs: mockCurrentAd.averageIntervalMs, + failureReason: + 'ActionsClientLlm: action result status is error: an error occurred while running the action - Response validation failed (Error: [usage.input_tokens]: expected value of type [number] but got [undefined])', + }, + { + timestamp: '2024-06-13T17:55:11.360Z', + id: '9abb49bd-2f5d-43d2-bc2f-dd3c3cab25ad', + backingIndex: '.ds-.kibana-elastic-ai-assistant-attack-discovery-default-2024.06.12-000001', + createdAt: '2024-06-13T17:55:11.360Z', + updatedAt: '2024-06-17T20:47:57.556Z', + lastViewedAt: '2024-06-17T20:46:57.556Z', + users: [mockAuthenticatedUser], + namespace: 'default', + status: 'failed', + alertsContextCount: undefined, + apiConfig: { + connectorId: 'my-bedrock-old', + actionTypeId: '.bedrock', + defaultSystemPromptId: undefined, + model: undefined, + provider: undefined, + }, + attackDiscoveries: [], + replacements: {}, + generationIntervals: mockCurrentAd.generationIntervals, + averageIntervalMs: mockCurrentAd.averageIntervalMs, + failureReason: + 'ActionsClientLlm: action result status is error: an error occurred while running the action - Response validation failed (Error: [usage.input_tokens]: expected value of type [number] but got [undefined])', + }, + { + timestamp: '2024-06-12T19:54:50.428Z', + id: '745e005b-7248-4c08-b8b6-4cad263b4be0', + backingIndex: '.ds-.kibana-elastic-ai-assistant-attack-discovery-default-2024.06.12-000001', + createdAt: '2024-06-12T19:54:50.428Z', + updatedAt: '2024-06-17T20:47:27.182Z', + lastViewedAt: '2024-06-17T20:27:27.182Z', + users: [mockAuthenticatedUser], + namespace: 'default', + status: 'running', + alertsContextCount: 20, + apiConfig: { + connectorId: 'my-gen-ai', + actionTypeId: '.gen-ai', + defaultSystemPromptId: undefined, + model: undefined, + provider: undefined, + }, + attackDiscoveries: mockCurrentAd.attackDiscoveries, + replacements: {}, + generationIntervals: mockCurrentAd.generationIntervals, + averageIntervalMs: mockCurrentAd.averageIntervalMs, + failureReason: undefined, + }, + { + timestamp: '2024-06-13T17:50:59.409Z', + id: 'f48da2ca-b63e-4387-82d7-1423a68500aa', + backingIndex: '.ds-.kibana-elastic-ai-assistant-attack-discovery-default-2024.06.12-000001', + createdAt: '2024-06-13T17:50:59.409Z', + updatedAt: '2024-06-17T20:47:59.969Z', + lastViewedAt: '2024-06-17T20:47:35.227Z', + users: [mockAuthenticatedUser], + namespace: 'default', + status: 'succeeded', + alertsContextCount: 20, + apiConfig: { + connectorId: 'my-gpt4o-ai', + actionTypeId: '.gen-ai', + defaultSystemPromptId: undefined, + model: undefined, + provider: undefined, + }, + attackDiscoveries: mockCurrentAd.attackDiscoveries, + replacements: {}, + generationIntervals: mockCurrentAd.generationIntervals, + averageIntervalMs: mockCurrentAd.averageIntervalMs, + failureReason: undefined, + }, + { + timestamp: '2024-06-12T21:18:56.377Z', + id: '82fced1d-de48-42db-9f56-e45122dee017', + backingIndex: '.ds-.kibana-elastic-ai-assistant-attack-discovery-default-2024.06.12-000001', + createdAt: '2024-06-12T21:18:56.377Z', + updatedAt: '2024-06-17T20:47:39.372Z', + lastViewedAt: '2024-06-17T20:47:39.372Z', + users: [mockAuthenticatedUser], + namespace: 'default', + status: 'canceled', + alertsContextCount: 20, + apiConfig: { + connectorId: 'my-bedrock', + actionTypeId: '.bedrock', + defaultSystemPromptId: undefined, + model: undefined, + provider: undefined, + }, + attackDiscoveries: mockCurrentAd.attackDiscoveries, + replacements: {}, + generationIntervals: mockCurrentAd.generationIntervals, + averageIntervalMs: mockCurrentAd.averageIntervalMs, + failureReason: undefined, + }, + { + timestamp: '2024-06-12T16:44:23.107Z', + id: 'a4709094-6116-484b-b096-1e8d151cb4b7', + backingIndex: '.ds-.kibana-elastic-ai-assistant-attack-discovery-default-2024.06.12-000001', + createdAt: '2024-06-12T16:44:23.107Z', + updatedAt: '2024-06-17T20:48:16.961Z', + lastViewedAt: '2024-06-17T20:47:16.961Z', + users: [mockAuthenticatedUser], + namespace: 'default', + status: 'succeeded', + alertsContextCount: 0, + apiConfig: { + connectorId: 'my-gen-a2i', + actionTypeId: '.gen-ai', + defaultSystemPromptId: undefined, + model: undefined, + provider: undefined, + }, + attackDiscoveries: [ + ...mockCurrentAd.attackDiscoveries, + ...mockCurrentAd.attackDiscoveries, + ...mockCurrentAd.attackDiscoveries, + ...mockCurrentAd.attackDiscoveries, + ], + replacements: {}, + generationIntervals: mockCurrentAd.generationIntervals, + averageIntervalMs: mockCurrentAd.averageIntervalMs, + failureReason: 'steph threw an error', + }, + ]; + beforeEach(() => { + findAllAttackDiscoveries.mockResolvedValue(mockDiscoveries); + }); + it('returns the formatted stats object', async () => { + const stats = await getAttackDiscoveryStats({ + authenticatedUser: mockAuthenticatedUser, + dataClient: mockDataClient, + }); + expect(stats).toEqual([ + { + hasViewed: true, + status: 'failed', + count: 0, + connectorId: 'my-bedrock-old', + }, + { + hasViewed: false, + status: 'failed', + count: 0, + connectorId: 'my-bedrock-old', + }, + { + hasViewed: false, + status: 'running', + count: 1, + connectorId: 'my-gen-ai', + }, + { + hasViewed: false, + status: 'succeeded', + count: 1, + connectorId: 'my-gpt4o-ai', + }, + { + hasViewed: true, + status: 'canceled', + count: 1, + connectorId: 'my-bedrock', + }, + { + hasViewed: false, + status: 'succeeded', + count: 4, + connectorId: 'my-gen-a2i', + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers/helpers.ts b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.ts similarity index 55% rename from x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers/helpers.ts rename to x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.ts index 188976f0b3f5c..f016d6ac29118 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers/helpers.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers.ts @@ -5,29 +5,38 @@ * 2.0. */ -import { AnalyticsServiceSetup, AuthenticatedUser, Logger } from '@kbn/core/server'; +import { AnalyticsServiceSetup, AuthenticatedUser, KibanaRequest, Logger } from '@kbn/core/server'; +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; import { ApiConfig, AttackDiscovery, + AttackDiscoveryPostRequestBody, AttackDiscoveryResponse, AttackDiscoveryStat, AttackDiscoveryStatus, + ExecuteConnectorRequestBody, GenerationInterval, Replacements, } from '@kbn/elastic-assistant-common'; import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen'; -import type { Document } from '@langchain/core/documents'; import { v4 as uuidv4 } from 'uuid'; +import { ActionsClientLlm } from '@kbn/langchain/server'; + import { Moment } from 'moment'; import { transformError } from '@kbn/securitysolution-es-utils'; +import type { ActionsClient } from '@kbn/actions-plugin/server'; import moment from 'moment/moment'; import { uniq } from 'lodash/fp'; - +import { PublicMethodsOf } from '@kbn/utility-types'; +import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith'; +import { getLlmType } from '../utils'; +import type { GetRegisteredTools } from '../../services/app_context'; import { ATTACK_DISCOVERY_ERROR_EVENT, ATTACK_DISCOVERY_SUCCESS_EVENT, -} from '../../../lib/telemetry/event_based_telemetry'; -import { AttackDiscoveryDataClient } from '../../../lib/attack_discovery/persistence'; +} from '../../lib/telemetry/event_based_telemetry'; +import { AssistantToolParams } from '../../types'; +import { AttackDiscoveryDataClient } from '../../ai_assistant_data_clients/attack_discovery'; export const REQUIRED_FOR_ATTACK_DISCOVERY: AnonymizationFieldResponse[] = [ { @@ -44,6 +53,116 @@ export const REQUIRED_FOR_ATTACK_DISCOVERY: AnonymizationFieldResponse[] = [ }, ]; +export const getAssistantToolParams = ({ + actionsClient, + alertsIndexPattern, + anonymizationFields, + apiConfig, + esClient, + connectorTimeout, + langChainTimeout, + langSmithProject, + langSmithApiKey, + logger, + latestReplacements, + onNewReplacements, + request, + size, +}: { + actionsClient: PublicMethodsOf; + alertsIndexPattern: string; + anonymizationFields?: AnonymizationFieldResponse[]; + apiConfig: ApiConfig; + esClient: ElasticsearchClient; + connectorTimeout: number; + langChainTimeout: number; + langSmithProject?: string; + langSmithApiKey?: string; + logger: Logger; + latestReplacements: Replacements; + onNewReplacements: (newReplacements: Replacements) => void; + request: KibanaRequest< + unknown, + unknown, + ExecuteConnectorRequestBody | AttackDiscoveryPostRequestBody + >; + size: number; +}) => { + const traceOptions = { + projectName: langSmithProject, + tracers: [ + ...getLangSmithTracer({ + apiKey: langSmithApiKey, + projectName: langSmithProject, + logger, + }), + ], + }; + + const llm = new ActionsClientLlm({ + actionsClient, + connectorId: apiConfig.connectorId, + llmType: getLlmType(apiConfig.actionTypeId), + logger, + temperature: 0, // zero temperature for attack discovery, because we want structured JSON output + timeout: connectorTimeout, + traceOptions, + }); + + return formatAssistantToolParams({ + alertsIndexPattern, + anonymizationFields, + esClient, + latestReplacements, + langChainTimeout, + llm, + logger, + onNewReplacements, + request, + size, + }); +}; + +const formatAssistantToolParams = ({ + alertsIndexPattern, + anonymizationFields, + esClient, + langChainTimeout, + latestReplacements, + llm, + logger, + onNewReplacements, + request, + size, +}: { + alertsIndexPattern: string; + anonymizationFields?: AnonymizationFieldResponse[]; + esClient: ElasticsearchClient; + langChainTimeout: number; + latestReplacements: Replacements; + llm: ActionsClientLlm; + logger: Logger; + onNewReplacements: (newReplacements: Replacements) => void; + request: KibanaRequest< + unknown, + unknown, + ExecuteConnectorRequestBody | AttackDiscoveryPostRequestBody + >; + size: number; +}): Omit => ({ + alertsIndexPattern, + anonymizationFields: [...(anonymizationFields ?? []), ...REQUIRED_FOR_ATTACK_DISCOVERY], + isEnabledKnowledgeBase: false, // not required for attack discovery + esClient, + langChainTimeout, + llm, + logger, + onNewReplacements, + replacements: latestReplacements, + request, + size, +}); + export const attackDiscoveryStatus: { [k: string]: AttackDiscoveryStatus } = { canceled: 'canceled', failed: 'failed', @@ -68,8 +187,7 @@ export const addGenerationInterval = ( export const updateAttackDiscoveryStatusToRunning = async ( dataClient: AttackDiscoveryDataClient, authenticatedUser: AuthenticatedUser, - apiConfig: ApiConfig, - alertsContextCount: number + apiConfig: ApiConfig ): Promise<{ currentAd: AttackDiscoveryResponse; attackDiscoveryId: string; @@ -81,7 +199,6 @@ export const updateAttackDiscoveryStatusToRunning = async ( const currentAd = foundAttackDiscovery ? await dataClient?.updateAttackDiscovery({ attackDiscoveryUpdateProps: { - alertsContextCount, backingIndex: foundAttackDiscovery.backingIndex, id: foundAttackDiscovery.id, status: attackDiscoveryStatus.running, @@ -90,7 +207,6 @@ export const updateAttackDiscoveryStatusToRunning = async ( }) : await dataClient?.createAttackDiscovery({ attackDiscoveryCreate: { - alertsContextCount, apiConfig, attackDiscoveries: [], status: attackDiscoveryStatus.running, @@ -145,32 +261,38 @@ export const updateAttackDiscoveryStatusToCanceled = async ( return updatedAttackDiscovery; }; +const getDataFromJSON = (adStringified: string) => { + const { alertsContextCount, attackDiscoveries } = JSON.parse(adStringified); + return { alertsContextCount, attackDiscoveries }; +}; + export const updateAttackDiscoveries = async ({ - anonymizedAlerts, apiConfig, - attackDiscoveries, attackDiscoveryId, authenticatedUser, dataClient, latestReplacements, logger, + rawAttackDiscoveries, size, startTime, telemetry, }: { - anonymizedAlerts: Document[]; apiConfig: ApiConfig; - attackDiscoveries: AttackDiscovery[] | null; attackDiscoveryId: string; authenticatedUser: AuthenticatedUser; dataClient: AttackDiscoveryDataClient; latestReplacements: Replacements; logger: Logger; + rawAttackDiscoveries: string | null; size: number; startTime: Moment; telemetry: AnalyticsServiceSetup; }) => { try { + if (rawAttackDiscoveries == null) { + throw new Error('tool returned no attack discoveries'); + } const currentAd = await dataClient.getAttackDiscovery({ id: attackDiscoveryId, authenticatedUser, @@ -180,12 +302,12 @@ export const updateAttackDiscoveries = async ({ } const endTime = moment(); const durationMs = endTime.diff(startTime); - const alertsContextCount = anonymizedAlerts.length; + const { alertsContextCount, attackDiscoveries } = getDataFromJSON(rawAttackDiscoveries); const updateProps = { alertsContextCount, - attackDiscoveries: attackDiscoveries ?? undefined, + attackDiscoveries, status: attackDiscoveryStatus.succeeded, - ...(alertsContextCount === 0 + ...(alertsContextCount === 0 || attackDiscoveries === 0 ? {} : { generationIntervals: addGenerationInterval(currentAd.generationIntervals, { @@ -205,14 +327,13 @@ export const updateAttackDiscoveries = async ({ telemetry.reportEvent(ATTACK_DISCOVERY_SUCCESS_EVENT.eventType, { actionTypeId: apiConfig.actionTypeId, alertsContextCount: updateProps.alertsContextCount, - alertsCount: - uniq( - updateProps.attackDiscoveries?.flatMap( - (attackDiscovery: AttackDiscovery) => attackDiscovery.alertIds - ) - ).length ?? 0, + alertsCount: uniq( + updateProps.attackDiscoveries.flatMap( + (attackDiscovery: AttackDiscovery) => attackDiscovery.alertIds + ) + ).length, configuredAlertsCount: size, - discoveriesGenerated: updateProps.attackDiscoveries?.length ?? 0, + discoveriesGenerated: updateProps.attackDiscoveries.length, durationMs, model: apiConfig.model, provider: apiConfig.provider, @@ -229,6 +350,70 @@ export const updateAttackDiscoveries = async ({ } }; +export const handleToolError = async ({ + apiConfig, + attackDiscoveryId, + authenticatedUser, + dataClient, + err, + latestReplacements, + logger, + telemetry, +}: { + apiConfig: ApiConfig; + attackDiscoveryId: string; + authenticatedUser: AuthenticatedUser; + dataClient: AttackDiscoveryDataClient; + err: Error; + latestReplacements: Replacements; + logger: Logger; + telemetry: AnalyticsServiceSetup; +}) => { + try { + logger.error(err); + const error = transformError(err); + const currentAd = await dataClient.getAttackDiscovery({ + id: attackDiscoveryId, + authenticatedUser, + }); + + if (currentAd === null || currentAd?.status === 'canceled') { + return; + } + await dataClient.updateAttackDiscovery({ + attackDiscoveryUpdateProps: { + attackDiscoveries: [], + status: attackDiscoveryStatus.failed, + id: attackDiscoveryId, + replacements: latestReplacements, + backingIndex: currentAd.backingIndex, + failureReason: error.message, + }, + authenticatedUser, + }); + telemetry.reportEvent(ATTACK_DISCOVERY_ERROR_EVENT.eventType, { + actionTypeId: apiConfig.actionTypeId, + errorMessage: error.message, + model: apiConfig.model, + provider: apiConfig.provider, + }); + } catch (updateErr) { + const updateError = transformError(updateErr); + telemetry.reportEvent(ATTACK_DISCOVERY_ERROR_EVENT.eventType, { + actionTypeId: apiConfig.actionTypeId, + errorMessage: updateError.message, + model: apiConfig.model, + provider: apiConfig.provider, + }); + } +}; + +export const getAssistantTool = (getRegisteredTools: GetRegisteredTools, pluginName: string) => { + // get the attack discovery tool: + const assistantTools = getRegisteredTools(pluginName); + return assistantTools.find((tool) => tool.id === 'attack-discovery'); +}; + export const updateAttackDiscoveryLastViewedAt = async ({ connectorId, authenticatedUser, diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers/helpers.test.ts b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers/helpers.test.ts deleted file mode 100644 index 2e0a545eb083a..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/helpers/helpers.test.ts +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AuthenticatedUser } from '@kbn/core-security-common'; - -import { getAttackDiscoveryStats } from './helpers'; -import { AttackDiscoveryDataClient } from '../../../lib/attack_discovery/persistence'; -import { transformESSearchToAttackDiscovery } from '../../../lib/attack_discovery/persistence/transforms/transforms'; -import { getAttackDiscoverySearchEsMock } from '../../../__mocks__/attack_discovery_schema.mock'; - -jest.mock('lodash/fp', () => ({ - uniq: jest.fn((arr) => Array.from(new Set(arr))), -})); - -jest.mock('@kbn/securitysolution-es-utils', () => ({ - transformError: jest.fn((err) => err), -})); -jest.mock('@kbn/langchain/server', () => ({ - ActionsClientLlm: jest.fn(), -})); -jest.mock('../../evaluate/utils', () => ({ - getLangSmithTracer: jest.fn().mockReturnValue([]), -})); -jest.mock('../../utils', () => ({ - getLlmType: jest.fn().mockReturnValue('llm-type'), -})); -const findAttackDiscoveryByConnectorId = jest.fn(); -const updateAttackDiscovery = jest.fn(); -const createAttackDiscovery = jest.fn(); -const getAttackDiscovery = jest.fn(); -const findAllAttackDiscoveries = jest.fn(); -const mockDataClient = { - findAttackDiscoveryByConnectorId, - updateAttackDiscovery, - createAttackDiscovery, - getAttackDiscovery, - findAllAttackDiscoveries, -} as unknown as AttackDiscoveryDataClient; - -const mockAuthenticatedUser = { - username: 'user', - profile_uid: '1234', - authentication_realm: { - type: 'my_realm_type', - name: 'my_realm_name', - }, -} as AuthenticatedUser; - -const mockCurrentAd = transformESSearchToAttackDiscovery(getAttackDiscoverySearchEsMock())[0]; - -describe('helpers', () => { - const date = '2024-03-28T22:27:28.000Z'; - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - beforeEach(() => { - jest.clearAllMocks(); - jest.setSystemTime(new Date(date)); - getAttackDiscovery.mockResolvedValue(mockCurrentAd); - updateAttackDiscovery.mockResolvedValue({}); - }); - - describe('getAttackDiscoveryStats', () => { - const mockDiscoveries = [ - { - timestamp: '2024-06-13T17:55:11.360Z', - id: '8abb49bd-2f5d-43d2-bc2f-dd3c3cab25ad', - backingIndex: '.ds-.kibana-elastic-ai-assistant-attack-discovery-default-2024.06.12-000001', - createdAt: '2024-06-13T17:55:11.360Z', - updatedAt: '2024-06-17T20:47:57.556Z', - lastViewedAt: '2024-06-17T20:47:57.556Z', - users: [mockAuthenticatedUser], - namespace: 'default', - status: 'failed', - alertsContextCount: undefined, - apiConfig: { - connectorId: 'my-bedrock-old', - actionTypeId: '.bedrock', - defaultSystemPromptId: undefined, - model: undefined, - provider: undefined, - }, - attackDiscoveries: [], - replacements: {}, - generationIntervals: mockCurrentAd.generationIntervals, - averageIntervalMs: mockCurrentAd.averageIntervalMs, - failureReason: - 'ActionsClientLlm: action result status is error: an error occurred while running the action - Response validation failed (Error: [usage.input_tokens]: expected value of type [number] but got [undefined])', - }, - { - timestamp: '2024-06-13T17:55:11.360Z', - id: '9abb49bd-2f5d-43d2-bc2f-dd3c3cab25ad', - backingIndex: '.ds-.kibana-elastic-ai-assistant-attack-discovery-default-2024.06.12-000001', - createdAt: '2024-06-13T17:55:11.360Z', - updatedAt: '2024-06-17T20:47:57.556Z', - lastViewedAt: '2024-06-17T20:46:57.556Z', - users: [mockAuthenticatedUser], - namespace: 'default', - status: 'failed', - alertsContextCount: undefined, - apiConfig: { - connectorId: 'my-bedrock-old', - actionTypeId: '.bedrock', - defaultSystemPromptId: undefined, - model: undefined, - provider: undefined, - }, - attackDiscoveries: [], - replacements: {}, - generationIntervals: mockCurrentAd.generationIntervals, - averageIntervalMs: mockCurrentAd.averageIntervalMs, - failureReason: - 'ActionsClientLlm: action result status is error: an error occurred while running the action - Response validation failed (Error: [usage.input_tokens]: expected value of type [number] but got [undefined])', - }, - { - timestamp: '2024-06-12T19:54:50.428Z', - id: '745e005b-7248-4c08-b8b6-4cad263b4be0', - backingIndex: '.ds-.kibana-elastic-ai-assistant-attack-discovery-default-2024.06.12-000001', - createdAt: '2024-06-12T19:54:50.428Z', - updatedAt: '2024-06-17T20:47:27.182Z', - lastViewedAt: '2024-06-17T20:27:27.182Z', - users: [mockAuthenticatedUser], - namespace: 'default', - status: 'running', - alertsContextCount: 20, - apiConfig: { - connectorId: 'my-gen-ai', - actionTypeId: '.gen-ai', - defaultSystemPromptId: undefined, - model: undefined, - provider: undefined, - }, - attackDiscoveries: mockCurrentAd.attackDiscoveries, - replacements: {}, - generationIntervals: mockCurrentAd.generationIntervals, - averageIntervalMs: mockCurrentAd.averageIntervalMs, - failureReason: undefined, - }, - { - timestamp: '2024-06-13T17:50:59.409Z', - id: 'f48da2ca-b63e-4387-82d7-1423a68500aa', - backingIndex: '.ds-.kibana-elastic-ai-assistant-attack-discovery-default-2024.06.12-000001', - createdAt: '2024-06-13T17:50:59.409Z', - updatedAt: '2024-06-17T20:47:59.969Z', - lastViewedAt: '2024-06-17T20:47:35.227Z', - users: [mockAuthenticatedUser], - namespace: 'default', - status: 'succeeded', - alertsContextCount: 20, - apiConfig: { - connectorId: 'my-gpt4o-ai', - actionTypeId: '.gen-ai', - defaultSystemPromptId: undefined, - model: undefined, - provider: undefined, - }, - attackDiscoveries: mockCurrentAd.attackDiscoveries, - replacements: {}, - generationIntervals: mockCurrentAd.generationIntervals, - averageIntervalMs: mockCurrentAd.averageIntervalMs, - failureReason: undefined, - }, - { - timestamp: '2024-06-12T21:18:56.377Z', - id: '82fced1d-de48-42db-9f56-e45122dee017', - backingIndex: '.ds-.kibana-elastic-ai-assistant-attack-discovery-default-2024.06.12-000001', - createdAt: '2024-06-12T21:18:56.377Z', - updatedAt: '2024-06-17T20:47:39.372Z', - lastViewedAt: '2024-06-17T20:47:39.372Z', - users: [mockAuthenticatedUser], - namespace: 'default', - status: 'canceled', - alertsContextCount: 20, - apiConfig: { - connectorId: 'my-bedrock', - actionTypeId: '.bedrock', - defaultSystemPromptId: undefined, - model: undefined, - provider: undefined, - }, - attackDiscoveries: mockCurrentAd.attackDiscoveries, - replacements: {}, - generationIntervals: mockCurrentAd.generationIntervals, - averageIntervalMs: mockCurrentAd.averageIntervalMs, - failureReason: undefined, - }, - { - timestamp: '2024-06-12T16:44:23.107Z', - id: 'a4709094-6116-484b-b096-1e8d151cb4b7', - backingIndex: '.ds-.kibana-elastic-ai-assistant-attack-discovery-default-2024.06.12-000001', - createdAt: '2024-06-12T16:44:23.107Z', - updatedAt: '2024-06-17T20:48:16.961Z', - lastViewedAt: '2024-06-17T20:47:16.961Z', - users: [mockAuthenticatedUser], - namespace: 'default', - status: 'succeeded', - alertsContextCount: 0, - apiConfig: { - connectorId: 'my-gen-a2i', - actionTypeId: '.gen-ai', - defaultSystemPromptId: undefined, - model: undefined, - provider: undefined, - }, - attackDiscoveries: [ - ...mockCurrentAd.attackDiscoveries, - ...mockCurrentAd.attackDiscoveries, - ...mockCurrentAd.attackDiscoveries, - ...mockCurrentAd.attackDiscoveries, - ], - replacements: {}, - generationIntervals: mockCurrentAd.generationIntervals, - averageIntervalMs: mockCurrentAd.averageIntervalMs, - failureReason: 'steph threw an error', - }, - ]; - beforeEach(() => { - findAllAttackDiscoveries.mockResolvedValue(mockDiscoveries); - }); - it('returns the formatted stats object', async () => { - const stats = await getAttackDiscoveryStats({ - authenticatedUser: mockAuthenticatedUser, - dataClient: mockDataClient, - }); - expect(stats).toEqual([ - { - hasViewed: true, - status: 'failed', - count: 0, - connectorId: 'my-bedrock-old', - }, - { - hasViewed: false, - status: 'failed', - count: 0, - connectorId: 'my-bedrock-old', - }, - { - hasViewed: false, - status: 'running', - count: 1, - connectorId: 'my-gen-ai', - }, - { - hasViewed: false, - status: 'succeeded', - count: 1, - connectorId: 'my-gpt4o-ai', - }, - { - hasViewed: true, - status: 'canceled', - count: 1, - connectorId: 'my-bedrock', - }, - { - hasViewed: false, - status: 'succeeded', - count: 4, - connectorId: 'my-gen-a2i', - }, - ]); - }); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/handle_graph_error/index.tsx b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/handle_graph_error/index.tsx deleted file mode 100644 index e58b67bdcc1ad..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/handle_graph_error/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { AnalyticsServiceSetup, AuthenticatedUser, Logger } from '@kbn/core/server'; -import { ApiConfig, Replacements } from '@kbn/elastic-assistant-common'; -import { transformError } from '@kbn/securitysolution-es-utils'; - -import { AttackDiscoveryDataClient } from '../../../../../lib/attack_discovery/persistence'; -import { attackDiscoveryStatus } from '../../../helpers/helpers'; -import { ATTACK_DISCOVERY_ERROR_EVENT } from '../../../../../lib/telemetry/event_based_telemetry'; - -export const handleGraphError = async ({ - apiConfig, - attackDiscoveryId, - authenticatedUser, - dataClient, - err, - latestReplacements, - logger, - telemetry, -}: { - apiConfig: ApiConfig; - attackDiscoveryId: string; - authenticatedUser: AuthenticatedUser; - dataClient: AttackDiscoveryDataClient; - err: Error; - latestReplacements: Replacements; - logger: Logger; - telemetry: AnalyticsServiceSetup; -}) => { - try { - logger.error(err); - const error = transformError(err); - const currentAd = await dataClient.getAttackDiscovery({ - id: attackDiscoveryId, - authenticatedUser, - }); - - if (currentAd === null || currentAd?.status === 'canceled') { - return; - } - - await dataClient.updateAttackDiscovery({ - attackDiscoveryUpdateProps: { - attackDiscoveries: [], - status: attackDiscoveryStatus.failed, - id: attackDiscoveryId, - replacements: latestReplacements, - backingIndex: currentAd.backingIndex, - failureReason: error.message, - }, - authenticatedUser, - }); - telemetry.reportEvent(ATTACK_DISCOVERY_ERROR_EVENT.eventType, { - actionTypeId: apiConfig.actionTypeId, - errorMessage: error.message, - model: apiConfig.model, - provider: apiConfig.provider, - }); - } catch (updateErr) { - const updateError = transformError(updateErr); - telemetry.reportEvent(ATTACK_DISCOVERY_ERROR_EVENT.eventType, { - actionTypeId: apiConfig.actionTypeId, - errorMessage: updateError.message, - model: apiConfig.model, - provider: apiConfig.provider, - }); - } -}; diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/invoke_attack_discovery_graph/index.tsx b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/invoke_attack_discovery_graph/index.tsx deleted file mode 100644 index 8a8c49f796500..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/invoke_attack_discovery_graph/index.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ActionsClient } from '@kbn/actions-plugin/server'; -import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import { Logger } from '@kbn/core/server'; -import { ApiConfig, AttackDiscovery, Replacements } from '@kbn/elastic-assistant-common'; -import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen'; -import { ActionsClientLlm } from '@kbn/langchain/server'; -import { PublicMethodsOf } from '@kbn/utility-types'; -import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith'; -import type { Document } from '@langchain/core/documents'; - -import { getDefaultAttackDiscoveryGraph } from '../../../../../lib/attack_discovery/graphs/default_attack_discovery_graph'; -import { - ATTACK_DISCOVERY_GRAPH_RUN_NAME, - ATTACK_DISCOVERY_TAG, -} from '../../../../../lib/attack_discovery/graphs/default_attack_discovery_graph/constants'; -import { GraphState } from '../../../../../lib/attack_discovery/graphs/default_attack_discovery_graph/types'; -import { throwIfErrorCountsExceeded } from '../throw_if_error_counts_exceeded'; -import { getLlmType } from '../../../../utils'; - -export const invokeAttackDiscoveryGraph = async ({ - actionsClient, - alertsIndexPattern, - anonymizationFields, - apiConfig, - connectorTimeout, - esClient, - langSmithProject, - langSmithApiKey, - latestReplacements, - logger, - onNewReplacements, - size, -}: { - actionsClient: PublicMethodsOf; - alertsIndexPattern: string; - anonymizationFields: AnonymizationFieldResponse[]; - apiConfig: ApiConfig; - connectorTimeout: number; - esClient: ElasticsearchClient; - langSmithProject?: string; - langSmithApiKey?: string; - latestReplacements: Replacements; - logger: Logger; - onNewReplacements: (newReplacements: Replacements) => void; - size: number; -}): Promise<{ - anonymizedAlerts: Document[]; - attackDiscoveries: AttackDiscovery[] | null; -}> => { - const llmType = getLlmType(apiConfig.actionTypeId); - const model = apiConfig.model; - const tags = [ATTACK_DISCOVERY_TAG, llmType, model].flatMap((tag) => tag ?? []); - - const traceOptions = { - projectName: langSmithProject, - tracers: [ - ...getLangSmithTracer({ - apiKey: langSmithApiKey, - projectName: langSmithProject, - logger, - }), - ], - }; - - const llm = new ActionsClientLlm({ - actionsClient, - connectorId: apiConfig.connectorId, - llmType, - logger, - temperature: 0, // zero temperature for attack discovery, because we want structured JSON output - timeout: connectorTimeout, - traceOptions, - }); - - if (llm == null) { - throw new Error('LLM is required for attack discoveries'); - } - - const graph = getDefaultAttackDiscoveryGraph({ - alertsIndexPattern, - anonymizationFields, - esClient, - llm, - logger, - onNewReplacements, - replacements: latestReplacements, - size, - }); - - logger?.debug(() => 'invokeAttackDiscoveryGraph: invoking the Attack discovery graph'); - - const result: GraphState = await graph.invoke( - {}, - { - callbacks: [...(traceOptions?.tracers ?? [])], - runName: ATTACK_DISCOVERY_GRAPH_RUN_NAME, - tags, - } - ); - const { - attackDiscoveries, - anonymizedAlerts, - errors, - generationAttempts, - hallucinationFailures, - maxGenerationAttempts, - maxHallucinationFailures, - } = result; - - throwIfErrorCountsExceeded({ - errors, - generationAttempts, - hallucinationFailures, - logger, - maxGenerationAttempts, - maxHallucinationFailures, - }); - - return { anonymizedAlerts, attackDiscoveries }; -}; diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/request_is_valid/index.test.tsx b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/request_is_valid/index.test.tsx deleted file mode 100644 index 9cbf3fa06510d..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/request_is_valid/index.test.tsx +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { KibanaRequest } from '@kbn/core-http-server'; -import type { AttackDiscoveryPostRequestBody } from '@kbn/elastic-assistant-common'; - -import { mockAnonymizationFields } from '../../../../../lib/attack_discovery/graphs/default_attack_discovery_graph/mock/mock_anonymization_fields'; -import { requestIsValid } from '.'; - -describe('requestIsValid', () => { - const alertsIndexPattern = '.alerts-security.alerts-default'; - const replacements = { uuid: 'original_value' }; - const size = 20; - const request = { - body: { - actionTypeId: '.bedrock', - alertsIndexPattern, - anonymizationFields: mockAnonymizationFields, - connectorId: 'test-connector-id', - replacements, - size, - subAction: 'invokeAI', - }, - } as unknown as KibanaRequest; - - it('returns false when the request is missing required anonymization parameters', () => { - const requestMissingAnonymizationParams = { - body: { - alertsIndexPattern: '.alerts-security.alerts-default', - isEnabledKnowledgeBase: false, - size: 20, - }, - } as unknown as KibanaRequest; - - const params = { - alertsIndexPattern, - request: requestMissingAnonymizationParams, // <-- missing required anonymization parameters - size, - }; - - expect(requestIsValid(params)).toBe(false); - }); - - it('returns false when the alertsIndexPattern is undefined', () => { - const params = { - alertsIndexPattern: undefined, // <-- alertsIndexPattern is undefined - request, - size, - }; - - expect(requestIsValid(params)).toBe(false); - }); - - it('returns false when size is undefined', () => { - const params = { - alertsIndexPattern, - request, - size: undefined, // <-- size is undefined - }; - - expect(requestIsValid(params)).toBe(false); - }); - - it('returns false when size is out of range', () => { - const params = { - alertsIndexPattern, - request, - size: 0, // <-- size is out of range - }; - - expect(requestIsValid(params)).toBe(false); - }); - - it('returns true if all required params are provided', () => { - const params = { - alertsIndexPattern, - request, - size, - }; - - expect(requestIsValid(params)).toBe(true); - }); -}); diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/request_is_valid/index.tsx b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/request_is_valid/index.tsx deleted file mode 100644 index 36487d8f6b3e2..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/request_is_valid/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { KibanaRequest } from '@kbn/core/server'; -import { - AttackDiscoveryPostRequestBody, - ExecuteConnectorRequestBody, - sizeIsOutOfRange, -} from '@kbn/elastic-assistant-common'; - -import { requestHasRequiredAnonymizationParams } from '../../../../../lib/langchain/helpers'; - -export const requestIsValid = ({ - alertsIndexPattern, - request, - size, -}: { - alertsIndexPattern: string | undefined; - request: KibanaRequest< - unknown, - unknown, - ExecuteConnectorRequestBody | AttackDiscoveryPostRequestBody - >; - size: number | undefined; -}): boolean => - requestHasRequiredAnonymizationParams(request) && - alertsIndexPattern != null && - size != null && - !sizeIsOutOfRange(size); diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/throw_if_error_counts_exceeded/index.ts b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/throw_if_error_counts_exceeded/index.ts deleted file mode 100644 index 409ee2da74cd2..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/throw_if_error_counts_exceeded/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Logger } from '@kbn/core/server'; - -import * as i18n from './translations'; - -export const throwIfErrorCountsExceeded = ({ - errors, - generationAttempts, - hallucinationFailures, - logger, - maxGenerationAttempts, - maxHallucinationFailures, -}: { - errors: string[]; - generationAttempts: number; - hallucinationFailures: number; - logger?: Logger; - maxGenerationAttempts: number; - maxHallucinationFailures: number; -}): void => { - if (hallucinationFailures >= maxHallucinationFailures) { - const hallucinationFailuresError = `${i18n.MAX_HALLUCINATION_FAILURES( - hallucinationFailures - )}\n${errors.join(',\n')}`; - - logger?.error(hallucinationFailuresError); - throw new Error(hallucinationFailuresError); - } - - if (generationAttempts >= maxGenerationAttempts) { - const generationAttemptsError = `${i18n.MAX_GENERATION_ATTEMPTS( - generationAttempts - )}\n${errors.join(',\n')}`; - - logger?.error(generationAttemptsError); - throw new Error(generationAttemptsError); - } -}; diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/throw_if_error_counts_exceeded/translations.ts b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/throw_if_error_counts_exceeded/translations.ts deleted file mode 100644 index fbe06d0e73b2a..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/helpers/throw_if_error_counts_exceeded/translations.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const MAX_HALLUCINATION_FAILURES = (hallucinationFailures: number) => - i18n.translate( - 'xpack.elasticAssistantPlugin.attackDiscovery.defaultAttackDiscoveryGraph.nodes.retriever.helpers.throwIfErrorCountsExceeded.maxHallucinationFailuresErrorMessage', - { - defaultMessage: - 'Maximum hallucination failures ({hallucinationFailures}) reached. Try sending fewer alerts to this model.', - values: { hallucinationFailures }, - } - ); - -export const MAX_GENERATION_ATTEMPTS = (generationAttempts: number) => - i18n.translate( - 'xpack.elasticAssistantPlugin.attackDiscovery.defaultAttackDiscoveryGraph.nodes.retriever.helpers.throwIfErrorCountsExceeded.maxGenerationAttemptsErrorMessage', - { - defaultMessage: - 'Maximum generation attempts ({generationAttempts}) reached. Try sending fewer alerts to this model.', - values: { generationAttempts }, - } - ); diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.test.ts b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post_attack_discovery.test.ts similarity index 79% rename from x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.test.ts rename to x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post_attack_discovery.test.ts index d50987317b0e3..cbd3e6063fbd2 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.test.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post_attack_discovery.test.ts @@ -7,27 +7,22 @@ import { AuthenticatedUser } from '@kbn/core-security-common'; import { postAttackDiscoveryRoute } from './post_attack_discovery'; -import { serverMock } from '../../../__mocks__/server'; -import { requestContextMock } from '../../../__mocks__/request_context'; +import { serverMock } from '../../__mocks__/server'; +import { requestContextMock } from '../../__mocks__/request_context'; import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; import { actionsMock } from '@kbn/actions-plugin/server/mocks'; -import { AttackDiscoveryDataClient } from '../../../lib/attack_discovery/persistence'; -import { transformESSearchToAttackDiscovery } from '../../../lib/attack_discovery/persistence/transforms/transforms'; -import { getAttackDiscoverySearchEsMock } from '../../../__mocks__/attack_discovery_schema.mock'; -import { postAttackDiscoveryRequest } from '../../../__mocks__/request'; +import { AttackDiscoveryDataClient } from '../../ai_assistant_data_clients/attack_discovery'; +import { transformESSearchToAttackDiscovery } from '../../ai_assistant_data_clients/attack_discovery/transforms'; +import { getAttackDiscoverySearchEsMock } from '../../__mocks__/attack_discovery_schema.mock'; +import { postAttackDiscoveryRequest } from '../../__mocks__/request'; import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants'; import { AttackDiscoveryPostRequestBody } from '@kbn/elastic-assistant-common'; - -import { updateAttackDiscoveryStatusToRunning } from '../helpers/helpers'; - -jest.mock('../helpers/helpers', () => { - const original = jest.requireActual('../helpers/helpers'); - - return { - ...original, - updateAttackDiscoveryStatusToRunning: jest.fn(), - }; -}); +import { + getAssistantTool, + getAssistantToolParams, + updateAttackDiscoveryStatusToRunning, +} from './helpers'; +jest.mock('./helpers'); const { clients, context } = requestContextMock.createTools(); const server: ReturnType = serverMock.create(); @@ -77,6 +72,8 @@ describe('postAttackDiscoveryRoute', () => { context.elasticAssistant.actions = actionsMock.createStart(); postAttackDiscoveryRoute(server.router); findAttackDiscoveryByConnectorId.mockResolvedValue(mockCurrentAd); + (getAssistantTool as jest.Mock).mockReturnValue({ getTool: jest.fn() }); + (getAssistantToolParams as jest.Mock).mockReturnValue({ tool: 'tool' }); (updateAttackDiscoveryStatusToRunning as jest.Mock).mockResolvedValue({ currentAd: runningAd, attackDiscoveryId: mockCurrentAd.id, @@ -120,6 +117,15 @@ describe('postAttackDiscoveryRoute', () => { }); }); + it('should handle assistantTool null response', async () => { + (getAssistantTool as jest.Mock).mockReturnValue(null); + const response = await server.inject( + postAttackDiscoveryRequest(mockRequestBody), + requestContextMock.convertContext(context) + ); + expect(response.status).toEqual(404); + }); + it('should handle updateAttackDiscoveryStatusToRunning error', async () => { (updateAttackDiscoveryStatusToRunning as jest.Mock).mockRejectedValue(new Error('Oh no!')); const response = await server.inject( diff --git a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.ts b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post_attack_discovery.ts similarity index 79% rename from x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.ts rename to x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post_attack_discovery.ts index b0273741bdf5e..b9c680dde3d1d 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post/post_attack_discovery.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/attack_discovery/post_attack_discovery.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/schemas/common'; import { type IKibanaResponse, IRouter, Logger } from '@kbn/core/server'; import { AttackDiscoveryPostRequestBody, @@ -12,17 +13,20 @@ import { ELASTIC_AI_ASSISTANT_INTERNAL_API_VERSION, Replacements, } from '@kbn/elastic-assistant-common'; -import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/schemas/common'; import { transformError } from '@kbn/securitysolution-es-utils'; import moment from 'moment/moment'; -import { ATTACK_DISCOVERY } from '../../../../common/constants'; -import { handleGraphError } from './helpers/handle_graph_error'; -import { updateAttackDiscoveries, updateAttackDiscoveryStatusToRunning } from '../helpers/helpers'; -import { buildResponse } from '../../../lib/build_response'; -import { ElasticAssistantRequestHandlerContext } from '../../../types'; -import { invokeAttackDiscoveryGraph } from './helpers/invoke_attack_discovery_graph'; -import { requestIsValid } from './helpers/request_is_valid'; +import { ATTACK_DISCOVERY } from '../../../common/constants'; +import { + getAssistantTool, + getAssistantToolParams, + handleToolError, + updateAttackDiscoveries, + updateAttackDiscoveryStatusToRunning, +} from './helpers'; +import { DEFAULT_PLUGIN_NAME, getPluginNameFromRequest } from '../helpers'; +import { buildResponse } from '../../lib/build_response'; +import { ElasticAssistantRequestHandlerContext } from '../../types'; const ROUTE_HANDLER_TIMEOUT = 10 * 60 * 1000; // 10 * 60 seconds = 10 minutes const LANG_CHAIN_TIMEOUT = ROUTE_HANDLER_TIMEOUT - 10_000; // 9 minutes 50 seconds @@ -81,6 +85,11 @@ export const postAttackDiscoveryRoute = ( statusCode: 500, }); } + const pluginName = getPluginNameFromRequest({ + request, + defaultPluginName: DEFAULT_PLUGIN_NAME, + logger, + }); // get parameters from the request body const alertsIndexPattern = decodeURIComponent(request.body.alertsIndexPattern); @@ -93,19 +102,6 @@ export const postAttackDiscoveryRoute = ( size, } = request.body; - if ( - !requestIsValid({ - alertsIndexPattern, - request, - size, - }) - ) { - return resp.error({ - body: 'Bad Request', - statusCode: 400, - }); - } - // get an Elasticsearch client for the authenticated user: const esClient = (await context.core).elasticsearch.client.asCurrentUser; @@ -115,45 +111,59 @@ export const postAttackDiscoveryRoute = ( latestReplacements = { ...latestReplacements, ...newReplacements }; }; - const { currentAd, attackDiscoveryId } = await updateAttackDiscoveryStatusToRunning( - dataClient, - authenticatedUser, - apiConfig, - size + const assistantTool = getAssistantTool( + (await context.elasticAssistant).getRegisteredTools, + pluginName ); - // Don't await the results of invoking the graph; (just the metadata will be returned from the route handler): - invokeAttackDiscoveryGraph({ + if (!assistantTool) { + return response.notFound(); // attack discovery tool not found + } + + const assistantToolParams = getAssistantToolParams({ actionsClient, alertsIndexPattern, anonymizationFields, apiConfig, - connectorTimeout: CONNECTOR_TIMEOUT, esClient, + latestReplacements, + connectorTimeout: CONNECTOR_TIMEOUT, + langChainTimeout: LANG_CHAIN_TIMEOUT, langSmithProject, langSmithApiKey, - latestReplacements, logger, onNewReplacements, + request, size, - }) - .then(({ anonymizedAlerts, attackDiscoveries }) => + }); + + // invoke the attack discovery tool: + const toolInstance = assistantTool.getTool(assistantToolParams); + + const { currentAd, attackDiscoveryId } = await updateAttackDiscoveryStatusToRunning( + dataClient, + authenticatedUser, + apiConfig + ); + + toolInstance + ?.invoke('') + .then((rawAttackDiscoveries: string) => updateAttackDiscoveries({ - anonymizedAlerts, apiConfig, - attackDiscoveries, attackDiscoveryId, authenticatedUser, dataClient, latestReplacements, logger, + rawAttackDiscoveries, size, startTime, telemetry, }) ) .catch((err) => - handleGraphError({ + handleToolError({ apiConfig, attackDiscoveryId, authenticatedUser, diff --git a/x-pack/plugins/elastic_assistant/server/routes/evaluate/get_graphs_from_names/index.ts b/x-pack/plugins/elastic_assistant/server/routes/evaluate/get_graphs_from_names/index.ts deleted file mode 100644 index c0320c9ff6adf..0000000000000 --- a/x-pack/plugins/elastic_assistant/server/routes/evaluate/get_graphs_from_names/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - ASSISTANT_GRAPH_MAP, - AssistantGraphMetadata, - AttackDiscoveryGraphMetadata, -} from '../../../lib/langchain/graphs'; - -export interface GetGraphsFromNamesResults { - attackDiscoveryGraphs: AttackDiscoveryGraphMetadata[]; - assistantGraphs: AssistantGraphMetadata[]; -} - -export const getGraphsFromNames = (graphNames: string[]): GetGraphsFromNamesResults => - graphNames.reduce( - (acc, graphName) => { - const graph = ASSISTANT_GRAPH_MAP[graphName]; - if (graph != null) { - return graph.graphType === 'assistant' - ? { ...acc, assistantGraphs: [...acc.assistantGraphs, graph] } - : { ...acc, attackDiscoveryGraphs: [...acc.attackDiscoveryGraphs, graph] }; - } - - return acc; - }, - { - attackDiscoveryGraphs: [], - assistantGraphs: [], - } - ); diff --git a/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts b/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts index eb12946a9b61f..29a7527964677 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/evaluate/post_evaluate.ts @@ -29,7 +29,6 @@ import { createStructuredChatAgent, createToolCallingAgent, } from 'langchain/agents'; -import { omit } from 'lodash/fp'; import { buildResponse } from '../../lib/build_response'; import { AssistantDataClients } from '../../lib/langchain/executors/types'; import { AssistantToolParams, ElasticAssistantRequestHandlerContext, GetElser } from '../../types'; @@ -37,7 +36,6 @@ import { DEFAULT_PLUGIN_NAME, isV2KnowledgeBaseEnabled, performChecks } from '.. import { fetchLangSmithDataset } from './utils'; import { transformESSearchToAnonymizationFields } from '../../ai_assistant_data_clients/anonymization_fields/helpers'; import { EsAnonymizationFieldsSchema } from '../../ai_assistant_data_clients/anonymization_fields/types'; -import { evaluateAttackDiscovery } from '../../lib/attack_discovery/evaluation'; import { DefaultAssistantGraph, getDefaultAssistantGraph, @@ -49,12 +47,9 @@ import { structuredChatAgentPrompt, } from '../../lib/langchain/graphs/default_assistant_graph/prompts'; import { getLlmClass, getLlmType, isOpenSourceModel } from '../utils'; -import { getGraphsFromNames } from './get_graphs_from_names'; const DEFAULT_SIZE = 20; const ROUTE_HANDLER_TIMEOUT = 10 * 60 * 1000; // 10 * 60 seconds = 10 minutes -const LANG_CHAIN_TIMEOUT = ROUTE_HANDLER_TIMEOUT - 10_000; // 9 minutes 50 seconds -const CONNECTOR_TIMEOUT = LANG_CHAIN_TIMEOUT - 10_000; // 9 minutes 40 seconds export const postEvaluateRoute = ( router: IRouter, @@ -111,10 +106,8 @@ export const postEvaluateRoute = ( const { alertsIndexPattern, datasetName, - evaluatorConnectorId, graphs: graphNames, langSmithApiKey, - langSmithProject, connectorIds, size, replacements, @@ -131,9 +124,7 @@ export const postEvaluateRoute = ( logger.info('postEvaluateRoute:'); logger.info(`request.query:\n${JSON.stringify(request.query, null, 2)}`); - logger.info( - `request.body:\n${JSON.stringify(omit(['langSmithApiKey'], request.body), null, 2)}` - ); + logger.info(`request.body:\n${JSON.stringify(request.body, null, 2)}`); logger.info(`Evaluation ID: ${evaluationId}`); const totalExecutions = connectorIds.length * graphNames.length * dataset.length; @@ -179,38 +170,6 @@ export const postEvaluateRoute = ( // Fetch any tools registered to the security assistant const assistantTools = assistantContext.getRegisteredTools(DEFAULT_PLUGIN_NAME); - const { attackDiscoveryGraphs } = getGraphsFromNames(graphNames); - - if (attackDiscoveryGraphs.length > 0) { - try { - // NOTE: we don't wait for the evaluation to finish here, because - // the client will retry / timeout when evaluations take too long - void evaluateAttackDiscovery({ - actionsClient, - alertsIndexPattern, - attackDiscoveryGraphs, - connectors, - connectorTimeout: CONNECTOR_TIMEOUT, - datasetName, - esClient, - evaluationId, - evaluatorConnectorId, - langSmithApiKey, - langSmithProject, - logger, - runName, - size, - }); - } catch (err) { - logger.error(() => `Error evaluating attack discovery: ${err}`); - } - - // Return early if we're only running attack discovery graphs - return response.ok({ - body: { evaluationId, success: true }, - }); - } - const graphs: Array<{ name: string; graph: DefaultAssistantGraph; diff --git a/x-pack/plugins/elastic_assistant/server/routes/evaluate/utils.ts b/x-pack/plugins/elastic_assistant/server/routes/evaluate/utils.ts index 0260c47b4bd29..34f009e266515 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/evaluate/utils.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/evaluate/utils.ts @@ -21,7 +21,7 @@ export const fetchLangSmithDataset = async ( logger: Logger, langSmithApiKey?: string ): Promise => { - if (datasetName === undefined || (langSmithApiKey == null && !isLangSmithEnabled())) { + if (datasetName === undefined || !isLangSmithEnabled()) { throw new Error('LangSmith dataset name not provided or LangSmith not enabled'); } diff --git a/x-pack/plugins/elastic_assistant/server/routes/index.ts b/x-pack/plugins/elastic_assistant/server/routes/index.ts index a6d7a4298c2b7..43e1229250f46 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/index.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/index.ts @@ -9,8 +9,8 @@ export { postActionsConnectorExecuteRoute } from './post_actions_connector_execute'; // Attack Discovery -export { postAttackDiscoveryRoute } from './attack_discovery/post/post_attack_discovery'; -export { getAttackDiscoveryRoute } from './attack_discovery/get/get_attack_discovery'; +export { postAttackDiscoveryRoute } from './attack_discovery/post_attack_discovery'; +export { getAttackDiscoveryRoute } from './attack_discovery/get_attack_discovery'; // Knowledge Base export { deleteKnowledgeBaseRoute } from './knowledge_base/delete_knowledge_base'; diff --git a/x-pack/plugins/elastic_assistant/server/routes/register_routes.ts b/x-pack/plugins/elastic_assistant/server/routes/register_routes.ts index 7898629e15b5c..56eb9760e442a 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/register_routes.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/register_routes.ts @@ -7,9 +7,9 @@ import type { Logger } from '@kbn/core/server'; -import { cancelAttackDiscoveryRoute } from './attack_discovery/post/cancel/cancel_attack_discovery'; -import { getAttackDiscoveryRoute } from './attack_discovery/get/get_attack_discovery'; -import { postAttackDiscoveryRoute } from './attack_discovery/post/post_attack_discovery'; +import { cancelAttackDiscoveryRoute } from './attack_discovery/cancel_attack_discovery'; +import { getAttackDiscoveryRoute } from './attack_discovery/get_attack_discovery'; +import { postAttackDiscoveryRoute } from './attack_discovery/post_attack_discovery'; import { ElasticAssistantPluginRouter, GetElser } from '../types'; import { createConversationRoute } from './user_conversations/create_route'; import { deleteConversationRoute } from './user_conversations/delete_route'; diff --git a/x-pack/plugins/elastic_assistant/server/types.ts b/x-pack/plugins/elastic_assistant/server/types.ts index e84b97ab43d7a..45bd5a4149b58 100755 --- a/x-pack/plugins/elastic_assistant/server/types.ts +++ b/x-pack/plugins/elastic_assistant/server/types.ts @@ -43,10 +43,10 @@ import { ActionsClientGeminiChatModel, ActionsClientLlm, } from '@kbn/langchain/server'; -import type { InferenceServerStart } from '@kbn/inference-plugin/server'; +import type { InferenceServerStart } from '@kbn/inference-plugin/server'; import type { GetAIAssistantKnowledgeBaseDataClientParams } from './ai_assistant_data_clients/knowledge_base'; -import { AttackDiscoveryDataClient } from './lib/attack_discovery/persistence'; +import { AttackDiscoveryDataClient } from './ai_assistant_data_clients/attack_discovery'; import { AIAssistantConversationsDataClient } from './ai_assistant_data_clients/conversations'; import type { GetRegisteredFeatures, GetRegisteredTools } from './services/app_context'; import { AIAssistantDataClient } from './ai_assistant_data_clients'; diff --git a/x-pack/plugins/security_solution/public/attack_discovery/attack_discovery_panel/actionable_summary/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/attack_discovery_panel/actionable_summary/index.tsx index dd995d115b6c3..885ab18c879a7 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/attack_discovery_panel/actionable_summary/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/attack_discovery_panel/actionable_summary/index.tsx @@ -6,11 +6,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import { - replaceAnonymizedValuesWithOriginalValues, - type AttackDiscovery, - type Replacements, -} from '@kbn/elastic-assistant-common'; +import type { AttackDiscovery, Replacements } from '@kbn/elastic-assistant-common'; import React, { useMemo } from 'react'; import { AttackDiscoveryMarkdownFormatter } from '../../attack_discovery_markdown_formatter'; @@ -27,41 +23,26 @@ const ActionableSummaryComponent: React.FC = ({ replacements, showAnonymized = false, }) => { - const entitySummary = useMemo( + const entitySummaryMarkdownWithReplacements = useMemo( () => - showAnonymized - ? attackDiscovery.entitySummaryMarkdown - : replaceAnonymizedValuesWithOriginalValues({ - messageContent: attackDiscovery.entitySummaryMarkdown ?? '', - replacements: { ...replacements }, - }), - - [attackDiscovery.entitySummaryMarkdown, replacements, showAnonymized] - ); - - // title will be used as a fallback if entitySummaryMarkdown is empty - const title = useMemo( - () => - showAnonymized - ? attackDiscovery.title - : replaceAnonymizedValuesWithOriginalValues({ - messageContent: attackDiscovery.title, - replacements: { ...replacements }, - }), - - [attackDiscovery.title, replacements, showAnonymized] + Object.entries(replacements ?? {}).reduce( + (acc, [key, value]) => acc.replace(key, value), + attackDiscovery.entitySummaryMarkdown + ), + [attackDiscovery.entitySummaryMarkdown, replacements] ); - const entitySummaryOrTitle = - entitySummary != null && entitySummary.length > 0 ? entitySummary : title; - return ( diff --git a/x-pack/plugins/security_solution/public/attack_discovery/attack_discovery_panel/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/attack_discovery_panel/index.tsx index c6ac9c70e8413..2aaac0449886a 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/attack_discovery_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/attack_discovery_panel/index.tsx @@ -49,15 +49,8 @@ const AttackDiscoveryPanelComponent: React.FC = ({ ); const buttonContent = useMemo( - () => ( - - ), - [attackDiscovery.title, replacements, showAnonymized] + () => <Title isLoading={false} title={attackDiscovery.title} />, + [attackDiscovery.title] ); return ( diff --git a/x-pack/plugins/security_solution/public/attack_discovery/attack_discovery_panel/title/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/attack_discovery_panel/title/index.tsx index 13326a07adc70..4b0375e4fe503 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/attack_discovery_panel/title/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/attack_discovery_panel/title/index.tsx @@ -7,41 +7,20 @@ import { EuiFlexGroup, EuiFlexItem, EuiSkeletonTitle, EuiTitle, useEuiTheme } from '@elastic/eui'; import { AssistantAvatar } from '@kbn/elastic-assistant'; -import { - replaceAnonymizedValuesWithOriginalValues, - type Replacements, -} from '@kbn/elastic-assistant-common'; import { css } from '@emotion/react'; -import React, { useMemo } from 'react'; +import React from 'react'; const AVATAR_SIZE = 24; // px interface Props { isLoading: boolean; - replacements?: Replacements; - showAnonymized?: boolean; title: string; } -const TitleComponent: React.FC<Props> = ({ - isLoading, - replacements, - showAnonymized = false, - title, -}) => { +const TitleComponent: React.FC<Props> = ({ isLoading, title }) => { const { euiTheme } = useEuiTheme(); - const titleWithReplacements = useMemo( - () => - replaceAnonymizedValuesWithOriginalValues({ - messageContent: title, - replacements: { ...replacements }, - }), - - [replacements, title] - ); - return ( <EuiFlexGroup alignItems="center" data-test-subj="title" gutterSize="s"> <EuiFlexItem @@ -74,7 +53,7 @@ const TitleComponent: React.FC<Props> = ({ /> ) : ( <EuiTitle data-test-subj="titleText" size="xs"> - <h2>{showAnonymized ? title : titleWithReplacements}</h2> + <h2>{title}</h2> </EuiTitle> )} </EuiFlexItem> diff --git a/x-pack/plugins/security_solution/public/attack_discovery/get_attack_discovery_markdown/get_attack_discovery_markdown.ts b/x-pack/plugins/security_solution/public/attack_discovery/get_attack_discovery_markdown/get_attack_discovery_markdown.ts index 0ae524c25ee95..5309ef1de6bb2 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/get_attack_discovery_markdown/get_attack_discovery_markdown.ts +++ b/x-pack/plugins/security_solution/public/attack_discovery/get_attack_discovery_markdown/get_attack_discovery_markdown.ts @@ -56,7 +56,7 @@ export const getAttackDiscoveryMarkdown = ({ replacements?: Replacements; }): string => { const title = getMarkdownFields(attackDiscovery.title); - const entitySummaryMarkdown = getMarkdownFields(attackDiscovery.entitySummaryMarkdown ?? ''); + const entitySummaryMarkdown = getMarkdownFields(attackDiscovery.entitySummaryMarkdown); const summaryMarkdown = getMarkdownFields(attackDiscovery.summaryMarkdown); const detailsMarkdown = getMarkdownFields(attackDiscovery.detailsMarkdown); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/hooks/use_poll_api.tsx b/x-pack/plugins/security_solution/public/attack_discovery/hooks/use_poll_api.tsx index ab0a5ac4ede96..874a4d1c99ded 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/hooks/use_poll_api.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/hooks/use_poll_api.tsx @@ -106,9 +106,7 @@ export const usePollApi = ({ ...attackDiscovery, id: attackDiscovery.id ?? uuid.v4(), detailsMarkdown: replaceNewlineLiterals(attackDiscovery.detailsMarkdown), - entitySummaryMarkdown: replaceNewlineLiterals( - attackDiscovery.entitySummaryMarkdown ?? '' - ), + entitySummaryMarkdown: replaceNewlineLiterals(attackDiscovery.entitySummaryMarkdown), summaryMarkdown: replaceNewlineLiterals(attackDiscovery.summaryMarkdown), })), }; @@ -125,7 +123,7 @@ export const usePollApi = ({ const rawResponse = await http.fetch( `/internal/elastic_assistant/attack_discovery/cancel/${connectorId}`, { - method: 'POST', + method: 'PUT', version: ELASTIC_AI_ASSISTANT_INTERNAL_API_VERSION, } ); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/animated_counter/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/animated_counter/index.tsx index 533b95bf7087f..5dd4cb8fc4267 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/animated_counter/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/animated_counter/index.tsx @@ -52,7 +52,7 @@ const AnimatedCounterComponent: React.FC<Props> = ({ animationDurationMs = 1000 css={css` height: 32px; margin-right: ${euiTheme.size.xs}; - width: ${count < 100 ? 40 : 60}px; + width: ${count < 100 ? 40 : 53}px; `} data-test-subj="animatedCounter" ref={d3Ref} diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/index.test.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/index.test.tsx index 0707950383046..56b2205b28726 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/index.test.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/index.test.tsx @@ -16,8 +16,6 @@ jest.mock('../../../assistant/use_assistant_availability'); describe('EmptyPrompt', () => { const alertsCount = 20; - const aiConnectorsCount = 2; - const attackDiscoveriesCount = 0; const onGenerate = jest.fn(); beforeEach(() => { @@ -35,8 +33,6 @@ describe('EmptyPrompt', () => { <TestProviders> <EmptyPrompt alertsCount={alertsCount} - aiConnectorsCount={aiConnectorsCount} - attackDiscoveriesCount={attackDiscoveriesCount} isLoading={false} isDisabled={false} onGenerate={onGenerate} @@ -73,34 +69,8 @@ describe('EmptyPrompt', () => { }); describe('when loading is true', () => { - beforeEach(() => { - (useAssistantAvailability as jest.Mock).mockReturnValue({ - hasAssistantPrivilege: true, - isAssistantEnabled: true, - }); - - render( - <TestProviders> - <EmptyPrompt - aiConnectorsCount={2} // <-- non-null - attackDiscoveriesCount={0} // <-- no discoveries - alertsCount={alertsCount} - isLoading={true} // <-- loading - isDisabled={false} - onGenerate={onGenerate} - /> - </TestProviders> - ); - }); - - it('returns null', () => { - const emptyPrompt = screen.queryByTestId('emptyPrompt'); + const isLoading = true; - expect(emptyPrompt).not.toBeInTheDocument(); - }); - }); - - describe('when aiConnectorsCount is null', () => { beforeEach(() => { (useAssistantAvailability as jest.Mock).mockReturnValue({ hasAssistantPrivilege: true, @@ -110,10 +80,8 @@ describe('EmptyPrompt', () => { render( <TestProviders> <EmptyPrompt - aiConnectorsCount={null} // <-- null - attackDiscoveriesCount={0} // <-- no discoveries alertsCount={alertsCount} - isLoading={false} // <-- not loading + isLoading={isLoading} isDisabled={false} onGenerate={onGenerate} /> @@ -121,38 +89,10 @@ describe('EmptyPrompt', () => { ); }); - it('returns null', () => { - const emptyPrompt = screen.queryByTestId('emptyPrompt'); - - expect(emptyPrompt).not.toBeInTheDocument(); - }); - }); - - describe('when there are attack discoveries', () => { - beforeEach(() => { - (useAssistantAvailability as jest.Mock).mockReturnValue({ - hasAssistantPrivilege: true, - isAssistantEnabled: true, - }); - - render( - <TestProviders> - <EmptyPrompt - aiConnectorsCount={2} // <-- non-null - attackDiscoveriesCount={7} // there are discoveries - alertsCount={alertsCount} - isLoading={false} // <-- not loading - isDisabled={false} - onGenerate={onGenerate} - /> - </TestProviders> - ); - }); - - it('returns null', () => { - const emptyPrompt = screen.queryByTestId('emptyPrompt'); + it('disables the generate button while loading', () => { + const generateButton = screen.getByTestId('generate'); - expect(emptyPrompt).not.toBeInTheDocument(); + expect(generateButton).toBeDisabled(); }); }); @@ -169,8 +109,6 @@ describe('EmptyPrompt', () => { <TestProviders> <EmptyPrompt alertsCount={alertsCount} - aiConnectorsCount={2} // <-- non-null - attackDiscoveriesCount={0} // <-- no discoveries isLoading={false} isDisabled={isDisabled} onGenerate={onGenerate} diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/index.tsx index 3d89f5be87030..75c8533efcc92 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_prompt/index.tsx @@ -7,6 +7,7 @@ import { AssistantAvatar } from '@kbn/elastic-assistant'; import { + EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, @@ -14,28 +15,24 @@ import { EuiLink, EuiSpacer, EuiText, + EuiToolTip, useEuiTheme, } from '@elastic/eui'; import { css } from '@emotion/react'; import React, { useMemo } from 'react'; import { AnimatedCounter } from './animated_counter'; -import { Generate } from '../generate'; import * as i18n from './translations'; interface Props { - aiConnectorsCount: number | null; // null when connectors are not configured alertsCount: number; - attackDiscoveriesCount: number; isDisabled?: boolean; isLoading: boolean; onGenerate: () => void; } const EmptyPromptComponent: React.FC<Props> = ({ - aiConnectorsCount, alertsCount, - attackDiscoveriesCount, isLoading, isDisabled = false, onGenerate, @@ -113,12 +110,24 @@ const EmptyPromptComponent: React.FC<Props> = ({ ); const actions = useMemo(() => { - return <Generate isLoading={isLoading} isDisabled={isDisabled} onGenerate={onGenerate} />; - }, [isDisabled, isLoading, onGenerate]); + const disabled = isLoading || isDisabled; - if (isLoading || aiConnectorsCount == null || attackDiscoveriesCount > 0) { - return null; - } + return ( + <EuiToolTip + content={disabled ? i18n.SELECT_A_CONNECTOR : null} + data-test-subj="generateTooltip" + > + <EuiButton + color="primary" + data-test-subj="generate" + disabled={disabled} + onClick={onGenerate} + > + {i18n.GENERATE} + </EuiButton> + </EuiToolTip> + ); + }, [isDisabled, isLoading, onGenerate]); return ( <EuiFlexGroup diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_states/helpers/show_empty_states/index.ts b/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_states/helpers/show_empty_states/index.ts deleted file mode 100644 index e2c7018ef5826..0000000000000 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_states/helpers/show_empty_states/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - showEmptyPrompt, - showFailurePrompt, - showNoAlertsPrompt, - showWelcomePrompt, -} from '../../../helpers'; - -export const showEmptyStates = ({ - aiConnectorsCount, - alertsContextCount, - attackDiscoveriesCount, - connectorId, - failureReason, - isLoading, -}: { - aiConnectorsCount: number | null; - alertsContextCount: number | null; - attackDiscoveriesCount: number; - connectorId: string | undefined; - failureReason: string | null; - isLoading: boolean; -}): boolean => { - const showWelcome = showWelcomePrompt({ aiConnectorsCount, isLoading }); - const showFailure = showFailurePrompt({ connectorId, failureReason, isLoading }); - const showNoAlerts = showNoAlertsPrompt({ alertsContextCount, connectorId, isLoading }); - const showEmpty = showEmptyPrompt({ aiConnectorsCount, attackDiscoveriesCount, isLoading }); - - return showWelcome || showFailure || showNoAlerts || showEmpty; -}; diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_states/index.test.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_states/index.test.tsx index 9eacd696a2ff1..3b5b87ada83ec 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_states/index.test.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_states/index.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS } from '@kbn/elastic-assistant'; import { render, screen } from '@testing-library/react'; import React from 'react'; @@ -19,6 +18,7 @@ describe('EmptyStates', () => { const aiConnectorsCount = 0; // <-- no connectors configured const alertsContextCount = null; + const alertsCount = 0; const attackDiscoveriesCount = 0; const connectorId = undefined; const isLoading = false; @@ -29,12 +29,12 @@ describe('EmptyStates', () => { <EmptyStates aiConnectorsCount={aiConnectorsCount} alertsContextCount={alertsContextCount} + alertsCount={alertsCount} attackDiscoveriesCount={attackDiscoveriesCount} connectorId={connectorId} failureReason={null} isLoading={isLoading} onGenerate={onGenerate} - upToAlertsCount={DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS} /> </TestProviders> ); @@ -59,6 +59,7 @@ describe('EmptyStates', () => { const aiConnectorsCount = 1; const alertsContextCount = 0; // <-- no alerts to analyze + const alertsCount = 0; const attackDiscoveriesCount = 0; const connectorId = 'test-connector-id'; const isLoading = false; @@ -69,12 +70,12 @@ describe('EmptyStates', () => { <EmptyStates aiConnectorsCount={aiConnectorsCount} alertsContextCount={alertsContextCount} + alertsCount={alertsCount} attackDiscoveriesCount={attackDiscoveriesCount} connectorId={connectorId} failureReason={null} isLoading={isLoading} onGenerate={onGenerate} - upToAlertsCount={DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS} /> </TestProviders> ); @@ -103,7 +104,8 @@ describe('EmptyStates', () => { const aiConnectorsCount = 1; const alertsContextCount = 10; - const attackDiscoveriesCount = 0; + const alertsCount = 10; + const attackDiscoveriesCount = 10; const connectorId = 'test-connector-id'; const isLoading = false; const onGenerate = jest.fn(); @@ -113,12 +115,12 @@ describe('EmptyStates', () => { <EmptyStates aiConnectorsCount={aiConnectorsCount} alertsContextCount={alertsContextCount} + alertsCount={alertsCount} attackDiscoveriesCount={attackDiscoveriesCount} connectorId={connectorId} failureReason={"you're a failure"} isLoading={isLoading} onGenerate={onGenerate} - upToAlertsCount={DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS} /> </TestProviders> ); @@ -147,7 +149,8 @@ describe('EmptyStates', () => { const aiConnectorsCount = 1; const alertsContextCount = 10; - const attackDiscoveriesCount = 0; + const alertsCount = 10; + const attackDiscoveriesCount = 10; const connectorId = 'test-connector-id'; const failureReason = 'this failure should NOT be displayed, because we are loading'; // <-- failureReason is provided const isLoading = true; // <-- loading data @@ -158,12 +161,12 @@ describe('EmptyStates', () => { <EmptyStates aiConnectorsCount={aiConnectorsCount} alertsContextCount={alertsContextCount} + alertsCount={alertsCount} attackDiscoveriesCount={attackDiscoveriesCount} connectorId={connectorId} failureReason={failureReason} isLoading={isLoading} onGenerate={onGenerate} - upToAlertsCount={DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS} /> </TestProviders> ); @@ -192,7 +195,8 @@ describe('EmptyStates', () => { const aiConnectorsCount = 1; const alertsContextCount = 20; // <-- alerts were sent as context to be analyzed - const attackDiscoveriesCount = 0; + const alertsCount = 0; // <-- no alerts contributed to attack discoveries + const attackDiscoveriesCount = 0; // <-- no attack discoveries were generated from the alerts const connectorId = 'test-connector-id'; const isLoading = false; const onGenerate = jest.fn(); @@ -202,12 +206,12 @@ describe('EmptyStates', () => { <EmptyStates aiConnectorsCount={aiConnectorsCount} alertsContextCount={alertsContextCount} + alertsCount={alertsCount} attackDiscoveriesCount={attackDiscoveriesCount} connectorId={connectorId} failureReason={null} isLoading={isLoading} onGenerate={onGenerate} - upToAlertsCount={DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS} /> </TestProviders> ); @@ -236,6 +240,7 @@ describe('EmptyStates', () => { const aiConnectorsCount = null; // <-- no connectors configured const alertsContextCount = 20; // <-- alerts were sent as context to be analyzed + const alertsCount = 0; const attackDiscoveriesCount = 0; const connectorId = undefined; const isLoading = false; @@ -246,12 +251,12 @@ describe('EmptyStates', () => { <EmptyStates aiConnectorsCount={aiConnectorsCount} alertsContextCount={alertsContextCount} + alertsCount={alertsCount} attackDiscoveriesCount={attackDiscoveriesCount} connectorId={connectorId} failureReason={null} isLoading={isLoading} onGenerate={onGenerate} - upToAlertsCount={DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS} /> </TestProviders> ); @@ -282,6 +287,7 @@ describe('EmptyStates', () => { const aiConnectorsCount = 0; // <-- no connectors configured (welcome prompt should be shown if not loading) const alertsContextCount = null; + const alertsCount = 0; const attackDiscoveriesCount = 0; const connectorId = undefined; const isLoading = true; // <-- loading data @@ -292,12 +298,12 @@ describe('EmptyStates', () => { <EmptyStates aiConnectorsCount={aiConnectorsCount} alertsContextCount={alertsContextCount} + alertsCount={alertsCount} attackDiscoveriesCount={attackDiscoveriesCount} connectorId={connectorId} failureReason={null} isLoading={isLoading} onGenerate={onGenerate} - upToAlertsCount={DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS} /> </TestProviders> ); @@ -332,7 +338,8 @@ describe('EmptyStates', () => { const aiConnectorsCount = 1; const alertsContextCount = 20; // <-- alerts were sent as context to be analyzed - const attackDiscoveriesCount = 7; // <-- attack discoveries are present + const alertsCount = 10; // <-- alerts contributed to attack discoveries + const attackDiscoveriesCount = 3; // <-- attack discoveries were generated from the alerts const connectorId = 'test-connector-id'; const isLoading = false; const onGenerate = jest.fn(); @@ -342,12 +349,12 @@ describe('EmptyStates', () => { <EmptyStates aiConnectorsCount={aiConnectorsCount} alertsContextCount={alertsContextCount} + alertsCount={alertsCount} attackDiscoveriesCount={attackDiscoveriesCount} connectorId={connectorId} failureReason={null} isLoading={isLoading} onGenerate={onGenerate} - upToAlertsCount={DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS} /> </TestProviders> ); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_states/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_states/index.tsx index a083ec7b77fdd..49b4557c72192 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_states/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/empty_states/index.tsx @@ -9,55 +9,51 @@ import React from 'react'; import { Failure } from '../failure'; import { EmptyPrompt } from '../empty_prompt'; -import { showFailurePrompt, showNoAlertsPrompt, showWelcomePrompt } from '../helpers'; +import { showEmptyPrompt, showNoAlertsPrompt, showWelcomePrompt } from '../helpers'; import { NoAlerts } from '../no_alerts'; import { Welcome } from '../welcome'; interface Props { - aiConnectorsCount: number | null; // null when connectors are not configured - alertsContextCount: number | null; // null when unavailable for the current connector + aiConnectorsCount: number | null; + alertsContextCount: number | null; + alertsCount: number; attackDiscoveriesCount: number; connectorId: string | undefined; failureReason: string | null; isLoading: boolean; onGenerate: () => Promise<void>; - upToAlertsCount: number; } const EmptyStatesComponent: React.FC<Props> = ({ aiConnectorsCount, alertsContextCount, + alertsCount, attackDiscoveriesCount, connectorId, failureReason, isLoading, onGenerate, - upToAlertsCount, }) => { - const isDisabled = connectorId == null; - if (showWelcomePrompt({ aiConnectorsCount, isLoading })) { return <Welcome />; - } - - if (showFailurePrompt({ connectorId, failureReason, isLoading })) { + } else if (!isLoading && failureReason != null) { return <Failure failureReason={failureReason} />; + } else if (showNoAlertsPrompt({ alertsContextCount, isLoading })) { + return <NoAlerts />; + } else if (showEmptyPrompt({ aiConnectorsCount, attackDiscoveriesCount, isLoading })) { + return ( + <EmptyPrompt + alertsCount={alertsCount} + isDisabled={connectorId == null} + isLoading={isLoading} + onGenerate={onGenerate} + /> + ); } - if (showNoAlertsPrompt({ alertsContextCount, connectorId, isLoading })) { - return <NoAlerts isLoading={isLoading} isDisabled={isDisabled} onGenerate={onGenerate} />; - } - - return ( - <EmptyPrompt - aiConnectorsCount={aiConnectorsCount} - alertsCount={upToAlertsCount} - attackDiscoveriesCount={attackDiscoveriesCount} - isDisabled={isDisabled} - isLoading={isLoading} - onGenerate={onGenerate} - /> - ); + return null; }; +EmptyStatesComponent.displayName = 'EmptyStates'; + export const EmptyStates = React.memo(EmptyStatesComponent); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/failure/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/failure/index.tsx index c9c27446fe51c..4318f3f78536a 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/failure/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/failure/index.tsx @@ -5,53 +5,13 @@ * 2.0. */ -import { - EuiAccordion, - EuiCodeBlock, - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiText, -} from '@elastic/eui'; +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui'; import { css } from '@emotion/react'; -import React, { useMemo } from 'react'; +import React from 'react'; import * as i18n from './translations'; -interface Props { - failureReason: string | null | undefined; -} - -const FailureComponent: React.FC<Props> = ({ failureReason }) => { - const Failures = useMemo(() => { - const failures = failureReason != null ? failureReason.split('\n') : ''; - const [firstFailure, ...restFailures] = failures; - - return ( - <> - <p>{firstFailure}</p> - - {restFailures.length > 0 && ( - <EuiAccordion - id="failuresFccordion" - buttonContent={i18n.DETAILS} - data-test-subj="failuresAccordion" - paddingSize="s" - > - <> - {restFailures.map((failure, i) => ( - <EuiCodeBlock fontSize="m" key={i} paddingSize="m"> - {failure} - </EuiCodeBlock> - ))} - </> - </EuiAccordion> - )} - </> - ); - }, [failureReason]); - +const FailureComponent: React.FC<{ failureReason: string }> = ({ failureReason }) => { return ( <EuiFlexGroup alignItems="center" data-test-subj="failure" direction="column"> <EuiFlexItem data-test-subj="emptyPromptContainer" grow={false}> @@ -66,7 +26,7 @@ const FailureComponent: React.FC<Props> = ({ failureReason }) => { `} data-test-subj="bodyText" > - {Failures} + {failureReason} </EuiText> } title={<h2 data-test-subj="failureTitle">{i18n.FAILURE_TITLE}</h2>} diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/failure/translations.ts b/x-pack/plugins/security_solution/public/attack_discovery/pages/failure/translations.ts index ecaa7fad240e1..b36104d202ba8 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/failure/translations.ts +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/failure/translations.ts @@ -7,10 +7,10 @@ import { i18n } from '@kbn/i18n'; -export const DETAILS = i18n.translate( - 'xpack.securitySolution.attackDiscovery.pages.failure.detailsAccordionButton', +export const LEARN_MORE = i18n.translate( + 'xpack.securitySolution.attackDiscovery.pages.failure.learnMoreLink', { - defaultMessage: 'Details', + defaultMessage: 'Learn more about Attack discovery', } ); @@ -20,10 +20,3 @@ export const FAILURE_TITLE = i18n.translate( defaultMessage: 'Attack discovery generation failed', } ); - -export const LEARN_MORE = i18n.translate( - 'xpack.securitySolution.attackDiscovery.pages.failure.learnMoreLink', - { - defaultMessage: 'Learn more about Attack discovery', - } -); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/generate/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/generate/index.tsx deleted file mode 100644 index 16ed376dd3af4..0000000000000 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/generate/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButton, EuiToolTip } from '@elastic/eui'; -import React from 'react'; - -import * as i18n from '../empty_prompt/translations'; - -interface Props { - isDisabled?: boolean; - isLoading: boolean; - onGenerate: () => void; -} - -const GenerateComponent: React.FC<Props> = ({ isLoading, isDisabled = false, onGenerate }) => { - const disabled = isLoading || isDisabled; - - return ( - <EuiToolTip - content={disabled ? i18n.SELECT_A_CONNECTOR : null} - data-test-subj="generateTooltip" - > - <EuiButton color="primary" data-test-subj="generate" disabled={disabled} onClick={onGenerate}> - {i18n.GENERATE} - </EuiButton> - </EuiToolTip> - ); -}; - -GenerateComponent.displayName = 'Generate'; - -export const Generate = React.memo(GenerateComponent); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.test.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.test.tsx index 7b0688eadafef..aee53d889c7ac 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.test.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS } from '@kbn/elastic-assistant'; import { fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; @@ -32,11 +31,9 @@ describe('Header', () => { connectorsAreConfigured={true} isDisabledActions={false} isLoading={false} - localStorageAttackDiscoveryMaxAlerts={`${DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS}`} onCancel={jest.fn()} onGenerate={jest.fn()} onConnectorIdSelected={jest.fn()} - setLocalStorageAttackDiscoveryMaxAlerts={jest.fn()} /> </TestProviders> ); @@ -57,11 +54,9 @@ describe('Header', () => { connectorsAreConfigured={connectorsAreConfigured} isDisabledActions={false} isLoading={false} - localStorageAttackDiscoveryMaxAlerts={`${DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS}`} onCancel={jest.fn()} onGenerate={jest.fn()} onConnectorIdSelected={jest.fn()} - setLocalStorageAttackDiscoveryMaxAlerts={jest.fn()} /> </TestProviders> ); @@ -82,11 +77,9 @@ describe('Header', () => { connectorsAreConfigured={true} isDisabledActions={false} isLoading={false} - localStorageAttackDiscoveryMaxAlerts={`${DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS}`} onCancel={jest.fn()} onConnectorIdSelected={jest.fn()} onGenerate={onGenerate} - setLocalStorageAttackDiscoveryMaxAlerts={jest.fn()} /> </TestProviders> ); @@ -109,11 +102,9 @@ describe('Header', () => { connectorsAreConfigured={true} isDisabledActions={false} isLoading={isLoading} - localStorageAttackDiscoveryMaxAlerts={`${DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS}`} onCancel={jest.fn()} onConnectorIdSelected={jest.fn()} onGenerate={jest.fn()} - setLocalStorageAttackDiscoveryMaxAlerts={jest.fn()} /> </TestProviders> ); @@ -135,11 +126,9 @@ describe('Header', () => { connectorsAreConfigured={true} isDisabledActions={false} isLoading={isLoading} - localStorageAttackDiscoveryMaxAlerts={`${DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS}`} onCancel={onCancel} onConnectorIdSelected={jest.fn()} onGenerate={jest.fn()} - setLocalStorageAttackDiscoveryMaxAlerts={jest.fn()} /> </TestProviders> ); @@ -161,11 +150,9 @@ describe('Header', () => { connectorsAreConfigured={true} isDisabledActions={false} isLoading={false} - localStorageAttackDiscoveryMaxAlerts={`${DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS}`} onCancel={jest.fn()} onConnectorIdSelected={jest.fn()} onGenerate={jest.fn()} - setLocalStorageAttackDiscoveryMaxAlerts={jest.fn()} /> </TestProviders> ); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.tsx index ff170805670a6..583bcc25d0eb6 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/header/index.tsx @@ -9,11 +9,10 @@ import type { EuiButtonProps } from '@elastic/eui'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiToolTip, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import { ConnectorSelectorInline } from '@kbn/elastic-assistant'; -import type { AttackDiscoveryStats } from '@kbn/elastic-assistant-common'; import { noop } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { SettingsModal } from './settings_modal'; +import type { AttackDiscoveryStats } from '@kbn/elastic-assistant-common'; import { StatusBell } from './status_bell'; import * as i18n from './translations'; @@ -22,11 +21,9 @@ interface Props { connectorsAreConfigured: boolean; isLoading: boolean; isDisabledActions: boolean; - localStorageAttackDiscoveryMaxAlerts: string | undefined; onGenerate: () => void; onCancel: () => void; onConnectorIdSelected: (connectorId: string) => void; - setLocalStorageAttackDiscoveryMaxAlerts: React.Dispatch<React.SetStateAction<string | undefined>>; stats: AttackDiscoveryStats | null; } @@ -35,11 +32,9 @@ const HeaderComponent: React.FC<Props> = ({ connectorsAreConfigured, isLoading, isDisabledActions, - localStorageAttackDiscoveryMaxAlerts, onGenerate, onConnectorIdSelected, onCancel, - setLocalStorageAttackDiscoveryMaxAlerts, stats, }) => { const { euiTheme } = useEuiTheme(); @@ -73,7 +68,6 @@ const HeaderComponent: React.FC<Props> = ({ }, [isLoading, handleCancel, onGenerate] ); - return ( <EuiFlexGroup alignItems="center" @@ -84,14 +78,6 @@ const HeaderComponent: React.FC<Props> = ({ data-test-subj="header" gutterSize="none" > - <EuiFlexItem grow={false}> - <SettingsModal - connectorId={connectorId} - isLoading={isLoading} - localStorageAttackDiscoveryMaxAlerts={localStorageAttackDiscoveryMaxAlerts} - setLocalStorageAttackDiscoveryMaxAlerts={setLocalStorageAttackDiscoveryMaxAlerts} - /> - </EuiFlexItem> <StatusBell stats={stats} /> {connectorsAreConfigured && ( <EuiFlexItem grow={false}> diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/settings_modal/alerts_settings/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/header/settings_modal/alerts_settings/index.tsx deleted file mode 100644 index b51a1fc3f85c8..0000000000000 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/settings_modal/alerts_settings/index.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { SingleRangeChangeEvent } from '@kbn/elastic-assistant'; -import { EuiFlexGroup, EuiFlexItem, EuiForm, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui'; -import { - AlertsRange, - SELECT_FEWER_ALERTS, - YOUR_ANONYMIZATION_SETTINGS, -} from '@kbn/elastic-assistant'; -import React, { useCallback } from 'react'; - -import * as i18n from '../translations'; - -export const MAX_ALERTS = 500; -export const MIN_ALERTS = 50; -export const ROW_MIN_WITH = 550; // px -export const STEP = 50; - -interface Props { - maxAlerts: string; - setMaxAlerts: React.Dispatch<React.SetStateAction<string>>; -} - -const AlertsSettingsComponent: React.FC<Props> = ({ maxAlerts, setMaxAlerts }) => { - const onChangeAlertsRange = useCallback( - (e: SingleRangeChangeEvent) => { - setMaxAlerts(e.currentTarget.value); - }, - [setMaxAlerts] - ); - - return ( - <EuiForm component="form"> - <EuiFormRow hasChildLabel={false} label={i18n.ALERTS}> - <EuiFlexGroup direction="column" gutterSize="none"> - <EuiFlexItem grow={false}> - <AlertsRange - maxAlerts={MAX_ALERTS} - minAlerts={MIN_ALERTS} - onChange={onChangeAlertsRange} - step={STEP} - value={maxAlerts} - /> - <EuiSpacer size="m" /> - </EuiFlexItem> - - <EuiFlexItem grow={true}> - <EuiText color="subdued" size="xs"> - <span>{i18n.LATEST_AND_RISKIEST_OPEN_ALERTS(Number(maxAlerts))}</span> - </EuiText> - </EuiFlexItem> - - <EuiFlexItem grow={true}> - <EuiText color="subdued" size="xs"> - <span>{YOUR_ANONYMIZATION_SETTINGS}</span> - </EuiText> - </EuiFlexItem> - - <EuiFlexItem grow={true}> - <EuiText color="subdued" size="xs"> - <span>{SELECT_FEWER_ALERTS}</span> - </EuiText> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFormRow> - </EuiForm> - ); -}; - -AlertsSettingsComponent.displayName = 'AlertsSettings'; - -export const AlertsSettings = React.memo(AlertsSettingsComponent); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/settings_modal/footer/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/header/settings_modal/footer/index.tsx deleted file mode 100644 index 0066376a0e198..0000000000000 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/settings_modal/footer/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/react'; -import React from 'react'; - -import * as i18n from '../translations'; - -interface Props { - closeModal: () => void; - onReset: () => void; - onSave: () => void; -} - -const FooterComponent: React.FC<Props> = ({ closeModal, onReset, onSave }) => { - const { euiTheme } = useEuiTheme(); - - return ( - <EuiFlexGroup alignItems="center" gutterSize="none" justifyContent="spaceBetween"> - <EuiFlexItem grow={false}> - <EuiButtonEmpty data-test-sub="reset" flush="both" onClick={onReset} size="s"> - {i18n.RESET} - </EuiButtonEmpty> - </EuiFlexItem> - - <EuiFlexItem grow={false}> - <EuiFlexGroup alignItems="center" gutterSize="none"> - <EuiFlexItem - css={css` - margin-right: ${euiTheme.size.s}; - `} - grow={false} - > - <EuiButtonEmpty data-test-sub="cancel" onClick={closeModal} size="s"> - {i18n.CANCEL} - </EuiButtonEmpty> - </EuiFlexItem> - - <EuiFlexItem grow={false}> - <EuiButton data-test-sub="save" fill onClick={onSave} size="s"> - {i18n.SAVE} - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - ); -}; - -FooterComponent.displayName = 'Footer'; - -export const Footer = React.memo(FooterComponent); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/settings_modal/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/header/settings_modal/index.tsx deleted file mode 100644 index 0d342c591f32b..0000000000000 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/settings_modal/index.tsx +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiButtonIcon, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiText, - EuiToolTip, - EuiTourStep, - useGeneratedHtmlId, -} from '@elastic/eui'; -import { - ATTACK_DISCOVERY_STORAGE_KEY, - DEFAULT_ASSISTANT_NAMESPACE, - DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS, - SHOW_SETTINGS_TOUR_LOCAL_STORAGE_KEY, -} from '@kbn/elastic-assistant'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useLocalStorage } from 'react-use'; - -import { AlertsSettings } from './alerts_settings'; -import { useSpaceId } from '../../../../common/hooks/use_space_id'; -import { Footer } from './footer'; -import { getIsTourEnabled } from './is_tour_enabled'; -import * as i18n from './translations'; - -interface Props { - connectorId: string | undefined; - isLoading: boolean; - localStorageAttackDiscoveryMaxAlerts: string | undefined; - setLocalStorageAttackDiscoveryMaxAlerts: React.Dispatch<React.SetStateAction<string | undefined>>; -} - -const SettingsModalComponent: React.FC<Props> = ({ - connectorId, - isLoading, - localStorageAttackDiscoveryMaxAlerts, - setLocalStorageAttackDiscoveryMaxAlerts, -}) => { - const spaceId = useSpaceId() ?? 'default'; - const modalTitleId = useGeneratedHtmlId(); - - const [maxAlerts, setMaxAlerts] = useState( - localStorageAttackDiscoveryMaxAlerts ?? `${DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS}` - ); - - const [isModalVisible, setIsModalVisible] = useState(false); - const showModal = useCallback(() => { - setMaxAlerts(localStorageAttackDiscoveryMaxAlerts ?? `${DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS}`); - - setIsModalVisible(true); - }, [localStorageAttackDiscoveryMaxAlerts]); - const closeModal = useCallback(() => setIsModalVisible(false), []); - - const onReset = useCallback(() => setMaxAlerts(`${DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS}`), []); - - const onSave = useCallback(() => { - setLocalStorageAttackDiscoveryMaxAlerts(maxAlerts); - closeModal(); - }, [closeModal, maxAlerts, setLocalStorageAttackDiscoveryMaxAlerts]); - - const [showSettingsTour, setShowSettingsTour] = useLocalStorage<boolean>( - `${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${spaceId}.${SHOW_SETTINGS_TOUR_LOCAL_STORAGE_KEY}.v8.16`, - true - ); - const onTourFinished = useCallback(() => setShowSettingsTour(() => false), [setShowSettingsTour]); - const [tourDelayElapsed, setTourDelayElapsed] = useState(false); - - useEffect(() => { - // visible EuiTourStep anchors don't follow the button when the layout changes (i.e. when the connectors finish loading) - const timeout = setTimeout(() => setTourDelayElapsed(true), 10000); - return () => clearTimeout(timeout); - }, []); - - const onSettingsClicked = useCallback(() => { - showModal(); - setShowSettingsTour(() => false); - }, [setShowSettingsTour, showModal]); - - const SettingsButton = useMemo( - () => ( - <EuiToolTip content={i18n.SETTINGS}> - <EuiButtonIcon - aria-label={i18n.SETTINGS} - data-test-subj="settings" - iconType="gear" - onClick={onSettingsClicked} - /> - </EuiToolTip> - ), - [onSettingsClicked] - ); - - const isTourEnabled = getIsTourEnabled({ - connectorId, - isLoading, - tourDelayElapsed, - showSettingsTour, - }); - - return ( - <> - {isTourEnabled ? ( - <EuiTourStep - anchorPosition="downCenter" - content={ - <> - <EuiText size="s"> - <p> - <span>{i18n.ATTACK_DISCOVERY_SENDS_MORE_ALERTS}</span> - <br /> - <span>{i18n.CONFIGURE_YOUR_SETTINGS_HERE}</span> - </p> - </EuiText> - </> - } - isStepOpen={showSettingsTour} - minWidth={300} - onFinish={onTourFinished} - step={1} - stepsTotal={1} - subtitle={i18n.RECENT_ATTACK_DISCOVERY_IMPROVEMENTS} - title={i18n.SEND_MORE_ALERTS} - > - {SettingsButton} - </EuiTourStep> - ) : ( - <>{SettingsButton}</> - )} - - {isModalVisible && ( - <EuiModal aria-labelledby={modalTitleId} data-test-subj="modal" onClose={closeModal}> - <EuiModalHeader> - <EuiModalHeaderTitle id={modalTitleId}>{i18n.SETTINGS}</EuiModalHeaderTitle> - </EuiModalHeader> - - <EuiModalBody> - <AlertsSettings maxAlerts={maxAlerts} setMaxAlerts={setMaxAlerts} /> - </EuiModalBody> - - <EuiModalFooter> - <Footer closeModal={closeModal} onReset={onReset} onSave={onSave} /> - </EuiModalFooter> - </EuiModal> - )} - </> - ); -}; - -SettingsModalComponent.displayName = 'SettingsModal'; - -export const SettingsModal = React.memo(SettingsModalComponent); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/settings_modal/is_tour_enabled/index.ts b/x-pack/plugins/security_solution/public/attack_discovery/pages/header/settings_modal/is_tour_enabled/index.ts deleted file mode 100644 index 7f2f356114902..0000000000000 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/settings_modal/is_tour_enabled/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const getIsTourEnabled = ({ - connectorId, - isLoading, - tourDelayElapsed, - showSettingsTour, -}: { - connectorId: string | undefined; - isLoading: boolean; - tourDelayElapsed: boolean; - showSettingsTour: boolean | undefined; -}): boolean => !isLoading && connectorId != null && tourDelayElapsed && !!showSettingsTour; diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/settings_modal/translations.ts b/x-pack/plugins/security_solution/public/attack_discovery/pages/header/settings_modal/translations.ts deleted file mode 100644 index dc42db84f2d8a..0000000000000 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/header/settings_modal/translations.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const ALERTS = i18n.translate( - 'xpack.securitySolution.attackDiscovery.settingsModal.alertsLabel', - { - defaultMessage: 'Alerts', - } -); - -export const ATTACK_DISCOVERY_SENDS_MORE_ALERTS = i18n.translate( - 'xpack.securitySolution.attackDiscovery.settingsModal.attackDiscoverySendsMoreAlertsTourText', - { - defaultMessage: 'Attack discovery sends more alerts as context.', - } -); - -export const CANCEL = i18n.translate( - 'xpack.securitySolution.attackDiscovery.settingsModal.cancelButton', - { - defaultMessage: 'Cancel', - } -); - -export const CONFIGURE_YOUR_SETTINGS_HERE = i18n.translate( - 'xpack.securitySolution.attackDiscovery.settingsModal.configureYourSettingsHereTourText', - { - defaultMessage: 'Configure your settings here.', - } -); - -export const LATEST_AND_RISKIEST_OPEN_ALERTS = (alertsCount: number) => - i18n.translate( - 'xpack.securitySolution.attackDiscovery.settingsModal.latestAndRiskiestOpenAlertsLabel', - { - defaultMessage: - 'Send Attack discovery information about your {alertsCount} newest and riskiest open or acknowledged alerts.', - values: { alertsCount }, - } - ); - -export const SAVE = i18n.translate( - 'xpack.securitySolution.attackDiscovery.settingsModal.saveButton', - { - defaultMessage: 'Save', - } -); - -export const SEND_MORE_ALERTS = i18n.translate( - 'xpack.securitySolution.attackDiscovery.settingsModal.tourTitle', - { - defaultMessage: 'Send more alerts', - } -); - -export const SETTINGS = i18n.translate( - 'xpack.securitySolution.attackDiscovery.settingsModal.settingsLabel', - { - defaultMessage: 'Settings', - } -); - -export const RECENT_ATTACK_DISCOVERY_IMPROVEMENTS = i18n.translate( - 'xpack.securitySolution.attackDiscovery.settingsModal.tourSubtitle', - { - defaultMessage: 'Recent Attack discovery improvements', - } -); - -export const RESET = i18n.translate( - 'xpack.securitySolution.attackDiscovery.settingsModal.resetLabel', - { - defaultMessage: 'Reset', - } -); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/helpers.test.ts b/x-pack/plugins/security_solution/public/attack_discovery/pages/helpers.test.ts index c7e1c579418b4..e94687611ea8f 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/helpers.test.ts @@ -12,7 +12,6 @@ describe('helpers', () => { it('returns true when isLoading is false and alertsContextCount is 0', () => { const result = showNoAlertsPrompt({ alertsContextCount: 0, - connectorId: 'test', isLoading: false, }); @@ -22,7 +21,6 @@ describe('helpers', () => { it('returns false when isLoading is true', () => { const result = showNoAlertsPrompt({ alertsContextCount: 0, - connectorId: 'test', isLoading: true, }); @@ -32,7 +30,6 @@ describe('helpers', () => { it('returns false when alertsContextCount is null', () => { const result = showNoAlertsPrompt({ alertsContextCount: null, - connectorId: 'test', isLoading: false, }); @@ -42,7 +39,6 @@ describe('helpers', () => { it('returns false when alertsContextCount greater than 0', () => { const result = showNoAlertsPrompt({ alertsContextCount: 20, - connectorId: 'test', isLoading: false, }); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/helpers.ts b/x-pack/plugins/security_solution/public/attack_discovery/pages/helpers.ts index b990c3ccf1555..e3d3be963bacd 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/helpers.ts +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/helpers.ts @@ -75,14 +75,11 @@ export const getErrorToastText = ( export const showNoAlertsPrompt = ({ alertsContextCount, - connectorId, isLoading, }: { alertsContextCount: number | null; - connectorId: string | undefined; isLoading: boolean; -}): boolean => - connectorId != null && !isLoading && alertsContextCount != null && alertsContextCount === 0; +}): boolean => !isLoading && alertsContextCount != null && alertsContextCount === 0; export const showWelcomePrompt = ({ aiConnectorsCount, @@ -114,26 +111,12 @@ export const showLoading = ({ loadingConnectorId: string | null; }): boolean => isLoading && (loadingConnectorId === connectorId || attackDiscoveriesCount === 0); -export const showSummary = (attackDiscoveriesCount: number) => attackDiscoveriesCount > 0; - -export const showFailurePrompt = ({ +export const showSummary = ({ connectorId, - failureReason, - isLoading, + attackDiscoveriesCount, + loadingConnectorId, }: { connectorId: string | undefined; - failureReason: string | null; - isLoading: boolean; -}): boolean => connectorId != null && !isLoading && failureReason != null; - -export const getSize = ({ - defaultMaxAlerts, - localStorageAttackDiscoveryMaxAlerts, -}: { - defaultMaxAlerts: number; - localStorageAttackDiscoveryMaxAlerts: string | undefined; -}): number => { - const size = Number(localStorageAttackDiscoveryMaxAlerts); - - return isNaN(size) || size <= 0 ? defaultMaxAlerts : size; -}; + attackDiscoveriesCount: number; + loadingConnectorId: string | null; +}): boolean => loadingConnectorId !== connectorId && attackDiscoveriesCount > 0; diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/index.tsx index e55b2fe5083b6..ea5c16fc3cbba 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/index.tsx @@ -5,13 +5,11 @@ * 2.0. */ -import { EuiEmptyPrompt, EuiLoadingLogo, EuiSpacer } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiLoadingLogo, EuiSpacer } from '@elastic/eui'; import { css } from '@emotion/react'; import { ATTACK_DISCOVERY_STORAGE_KEY, DEFAULT_ASSISTANT_NAMESPACE, - DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS, - MAX_ALERTS_LOCAL_STORAGE_KEY, useAssistantContext, useLoadConnectors, } from '@kbn/elastic-assistant'; @@ -25,16 +23,23 @@ import { HeaderPage } from '../../common/components/header_page'; import { useSpaceId } from '../../common/hooks/use_space_id'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { Header } from './header'; -import { CONNECTOR_ID_LOCAL_STORAGE_KEY, getSize, showLoading } from './helpers'; +import { + CONNECTOR_ID_LOCAL_STORAGE_KEY, + getInitialIsOpen, + showLoading, + showSummary, +} from './helpers'; +import { AttackDiscoveryPanel } from '../attack_discovery_panel'; +import { EmptyStates } from './empty_states'; import { LoadingCallout } from './loading_callout'; import { PageTitle } from './page_title'; -import { Results } from './results'; +import { Summary } from './summary'; import { useAttackDiscovery } from '../use_attack_discovery'; const AttackDiscoveryPageComponent: React.FC = () => { const spaceId = useSpaceId() ?? 'default'; - const { http } = useAssistantContext(); + const { http, knowledgeBase } = useAssistantContext(); const { data: aiConnectors } = useLoadConnectors({ http, }); @@ -49,12 +54,6 @@ const AttackDiscoveryPageComponent: React.FC = () => { `${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${spaceId}.${CONNECTOR_ID_LOCAL_STORAGE_KEY}` ); - const [localStorageAttackDiscoveryMaxAlerts, setLocalStorageAttackDiscoveryMaxAlerts] = - useLocalStorage<string>( - `${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${spaceId}.${MAX_ALERTS_LOCAL_STORAGE_KEY}`, - `${DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS}` - ); - const [connectorId, setConnectorId] = React.useState<string | undefined>( localStorageAttackDiscoveryConnectorId ); @@ -79,10 +78,6 @@ const AttackDiscoveryPageComponent: React.FC = () => { } = useAttackDiscovery({ connectorId, setLoadingConnectorId, - size: getSize({ - defaultMaxAlerts: DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS, - localStorageAttackDiscoveryMaxAlerts, - }), }); // get last updated from the cached attack discoveries if it exists: @@ -164,11 +159,9 @@ const AttackDiscoveryPageComponent: React.FC = () => { isLoading={isLoading} // disable header actions before post request has completed isDisabledActions={isLoadingPost} - localStorageAttackDiscoveryMaxAlerts={localStorageAttackDiscoveryMaxAlerts} onConnectorIdSelected={onConnectorIdSelected} onGenerate={onGenerate} onCancel={onCancel} - setLocalStorageAttackDiscoveryMaxAlerts={setLocalStorageAttackDiscoveryMaxAlerts} stats={stats} /> <EuiSpacer size="m" /> @@ -177,37 +170,68 @@ const AttackDiscoveryPageComponent: React.FC = () => { <EuiEmptyPrompt data-test-subj="animatedLogo" icon={animatedLogo} /> ) : ( <> - {showLoading({ + {showSummary({ attackDiscoveriesCount, connectorId, - isLoading: isLoading || isLoadingPost, loadingConnectorId, - }) ? ( - <LoadingCallout - alertsContextCount={alertsContextCount} - localStorageAttackDiscoveryMaxAlerts={localStorageAttackDiscoveryMaxAlerts} - approximateFutureTime={approximateFutureTime} - connectorIntervals={connectorIntervals} - /> - ) : ( - <Results - aiConnectorsCount={aiConnectors?.length ?? null} - alertsContextCount={alertsContextCount} + }) && ( + <Summary alertsCount={alertsCount} attackDiscoveriesCount={attackDiscoveriesCount} - connectorId={connectorId} - failureReason={failureReason} - isLoading={isLoading} - isLoadingPost={isLoadingPost} - localStorageAttackDiscoveryMaxAlerts={localStorageAttackDiscoveryMaxAlerts} - onGenerate={onGenerate} + lastUpdated={selectedConnectorLastUpdated} onToggleShowAnonymized={onToggleShowAnonymized} - selectedConnectorAttackDiscoveries={selectedConnectorAttackDiscoveries} - selectedConnectorLastUpdated={selectedConnectorLastUpdated} - selectedConnectorReplacements={selectedConnectorReplacements} showAnonymized={showAnonymized} /> )} + + <> + {showLoading({ + attackDiscoveriesCount, + connectorId, + isLoading: isLoading || isLoadingPost, + loadingConnectorId, + }) ? ( + <LoadingCallout + alertsCount={knowledgeBase.latestAlerts} + approximateFutureTime={approximateFutureTime} + connectorIntervals={connectorIntervals} + /> + ) : ( + selectedConnectorAttackDiscoveries.map((attackDiscovery, i) => ( + <React.Fragment key={attackDiscovery.id}> + <AttackDiscoveryPanel + attackDiscovery={attackDiscovery} + initialIsOpen={getInitialIsOpen(i)} + showAnonymized={showAnonymized} + replacements={selectedConnectorReplacements} + /> + <EuiSpacer size="l" /> + </React.Fragment> + )) + )} + </> + <EuiFlexGroup + css={css` + max-height: 100%; + min-height: 100%; + `} + direction="column" + gutterSize="none" + > + <EuiSpacer size="xxl" /> + <EuiFlexItem grow={false}> + <EmptyStates + aiConnectorsCount={aiConnectors?.length ?? null} + alertsContextCount={alertsContextCount} + alertsCount={knowledgeBase.latestAlerts} + attackDiscoveriesCount={attackDiscoveriesCount} + failureReason={failureReason} + connectorId={connectorId} + isLoading={isLoading || isLoadingPost} + onGenerate={onGenerate} + /> + </EuiFlexItem> + </EuiFlexGroup> </> )} <SpyRoute pageName={SecurityPageName.attackDiscovery} /> diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/index.test.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/index.test.tsx index f755017288300..af6efafb3c1dd 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/index.test.tsx @@ -29,10 +29,9 @@ describe('LoadingCallout', () => { ]; const defaultProps = { - alertsContextCount: 30, + alertsCount: 30, approximateFutureTime: new Date(), connectorIntervals, - localStorageAttackDiscoveryMaxAlerts: '50', }; it('renders the animated loading icon', () => { diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/index.tsx index aee8241ec73fc..7e392e3165711 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/index.tsx @@ -20,15 +20,13 @@ const BACKGROUND_COLOR_DARK = '#0B2030'; const BORDER_COLOR_DARK = '#0B2030'; interface Props { - alertsContextCount: number | null; + alertsCount: number; approximateFutureTime: Date | null; connectorIntervals: GenerationInterval[]; - localStorageAttackDiscoveryMaxAlerts: string | undefined; } const LoadingCalloutComponent: React.FC<Props> = ({ - alertsContextCount, - localStorageAttackDiscoveryMaxAlerts, + alertsCount, approximateFutureTime, connectorIntervals, }) => { @@ -48,14 +46,11 @@ const LoadingCalloutComponent: React.FC<Props> = ({ `} grow={false} > - <LoadingMessages - alertsContextCount={alertsContextCount} - localStorageAttackDiscoveryMaxAlerts={localStorageAttackDiscoveryMaxAlerts} - /> + <LoadingMessages alertsCount={alertsCount} /> </EuiFlexItem> </EuiFlexGroup> ), - [alertsContextCount, euiTheme.size.m, localStorageAttackDiscoveryMaxAlerts] + [alertsCount, euiTheme.size.m] ); const isDarkMode = theme.getTheme().darkMode === true; diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/loading_messages/get_loading_callout_alerts_count/index.ts b/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/loading_messages/get_loading_callout_alerts_count/index.ts deleted file mode 100644 index 9a3061272ca15..0000000000000 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/loading_messages/get_loading_callout_alerts_count/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const getLoadingCalloutAlertsCount = ({ - alertsContextCount, - defaultMaxAlerts, - localStorageAttackDiscoveryMaxAlerts, -}: { - alertsContextCount: number | null; - defaultMaxAlerts: number; - localStorageAttackDiscoveryMaxAlerts: string | undefined; -}): number => { - if (alertsContextCount != null && !isNaN(alertsContextCount) && alertsContextCount > 0) { - return alertsContextCount; - } - - const size = Number(localStorageAttackDiscoveryMaxAlerts); - - return isNaN(size) || size <= 0 ? defaultMaxAlerts : size; -}; diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/loading_messages/index.test.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/loading_messages/index.test.tsx index 8b3f174792c5e..250a25055791a 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/loading_messages/index.test.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/loading_messages/index.test.tsx @@ -16,7 +16,7 @@ describe('LoadingMessages', () => { it('renders the expected loading message', () => { render( <TestProviders> - <LoadingMessages alertsContextCount={20} localStorageAttackDiscoveryMaxAlerts={'30'} /> + <LoadingMessages alertsCount={20} /> </TestProviders> ); const attackDiscoveryGenerationInProgress = screen.getByTestId( @@ -31,7 +31,7 @@ describe('LoadingMessages', () => { it('renders the loading message with the expected alerts count', () => { render( <TestProviders> - <LoadingMessages alertsContextCount={20} localStorageAttackDiscoveryMaxAlerts={'30'} /> + <LoadingMessages alertsCount={20} /> </TestProviders> ); const aiCurrentlyAnalyzing = screen.getByTestId('aisCurrentlyAnalyzing'); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/loading_messages/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/loading_messages/index.tsx index 1a84771e5c635..9acd7b4d2dbbf 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/loading_messages/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/loading_callout/loading_messages/index.tsx @@ -7,34 +7,22 @@ import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { css } from '@emotion/react'; -import { DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS } from '@kbn/elastic-assistant'; import React from 'react'; import { useKibana } from '../../../../common/lib/kibana'; -import { getLoadingCalloutAlertsCount } from './get_loading_callout_alerts_count'; import * as i18n from '../translations'; const TEXT_COLOR = '#343741'; interface Props { - alertsContextCount: number | null; - localStorageAttackDiscoveryMaxAlerts: string | undefined; + alertsCount: number; } -const LoadingMessagesComponent: React.FC<Props> = ({ - alertsContextCount, - localStorageAttackDiscoveryMaxAlerts, -}) => { +const LoadingMessagesComponent: React.FC<Props> = ({ alertsCount }) => { const { theme } = useKibana().services; const isDarkMode = theme.getTheme().darkMode === true; - const alertsCount = getLoadingCalloutAlertsCount({ - alertsContextCount, - defaultMaxAlerts: DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS, - localStorageAttackDiscoveryMaxAlerts, - }); - return ( <EuiFlexGroup data-test-subj="loadingMessages" direction="column" gutterSize="none"> <EuiFlexItem grow={false}> diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/no_alerts/index.test.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/no_alerts/index.test.tsx index 6c6bbfb25cb7f..6c2640623e370 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/no_alerts/index.test.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/no_alerts/index.test.tsx @@ -13,7 +13,7 @@ import { ATTACK_DISCOVERY_ONLY, LEARN_MORE, NO_ALERTS_TO_ANALYZE } from './trans describe('NoAlerts', () => { beforeEach(() => { - render(<NoAlerts isDisabled={false} isLoading={false} onGenerate={jest.fn()} />); + render(<NoAlerts />); }); it('renders the avatar', () => { diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/no_alerts/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/no_alerts/index.tsx index ace75f568bf3d..a7b0cd929336b 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/no_alerts/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/pages/no_alerts/index.tsx @@ -17,15 +17,8 @@ import { import React, { useMemo } from 'react'; import * as i18n from './translations'; -import { Generate } from '../generate'; -interface Props { - isDisabled: boolean; - isLoading: boolean; - onGenerate: () => void; -} - -const NoAlertsComponent: React.FC<Props> = ({ isDisabled, isLoading, onGenerate }) => { +const NoAlertsComponent: React.FC = () => { const title = useMemo( () => ( <EuiFlexGroup @@ -90,14 +83,6 @@ const NoAlertsComponent: React.FC<Props> = ({ isDisabled, isLoading, onGenerate {i18n.LEARN_MORE} </EuiLink> </EuiFlexItem> - - <EuiFlexItem grow={false}> - <EuiSpacer size="m" /> - </EuiFlexItem> - - <EuiFlexItem grow={false}> - <Generate isDisabled={isDisabled} isLoading={isLoading} onGenerate={onGenerate} /> - </EuiFlexItem> </EuiFlexGroup> ); }; diff --git a/x-pack/plugins/security_solution/public/attack_discovery/pages/results/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/pages/results/index.tsx deleted file mode 100644 index 6e3e43127e711..0000000000000 --- a/x-pack/plugins/security_solution/public/attack_discovery/pages/results/index.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiSpacer } from '@elastic/eui'; -import { DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS } from '@kbn/elastic-assistant'; -import type { AttackDiscovery, Replacements } from '@kbn/elastic-assistant-common'; -import React from 'react'; - -import { AttackDiscoveryPanel } from '../../attack_discovery_panel'; -import { EmptyStates } from '../empty_states'; -import { showEmptyStates } from '../empty_states/helpers/show_empty_states'; -import { getInitialIsOpen, showSummary } from '../helpers'; -import { Summary } from '../summary'; - -interface Props { - aiConnectorsCount: number | null; // null when connectors are not configured - alertsContextCount: number | null; // null when unavailable for the current connector - alertsCount: number; - attackDiscoveriesCount: number; - connectorId: string | undefined; - failureReason: string | null; - isLoading: boolean; - isLoadingPost: boolean; - localStorageAttackDiscoveryMaxAlerts: string | undefined; - onGenerate: () => Promise<void>; - onToggleShowAnonymized: () => void; - selectedConnectorAttackDiscoveries: AttackDiscovery[]; - selectedConnectorLastUpdated: Date | null; - selectedConnectorReplacements: Replacements; - showAnonymized: boolean; -} - -const ResultsComponent: React.FC<Props> = ({ - aiConnectorsCount, - alertsContextCount, - alertsCount, - attackDiscoveriesCount, - connectorId, - failureReason, - isLoading, - isLoadingPost, - localStorageAttackDiscoveryMaxAlerts, - onGenerate, - onToggleShowAnonymized, - selectedConnectorAttackDiscoveries, - selectedConnectorLastUpdated, - selectedConnectorReplacements, - showAnonymized, -}) => { - if ( - showEmptyStates({ - aiConnectorsCount, - alertsContextCount, - attackDiscoveriesCount, - connectorId, - failureReason, - isLoading, - }) - ) { - return ( - <> - <EuiSpacer size="xxl" /> - <EmptyStates - aiConnectorsCount={aiConnectorsCount} - alertsContextCount={alertsContextCount} - attackDiscoveriesCount={attackDiscoveriesCount} - failureReason={failureReason} - connectorId={connectorId} - isLoading={isLoading || isLoadingPost} - onGenerate={onGenerate} - upToAlertsCount={Number( - localStorageAttackDiscoveryMaxAlerts ?? DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS - )} - /> - </> - ); - } - - return ( - <> - {showSummary(attackDiscoveriesCount) && ( - <Summary - alertsCount={alertsCount} - attackDiscoveriesCount={attackDiscoveriesCount} - lastUpdated={selectedConnectorLastUpdated} - onToggleShowAnonymized={onToggleShowAnonymized} - showAnonymized={showAnonymized} - /> - )} - - {selectedConnectorAttackDiscoveries.map((attackDiscovery, i) => ( - <React.Fragment key={attackDiscovery.id}> - <AttackDiscoveryPanel - attackDiscovery={attackDiscovery} - initialIsOpen={getInitialIsOpen(i)} - showAnonymized={showAnonymized} - replacements={selectedConnectorReplacements} - /> - <EuiSpacer size="l" /> - </React.Fragment> - ))} - </> - ); -}; - -ResultsComponent.displayName = 'Results'; - -export const Results = React.memo(ResultsComponent); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/helpers.test.ts b/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/helpers.test.ts index cc0034c90d1fa..f2fd17d5978b7 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/helpers.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS } from '@kbn/elastic-assistant'; import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/public/common'; import type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public'; import { omit } from 'lodash/fp'; @@ -133,7 +132,9 @@ describe('getRequestBody', () => { }, ], }; - + const knowledgeBase = { + latestAlerts: 20, + }; const traceOptions = { apmUrl: '/app/apm', langSmithProject: '', @@ -144,7 +145,7 @@ describe('getRequestBody', () => { const result = getRequestBody({ alertsIndexPattern, anonymizationFields, - size: DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS, + knowledgeBase, traceOptions, }); @@ -159,8 +160,8 @@ describe('getRequestBody', () => { }, langSmithProject: undefined, langSmithApiKey: undefined, + size: knowledgeBase.latestAlerts, replacements: {}, - size: DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS, subAction: 'invokeAI', }); }); @@ -169,7 +170,7 @@ describe('getRequestBody', () => { const result = getRequestBody({ alertsIndexPattern: undefined, anonymizationFields, - size: DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS, + knowledgeBase, traceOptions, }); @@ -184,8 +185,8 @@ describe('getRequestBody', () => { }, langSmithProject: undefined, langSmithApiKey: undefined, + size: knowledgeBase.latestAlerts, replacements: {}, - size: DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS, subAction: 'invokeAI', }); }); @@ -194,7 +195,7 @@ describe('getRequestBody', () => { const withLangSmith = { alertsIndexPattern, anonymizationFields, - size: DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS, + knowledgeBase, traceOptions: { apmUrl: '/app/apm', langSmithProject: 'A project', @@ -215,7 +216,7 @@ describe('getRequestBody', () => { }, langSmithApiKey: withLangSmith.traceOptions.langSmithApiKey, langSmithProject: withLangSmith.traceOptions.langSmithProject, - size: DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS, + size: knowledgeBase.latestAlerts, replacements: {}, subAction: 'invokeAI', }); @@ -225,8 +226,8 @@ describe('getRequestBody', () => { const result = getRequestBody({ alertsIndexPattern, anonymizationFields, + knowledgeBase, selectedConnector: connector, // <-- selectedConnector is provided - size: DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS, traceOptions, }); @@ -241,7 +242,7 @@ describe('getRequestBody', () => { }, langSmithProject: undefined, langSmithApiKey: undefined, - size: DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS, + size: knowledgeBase.latestAlerts, replacements: {}, subAction: 'invokeAI', }); @@ -257,8 +258,8 @@ describe('getRequestBody', () => { alertsIndexPattern, anonymizationFields, genAiConfig, // <-- genAiConfig is provided + knowledgeBase, selectedConnector: connector, // <-- selectedConnector is provided - size: DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS, traceOptions, }); @@ -273,8 +274,8 @@ describe('getRequestBody', () => { }, langSmithProject: undefined, langSmithApiKey: undefined, + size: knowledgeBase.latestAlerts, replacements: {}, - size: DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS, subAction: 'invokeAI', }); }); diff --git a/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/helpers.ts b/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/helpers.ts index 7aa9bfdd118d9..97eb132bdaaeb 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/helpers.ts +++ b/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/helpers.ts @@ -5,7 +5,10 @@ * 2.0. */ -import type { TraceOptions } from '@kbn/elastic-assistant/impl/assistant/types'; +import type { + KnowledgeBaseConfig, + TraceOptions, +} from '@kbn/elastic-assistant/impl/assistant/types'; import type { AttackDiscoveryPostRequestBody } from '@kbn/elastic-assistant-common'; import type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public'; import type { ActionConnectorProps } from '@kbn/triggers-actions-ui-plugin/public/types'; @@ -57,8 +60,8 @@ export const getRequestBody = ({ alertsIndexPattern, anonymizationFields, genAiConfig, + knowledgeBase, selectedConnector, - size, traceOptions, }: { alertsIndexPattern: string | undefined; @@ -80,7 +83,7 @@ export const getRequestBody = ({ }>; }; genAiConfig?: GenAiConfig; - size: number; + knowledgeBase: KnowledgeBaseConfig; selectedConnector?: ActionConnector; traceOptions: TraceOptions; }): AttackDiscoveryPostRequestBody => ({ @@ -92,8 +95,8 @@ export const getRequestBody = ({ langSmithApiKey: isEmpty(traceOptions?.langSmithApiKey) ? undefined : traceOptions?.langSmithApiKey, + size: knowledgeBase.latestAlerts, replacements: {}, // no need to re-use replacements in the current implementation - size, subAction: 'invokeAI', // non-streaming apiConfig: { connectorId: selectedConnector?.id ?? '', diff --git a/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/index.test.tsx b/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/index.test.tsx index 59659ee6d8649..6329ce5ca699a 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/index.test.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/index.test.tsx @@ -106,8 +106,6 @@ const mockAttackDiscoveries = [ const setLoadingConnectorId = jest.fn(); const setStatus = jest.fn(); -const SIZE = 20; - describe('useAttackDiscovery', () => { const mockPollApi = { cancelAttackDiscovery: jest.fn(), @@ -128,11 +126,7 @@ describe('useAttackDiscovery', () => { it('initializes with correct default values', () => { const { result } = renderHook(() => - useAttackDiscovery({ - connectorId: 'test-id', - setLoadingConnectorId, - size: 20, - }) + useAttackDiscovery({ connectorId: 'test-id', setLoadingConnectorId }) ); expect(result.current.alertsContextCount).toBeNull(); @@ -150,15 +144,14 @@ describe('useAttackDiscovery', () => { it('fetches attack discoveries and updates state correctly', async () => { (mockedUseKibana.services.http.fetch as jest.Mock).mockResolvedValue(mockAttackDiscoveryPost); - const { result } = renderHook(() => useAttackDiscovery({ connectorId: 'test-id', size: SIZE })); - + const { result } = renderHook(() => useAttackDiscovery({ connectorId: 'test-id' })); await act(async () => { await result.current.fetchAttackDiscoveries(); }); expect(mockedUseKibana.services.http.fetch).toHaveBeenCalledWith( '/internal/elastic_assistant/attack_discovery', { - body: `{"alertsIndexPattern":"alerts-index-pattern","anonymizationFields":[],"replacements":{},"size":${SIZE},"subAction":"invokeAI","apiConfig":{"connectorId":"test-id","actionTypeId":".gen-ai"}}`, + body: '{"alertsIndexPattern":"alerts-index-pattern","anonymizationFields":[],"size":20,"replacements":{},"subAction":"invokeAI","apiConfig":{"connectorId":"test-id","actionTypeId":".gen-ai"}}', method: 'POST', version: '1', } @@ -174,7 +167,7 @@ describe('useAttackDiscovery', () => { const error = new Error(errorMessage); (mockedUseKibana.services.http.fetch as jest.Mock).mockRejectedValue(error); - const { result } = renderHook(() => useAttackDiscovery({ connectorId: 'test-id', size: SIZE })); + const { result } = renderHook(() => useAttackDiscovery({ connectorId: 'test-id' })); await act(async () => { await result.current.fetchAttackDiscoveries(); @@ -191,11 +184,7 @@ describe('useAttackDiscovery', () => { it('sets loading state based on poll status', async () => { (usePollApi as jest.Mock).mockReturnValue({ ...mockPollApi, status: 'running' }); const { result } = renderHook(() => - useAttackDiscovery({ - connectorId: 'test-id', - setLoadingConnectorId, - size: SIZE, - }) + useAttackDiscovery({ connectorId: 'test-id', setLoadingConnectorId }) ); expect(result.current.isLoading).toBe(true); @@ -213,7 +202,7 @@ describe('useAttackDiscovery', () => { }, status: 'succeeded', }); - const { result } = renderHook(() => useAttackDiscovery({ connectorId: 'test-id', size: SIZE })); + const { result } = renderHook(() => useAttackDiscovery({ connectorId: 'test-id' })); expect(result.current.alertsContextCount).toEqual(20); // this is set from usePollApi @@ -238,7 +227,7 @@ describe('useAttackDiscovery', () => { }, status: 'failed', }); - const { result } = renderHook(() => useAttackDiscovery({ connectorId: 'test-id', size: SIZE })); + const { result } = renderHook(() => useAttackDiscovery({ connectorId: 'test-id' })); expect(result.current.failureReason).toEqual('something bad'); expect(result.current.isLoading).toBe(false); @@ -252,13 +241,7 @@ describe('useAttackDiscovery', () => { data: [], // <-- zero connectors configured }); - renderHook(() => - useAttackDiscovery({ - connectorId: 'test-id', - setLoadingConnectorId, - size: SIZE, - }) - ); + renderHook(() => useAttackDiscovery({ connectorId: 'test-id', setLoadingConnectorId })); }); afterEach(() => { diff --git a/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/index.tsx b/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/index.tsx index 4ad78981d4540..deb1c556bdb43 100644 --- a/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/index.tsx +++ b/x-pack/plugins/security_solution/public/attack_discovery/use_attack_discovery/index.tsx @@ -43,11 +43,9 @@ export interface UseAttackDiscovery { export const useAttackDiscovery = ({ connectorId, - size, setLoadingConnectorId, }: { connectorId: string | undefined; - size: number; setLoadingConnectorId?: (loadingConnectorId: string | null) => void; }): UseAttackDiscovery => { // get Kibana services and connectors @@ -77,7 +75,7 @@ export const useAttackDiscovery = ({ const [isLoading, setIsLoading] = useState(false); // get alerts index pattern and allow lists from the assistant context: - const { alertsIndexPattern, traceOptions } = useAssistantContext(); + const { alertsIndexPattern, knowledgeBase, traceOptions } = useAssistantContext(); const { data: anonymizationFields } = useFetchAnonymizationFields(); @@ -97,11 +95,18 @@ export const useAttackDiscovery = ({ alertsIndexPattern, anonymizationFields, genAiConfig, - size, + knowledgeBase, selectedConnector, traceOptions, }); - }, [aiConnectors, alertsIndexPattern, anonymizationFields, connectorId, size, traceOptions]); + }, [ + aiConnectors, + alertsIndexPattern, + anonymizationFields, + connectorId, + knowledgeBase, + traceOptions, + ]); useEffect(() => { if ( @@ -135,7 +140,7 @@ export const useAttackDiscovery = ({ useEffect(() => { if (pollData !== null && pollData.connectorId === connectorId) { if (pollData.alertsContextCount != null) setAlertsContextCount(pollData.alertsContextCount); - if (pollData.attackDiscoveries.length && pollData.attackDiscoveries[0].timestamp != null) { + if (pollData.attackDiscoveries.length) { // get last updated from timestamp, not from updatedAt since this can indicate the last time the status was updated setLastUpdated(new Date(pollData.attackDiscoveries[0].timestamp)); } diff --git a/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/attack_discovery_tool.test.ts b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/attack_discovery_tool.test.ts new file mode 100644 index 0000000000000..4d06751f57d7d --- /dev/null +++ b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/attack_discovery_tool.test.ts @@ -0,0 +1,340 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import type { KibanaRequest } from '@kbn/core-http-server'; +import type { AttackDiscoveryPostRequestBody } from '@kbn/elastic-assistant-common'; +import type { ActionsClientLlm } from '@kbn/langchain/server'; +import type { DynamicTool } from '@langchain/core/tools'; + +import { loggerMock } from '@kbn/logging-mocks'; + +import { ATTACK_DISCOVERY_TOOL } from './attack_discovery_tool'; +import { mockAnonymizationFields } from '../mock/mock_anonymization_fields'; +import { mockEmptyOpenAndAcknowledgedAlertsQueryResults } from '../mock/mock_empty_open_and_acknowledged_alerts_qery_results'; +import { mockOpenAndAcknowledgedAlertsQueryResults } from '../mock/mock_open_and_acknowledged_alerts_query_results'; + +jest.mock('langchain/chains', () => { + const mockLLMChain = jest.fn().mockImplementation(() => ({ + call: jest.fn().mockResolvedValue({ + records: [ + { + alertIds: [ + 'b6e883c29b32571aaa667fa13e65bbb4f95172a2b84bdfb85d6f16c72b2d2560', + '0215a6c5cc9499dd0290cd69a4947efb87d3ddd8b6385a766d122c2475be7367', + '600eb9eca925f4c5b544b4e9d3cf95d83b7829f8f74c5bd746369cb4c2968b9a', + 'e1f4a4ed70190eb4bd256c813029a6a9101575887cdbfa226ac330fbd3063f0c', + '2a7a4809ca625dfe22ccd35fbef7a7ba8ed07f109e5cbd17250755cfb0bc615f', + ], + detailsMarkdown: + '- Malicious Go application named "My Go Application.app" is being executed from temporary directories, likely indicating malware delivery\n- The malicious application is spawning child processes like `osascript` to display fake system dialogs and attempt to phish user credentials ({{ host.name 6c57a4f7-b30b-465d-a670-47377655b1bb }}, {{ user.name 639fab6d-369b-4879-beae-7767a7145c7f }})\n- The malicious application is also executing `chmod` to make the file `unix1` executable ({{ file.path /Users/james/unix1 }})\n- `unix1` is a potentially malicious executable that is being run with suspicious arguments related to the macOS keychain ({{ process.command_line /Users/james/unix1 /Users/james/library/Keychains/login.keychain-db TempTemp1234!! }})\n- Multiple detections indicate the presence of malware on the host attempting credential access and execution of malicious payloads', + entitySummaryMarkdown: + 'Malicious activity detected on {{ host.name 6c57a4f7-b30b-465d-a670-47377655b1bb }} involving user {{ user.name 639fab6d-369b-4879-beae-7767a7145c7f }}.', + mitreAttackTactics: ['Credential Access', 'Execution'], + summaryMarkdown: + 'Multiple detections indicate the presence of malware on a macOS host {{ host.name 6c57a4f7-b30b-465d-a670-47377655b1bb }} attempting credential theft and execution of malicious payloads targeting the user {{ user.name 639fab6d-369b-4879-beae-7767a7145c7f }}.', + title: 'Malware Delivering Malicious Payloads on macOS', + }, + ], + }), + })); + + return { + LLMChain: mockLLMChain, + }; +}); + +describe('AttackDiscoveryTool', () => { + const alertsIndexPattern = '.alerts-security.alerts-default'; + const replacements = { uuid: 'original_value' }; + const size = 20; + const request = { + body: { + actionTypeId: '.bedrock', + alertsIndexPattern, + anonymizationFields: mockAnonymizationFields, + connectorId: 'test-connector-id', + replacements, + size, + subAction: 'invokeAI', + }, + } as unknown as KibanaRequest<unknown, unknown, AttackDiscoveryPostRequestBody>; + + const esClient = { + search: jest.fn(), + } as unknown as ElasticsearchClient; + const llm = jest.fn() as unknown as ActionsClientLlm; + const logger = loggerMock.create(); + + const rest = { + anonymizationFields: mockAnonymizationFields, + isEnabledKnowledgeBase: false, + llm, + logger, + onNewReplacements: jest.fn(), + size, + }; + + beforeEach(() => { + jest.clearAllMocks(); + + (esClient.search as jest.Mock).mockResolvedValue(mockOpenAndAcknowledgedAlertsQueryResults); + }); + + describe('isSupported', () => { + it('returns false when the request is missing required anonymization parameters', () => { + const requestMissingAnonymizationParams = { + body: { + isEnabledKnowledgeBase: false, + alertsIndexPattern: '.alerts-security.alerts-default', + size: 20, + }, + } as unknown as KibanaRequest<unknown, unknown, AttackDiscoveryPostRequestBody>; + + const params = { + alertsIndexPattern, + esClient, + request: requestMissingAnonymizationParams, // <-- request is missing required anonymization parameters + ...rest, + }; + + expect(ATTACK_DISCOVERY_TOOL.isSupported(params)).toBe(false); + }); + + it('returns false when the alertsIndexPattern is undefined', () => { + const params = { + esClient, + request, + ...rest, + alertsIndexPattern: undefined, // <-- alertsIndexPattern is undefined + }; + + expect(ATTACK_DISCOVERY_TOOL.isSupported(params)).toBe(false); + }); + + it('returns false when size is undefined', () => { + const params = { + alertsIndexPattern, + esClient, + request, + ...rest, + size: undefined, // <-- size is undefined + }; + + expect(ATTACK_DISCOVERY_TOOL.isSupported(params)).toBe(false); + }); + + it('returns false when size is out of range', () => { + const params = { + alertsIndexPattern, + esClient, + request, + ...rest, + size: 0, // <-- size is out of range + }; + + expect(ATTACK_DISCOVERY_TOOL.isSupported(params)).toBe(false); + }); + + it('returns false when llm is undefined', () => { + const params = { + alertsIndexPattern, + esClient, + request, + ...rest, + llm: undefined, // <-- llm is undefined + }; + + expect(ATTACK_DISCOVERY_TOOL.isSupported(params)).toBe(false); + }); + + it('returns true if all required params are provided', () => { + const params = { + alertsIndexPattern, + esClient, + request, + ...rest, + }; + + expect(ATTACK_DISCOVERY_TOOL.isSupported(params)).toBe(true); + }); + }); + + describe('getTool', () => { + it('returns null when llm is undefined', () => { + const tool = ATTACK_DISCOVERY_TOOL.getTool({ + alertsIndexPattern, + esClient, + replacements, + request, + ...rest, + llm: undefined, // <-- llm is undefined + }); + + expect(tool).toBeNull(); + }); + + it('returns a `DynamicTool` with a `func` that calls `esClient.search()` with the expected alerts query', async () => { + const tool: DynamicTool = ATTACK_DISCOVERY_TOOL.getTool({ + alertsIndexPattern, + esClient, + replacements, + request, + ...rest, + }) as DynamicTool; + + await tool.func(''); + + expect(esClient.search).toHaveBeenCalledWith({ + allow_no_indices: true, + body: { + _source: false, + fields: mockAnonymizationFields.map(({ field }) => ({ + field, + include_unmapped: true, + })), + query: { + bool: { + filter: [ + { + bool: { + filter: [ + { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + 'kibana.alert.workflow_status': 'open', + }, + }, + { + match_phrase: { + 'kibana.alert.workflow_status': 'acknowledged', + }, + }, + ], + }, + }, + { + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: 'now-24h', + lte: 'now', + }, + }, + }, + ], + must: [], + must_not: [ + { + exists: { + field: 'kibana.alert.building_block_type', + }, + }, + ], + should: [], + }, + }, + ], + }, + }, + runtime_mappings: {}, + size, + sort: [ + { + 'kibana.alert.risk_score': { + order: 'desc', + }, + }, + { + '@timestamp': { + order: 'desc', + }, + }, + ], + }, + ignore_unavailable: true, + index: [alertsIndexPattern], + }); + }); + + it('returns a `DynamicTool` with a `func` returns an empty attack discoveries array when getAnonymizedAlerts returns no alerts', async () => { + (esClient.search as jest.Mock).mockResolvedValue( + mockEmptyOpenAndAcknowledgedAlertsQueryResults // <-- no alerts + ); + + const tool: DynamicTool = ATTACK_DISCOVERY_TOOL.getTool({ + alertsIndexPattern, + esClient, + replacements, + request, + ...rest, + }) as DynamicTool; + + const result = await tool.func(''); + const expected = JSON.stringify({ alertsContextCount: 0, attackDiscoveries: [] }, null, 2); // <-- empty attack discoveries array + + expect(result).toEqual(expected); + }); + + it('returns a `DynamicTool` with a `func` that returns the expected results', async () => { + const tool: DynamicTool = ATTACK_DISCOVERY_TOOL.getTool({ + alertsIndexPattern, + esClient, + replacements, + request, + ...rest, + }) as DynamicTool; + + await tool.func(''); + + const result = await tool.func(''); + const expected = JSON.stringify( + { + alertsContextCount: 20, + attackDiscoveries: [ + { + alertIds: [ + 'b6e883c29b32571aaa667fa13e65bbb4f95172a2b84bdfb85d6f16c72b2d2560', + '0215a6c5cc9499dd0290cd69a4947efb87d3ddd8b6385a766d122c2475be7367', + '600eb9eca925f4c5b544b4e9d3cf95d83b7829f8f74c5bd746369cb4c2968b9a', + 'e1f4a4ed70190eb4bd256c813029a6a9101575887cdbfa226ac330fbd3063f0c', + '2a7a4809ca625dfe22ccd35fbef7a7ba8ed07f109e5cbd17250755cfb0bc615f', + ], + detailsMarkdown: + '- Malicious Go application named "My Go Application.app" is being executed from temporary directories, likely indicating malware delivery\n- The malicious application is spawning child processes like `osascript` to display fake system dialogs and attempt to phish user credentials ({{ host.name 6c57a4f7-b30b-465d-a670-47377655b1bb }}, {{ user.name 639fab6d-369b-4879-beae-7767a7145c7f }})\n- The malicious application is also executing `chmod` to make the file `unix1` executable ({{ file.path /Users/james/unix1 }})\n- `unix1` is a potentially malicious executable that is being run with suspicious arguments related to the macOS keychain ({{ process.command_line /Users/james/unix1 /Users/james/library/Keychains/login.keychain-db TempTemp1234!! }})\n- Multiple detections indicate the presence of malware on the host attempting credential access and execution of malicious payloads', + entitySummaryMarkdown: + 'Malicious activity detected on {{ host.name 6c57a4f7-b30b-465d-a670-47377655b1bb }} involving user {{ user.name 639fab6d-369b-4879-beae-7767a7145c7f }}.', + mitreAttackTactics: ['Credential Access', 'Execution'], + summaryMarkdown: + 'Multiple detections indicate the presence of malware on a macOS host {{ host.name 6c57a4f7-b30b-465d-a670-47377655b1bb }} attempting credential theft and execution of malicious payloads targeting the user {{ user.name 639fab6d-369b-4879-beae-7767a7145c7f }}.', + title: 'Malware Delivering Malicious Payloads on macOS', + }, + ], + }, + null, + 2 + ); + + expect(result).toEqual(expected); + }); + + it('returns a tool instance with the expected tags', () => { + const tool = ATTACK_DISCOVERY_TOOL.getTool({ + alertsIndexPattern, + esClient, + replacements, + request, + ...rest, + }) as DynamicTool; + + expect(tool.tags).toEqual(['attack-discovery']); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/attack_discovery_tool.ts b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/attack_discovery_tool.ts new file mode 100644 index 0000000000000..264862d76b8f5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/attack_discovery_tool.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PromptTemplate } from '@langchain/core/prompts'; +import { requestHasRequiredAnonymizationParams } from '@kbn/elastic-assistant-plugin/server/lib/langchain/helpers'; +import type { AssistantTool, AssistantToolParams } from '@kbn/elastic-assistant-plugin/server'; +import { LLMChain } from 'langchain/chains'; +import { OutputFixingParser } from 'langchain/output_parsers'; +import { DynamicTool } from '@langchain/core/tools'; + +import { APP_UI_ID } from '../../../../common'; +import { getAnonymizedAlerts } from './get_anonymized_alerts'; +import { getOutputParser } from './get_output_parser'; +import { sizeIsOutOfRange } from '../open_and_acknowledged_alerts/helpers'; +import { getAttackDiscoveryPrompt } from './get_attack_discovery_prompt'; + +export interface AttackDiscoveryToolParams extends AssistantToolParams { + alertsIndexPattern: string; + size: number; +} + +export const ATTACK_DISCOVERY_TOOL_DESCRIPTION = + 'Call this for attack discoveries containing `markdown` that should be displayed verbatim (with no additional processing).'; + +/** + * Returns a tool for generating attack discoveries from open and acknowledged + * alerts, or null if the request doesn't have all the required parameters. + */ +export const ATTACK_DISCOVERY_TOOL: AssistantTool = { + id: 'attack-discovery', + name: 'AttackDiscoveryTool', + description: ATTACK_DISCOVERY_TOOL_DESCRIPTION, + sourceRegister: APP_UI_ID, + isSupported: (params: AssistantToolParams): params is AttackDiscoveryToolParams => { + const { alertsIndexPattern, llm, request, size } = params; + + return ( + requestHasRequiredAnonymizationParams(request) && + alertsIndexPattern != null && + size != null && + !sizeIsOutOfRange(size) && + llm != null + ); + }, + getTool(params: AssistantToolParams) { + if (!this.isSupported(params)) return null; + + const { + alertsIndexPattern, + anonymizationFields, + esClient, + langChainTimeout, + llm, + onNewReplacements, + replacements, + size, + } = params as AttackDiscoveryToolParams; + + return new DynamicTool({ + name: 'AttackDiscoveryTool', + description: ATTACK_DISCOVERY_TOOL_DESCRIPTION, + func: async () => { + if (llm == null) { + throw new Error('LLM is required for attack discoveries'); + } + + const anonymizedAlerts = await getAnonymizedAlerts({ + alertsIndexPattern, + anonymizationFields, + esClient, + onNewReplacements, + replacements, + size, + }); + + const alertsContextCount = anonymizedAlerts.length; + if (alertsContextCount === 0) { + // No alerts to analyze, so return an empty attack discoveries array + return JSON.stringify({ alertsContextCount, attackDiscoveries: [] }, null, 2); + } + + const outputParser = getOutputParser(); + const outputFixingParser = OutputFixingParser.fromLLM(llm, outputParser); + + const prompt = new PromptTemplate({ + template: `Answer the user's question as best you can:\n{format_instructions}\n{query}`, + inputVariables: ['query'], + partialVariables: { + format_instructions: outputFixingParser.getFormatInstructions(), + }, + }); + + const answerFormattingChain = new LLMChain({ + llm, + prompt, + outputKey: 'records', + outputParser: outputFixingParser, + }); + + const result = await answerFormattingChain.call({ + query: getAttackDiscoveryPrompt({ anonymizedAlerts }), + timeout: langChainTimeout, + }); + const attackDiscoveries = result.records; + + return JSON.stringify({ alertsContextCount, attackDiscoveries }, null, 2); + }, + tags: ['attack-discovery'], + }); + }, +}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/retriever/helpers/get_anonymized_alerts/index.test.ts b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_anonymized_alerts.test.ts similarity index 90% rename from x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/retriever/helpers/get_anonymized_alerts/index.test.ts rename to x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_anonymized_alerts.test.ts index b616c392ddd21..6b7526870eb9f 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/retriever/helpers/get_anonymized_alerts/index.test.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_anonymized_alerts.test.ts @@ -6,19 +6,19 @@ */ import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; -import { getOpenAndAcknowledgedAlertsQuery } from '@kbn/elastic-assistant-common'; -const MIN_SIZE = 10; +import { getAnonymizedAlerts } from './get_anonymized_alerts'; +import { mockOpenAndAcknowledgedAlertsQueryResults } from '../mock/mock_open_and_acknowledged_alerts_query_results'; +import { getOpenAndAcknowledgedAlertsQuery } from '../open_and_acknowledged_alerts/get_open_and_acknowledged_alerts_query'; +import { MIN_SIZE } from '../open_and_acknowledged_alerts/helpers'; -import { getAnonymizedAlerts } from '.'; -import { mockOpenAndAcknowledgedAlertsQueryResults } from '../../../../mock/mock_open_and_acknowledged_alerts_query_results'; - -jest.mock('@kbn/elastic-assistant-common', () => { - const original = jest.requireActual('@kbn/elastic-assistant-common'); +jest.mock('../open_and_acknowledged_alerts/get_open_and_acknowledged_alerts_query', () => { + const original = jest.requireActual( + '../open_and_acknowledged_alerts/get_open_and_acknowledged_alerts_query' + ); return { - ...original, - getOpenAndAcknowledgedAlertsQuery: jest.fn(), + getOpenAndAcknowledgedAlertsQuery: jest.fn(() => original), }; }); diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/retriever/helpers/get_anonymized_alerts/index.ts b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_anonymized_alerts.ts similarity index 77% rename from x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/retriever/helpers/get_anonymized_alerts/index.ts rename to x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_anonymized_alerts.ts index bc2a7f5bf9e71..5989caf439518 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/retriever/helpers/get_anonymized_alerts/index.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_anonymized_alerts.ts @@ -7,16 +7,12 @@ import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import type { ElasticsearchClient } from '@kbn/core/server'; -import { - Replacements, - getAnonymizedValue, - getOpenAndAcknowledgedAlertsQuery, - getRawDataOrDefault, - sizeIsOutOfRange, - transformRawData, -} from '@kbn/elastic-assistant-common'; +import type { Replacements } from '@kbn/elastic-assistant-common'; +import { getAnonymizedValue, transformRawData } from '@kbn/elastic-assistant-common'; +import type { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen'; -import { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen'; +import { getOpenAndAcknowledgedAlertsQuery } from '../open_and_acknowledged_alerts/get_open_and_acknowledged_alerts_query'; +import { getRawDataOrDefault, sizeIsOutOfRange } from '../open_and_acknowledged_alerts/helpers'; export const getAnonymizedAlerts = async ({ alertsIndexPattern, diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_alerts_context_prompt/index.test.ts b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_attack_discovery_prompt.test.ts similarity index 70% rename from x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_alerts_context_prompt/index.test.ts rename to x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_attack_discovery_prompt.test.ts index 287f5e6b2130a..bc290bf172382 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/nodes/generate/helpers/get_alerts_context_prompt/index.test.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_attack_discovery_prompt.test.ts @@ -4,17 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { getAttackDiscoveryPrompt } from './get_attack_discovery_prompt'; -import { getAlertsContextPrompt } from '.'; -import { getDefaultAttackDiscoveryPrompt } from '../../../helpers/get_default_attack_discovery_prompt'; - -describe('getAlertsContextPrompt', () => { - it('generates the correct prompt', () => { +describe('getAttackDiscoveryPrompt', () => { + it('should generate the correct attack discovery prompt', () => { const anonymizedAlerts = ['Alert 1', 'Alert 2', 'Alert 3']; - const expected = `You are a cyber security analyst tasked with analyzing security events from Elastic Security to identify and report on potential cyber attacks or progressions. Your report should focus on high-risk incidents that could severely impact the organization, rather than isolated alerts. Present your findings in a way that can be easily understood by anyone, regardless of their technical expertise, as if you were briefing the CISO. Break down your response into sections based on timing, hosts, and users involved. When correlating alerts, use kibana.alert.original_time when it's available, otherwise use @timestamp. Include appropriate context about the affected hosts and users. Describe how the attack progression might have occurred and, if feasible, attribute it to known threat groups. Prioritize high and critical alerts, but include lower-severity alerts if desired. In the description field, provide as much detail as possible, in a bulleted list explaining any attack progressions. Accuracy is of utmost importance. You MUST escape all JSON special characters (i.e. backslashes, double quotes, newlines, tabs, carriage returns, backspaces, and form feeds). + const expected = `You are a cyber security analyst tasked with analyzing security events from Elastic Security to identify and report on potential cyber attacks or progressions. Your report should focus on high-risk incidents that could severely impact the organization, rather than isolated alerts. Present your findings in a way that can be easily understood by anyone, regardless of their technical expertise, as if you were briefing the CISO. Break down your response into sections based on timing, hosts, and users involved. When correlating alerts, use kibana.alert.original_time when it's available, otherwise use @timestamp. Include appropriate context about the affected hosts and users. Describe how the attack progression might have occurred and, if feasible, attribute it to known threat groups. Prioritize high and critical alerts, but include lower-severity alerts if desired. In the description field, provide as much detail as possible, in a bulleted list explaining any attack progressions. Accuracy is of utmost importance. Escape backslashes to respect JSON validation. New lines must always be escaped with double backslashes, i.e. \\\\n to ensure valid JSON. Only return JSON output, as described above. Do not add any additional text to describe your output. -Use context from the following alerts to provide insights: +Use context from the following open and acknowledged alerts to provide insights: """ Alert 1 @@ -25,10 +23,7 @@ Alert 3 """ `; - const prompt = getAlertsContextPrompt({ - anonymizedAlerts, - attackDiscoveryPrompt: getDefaultAttackDiscoveryPrompt(), - }); + const prompt = getAttackDiscoveryPrompt({ anonymizedAlerts }); expect(prompt).toEqual(expected); }); diff --git a/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_attack_discovery_prompt.ts b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_attack_discovery_prompt.ts new file mode 100644 index 0000000000000..df211f0bd0a7d --- /dev/null +++ b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_attack_discovery_prompt.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// NOTE: we ask the LLM to `provide insights`. We do NOT use the feature name, `AttackDiscovery`, in the prompt. +export const getAttackDiscoveryPrompt = ({ + anonymizedAlerts, +}: { + anonymizedAlerts: string[]; +}) => `You are a cyber security analyst tasked with analyzing security events from Elastic Security to identify and report on potential cyber attacks or progressions. Your report should focus on high-risk incidents that could severely impact the organization, rather than isolated alerts. Present your findings in a way that can be easily understood by anyone, regardless of their technical expertise, as if you were briefing the CISO. Break down your response into sections based on timing, hosts, and users involved. When correlating alerts, use kibana.alert.original_time when it's available, otherwise use @timestamp. Include appropriate context about the affected hosts and users. Describe how the attack progression might have occurred and, if feasible, attribute it to known threat groups. Prioritize high and critical alerts, but include lower-severity alerts if desired. In the description field, provide as much detail as possible, in a bulleted list explaining any attack progressions. Accuracy is of utmost importance. Escape backslashes to respect JSON validation. New lines must always be escaped with double backslashes, i.e. \\\\n to ensure valid JSON. Only return JSON output, as described above. Do not add any additional text to describe your output. + +Use context from the following open and acknowledged alerts to provide insights: + +""" +${anonymizedAlerts.join('\n\n')} +""" +`; diff --git a/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_output_parser.test.ts b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_output_parser.test.ts new file mode 100644 index 0000000000000..5ad2cd11f817a --- /dev/null +++ b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_output_parser.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { getOutputParser } from './get_output_parser'; + +describe('getOutputParser', () => { + it('returns a structured output parser with the expected format instructions', () => { + const outputParser = getOutputParser(); + + const expected = `You must format your output as a JSON value that adheres to a given \"JSON Schema\" instance. + +\"JSON Schema\" is a declarative language that allows you to annotate and validate JSON documents. + +For example, the example \"JSON Schema\" instance {{\"properties\": {{\"foo\": {{\"description\": \"a list of test words\", \"type\": \"array\", \"items\": {{\"type\": \"string\"}}}}}}, \"required\": [\"foo\"]}}}} +would match an object with one required property, \"foo\". The \"type\" property specifies \"foo\" must be an \"array\", and the \"description\" property semantically describes it as \"a list of test words\". The items within \"foo\" must be strings. +Thus, the object {{\"foo\": [\"bar\", \"baz\"]}} is a well-formatted instance of this example \"JSON Schema\". The object {{\"properties\": {{\"foo\": [\"bar\", \"baz\"]}}}} is not well-formatted. + +Your output will be parsed and type-checked according to the provided schema instance, so make sure all fields in your output match the schema exactly and there are no trailing commas! + +Here is the JSON Schema instance your output must adhere to. Include the enclosing markdown codeblock: +\`\`\`json +{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"alertIds\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"The alert IDs that the insight is based on.\"},\"detailsMarkdown\":{\"type\":\"string\",\"description\":\"A detailed insight with markdown, where each markdown bullet contains a description of what happened that reads like a story of the attack as it played out and always uses special {{ field.name fieldValue1 fieldValue2 fieldValueN }} syntax for field names and values from the source data. Examples of CORRECT syntax (includes field names and values): {{ host.name hostNameValue }} {{ user.name userNameValue }} {{ source.ip sourceIpValue }} Examples of INCORRECT syntax (bad, because the field names are not included): {{ hostNameValue }} {{ userNameValue }} {{ sourceIpValue }}\"},\"entitySummaryMarkdown\":{\"type\":\"string\",\"description\":\"A short (no more than a sentence) summary of the insight featuring only the host.name and user.name fields (when they are applicable), using the same {{ field.name fieldValue1 fieldValue2 fieldValueN }} syntax\"},\"mitreAttackTactics\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},\"description\":\"An array of MITRE ATT&CK tactic for the insight, using one of the following values: Reconnaissance,Initial Access,Execution,Persistence,Privilege Escalation,Discovery,Lateral Movement,Command and Control,Exfiltration\"},\"summaryMarkdown\":{\"type\":\"string\",\"description\":\"A markdown summary of insight, using the same {{ field.name fieldValue1 fieldValue2 fieldValueN }} syntax\"},\"title\":{\"type\":\"string\",\"description\":\"A short, no more than 7 words, title for the insight, NOT formatted with special syntax or markdown. This must be as brief as possible.\"}},\"required\":[\"alertIds\",\"detailsMarkdown\",\"summaryMarkdown\",\"title\"],\"additionalProperties\":false},\"description\":\"Insights with markdown that always uses special {{ field.name fieldValue1 fieldValue2 fieldValueN }} syntax for field names and values from the source data. Examples of CORRECT syntax (includes field names and values): {{ host.name hostNameValue }} {{ user.name userNameValue }} {{ source.ip sourceIpValue }} Examples of INCORRECT syntax (bad, because the field names are not included): {{ hostNameValue }} {{ userNameValue }} {{ sourceIpValue }}\",\"$schema\":\"http://json-schema.org/draft-07/schema#\"} +\`\`\` +`; + + expect(outputParser.getFormatInstructions()).toEqual(expected); + }); +}); diff --git a/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_output_parser.ts b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_output_parser.ts new file mode 100644 index 0000000000000..3d66257f060e4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/assistant/tools/attack_discovery/get_output_parser.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { StructuredOutputParser } from 'langchain/output_parsers'; +import { z } from '@kbn/zod'; + +export const SYNTAX = '{{ field.name fieldValue1 fieldValue2 fieldValueN }}'; +const GOOD_SYNTAX_EXAMPLES = + 'Examples of CORRECT syntax (includes field names and values): {{ host.name hostNameValue }} {{ user.name userNameValue }} {{ source.ip sourceIpValue }}'; + +const BAD_SYNTAX_EXAMPLES = + 'Examples of INCORRECT syntax (bad, because the field names are not included): {{ hostNameValue }} {{ userNameValue }} {{ sourceIpValue }}'; + +const RECONNAISSANCE = 'Reconnaissance'; +const INITIAL_ACCESS = 'Initial Access'; +const EXECUTION = 'Execution'; +const PERSISTENCE = 'Persistence'; +const PRIVILEGE_ESCALATION = 'Privilege Escalation'; +const DISCOVERY = 'Discovery'; +const LATERAL_MOVEMENT = 'Lateral Movement'; +const COMMAND_AND_CONTROL = 'Command and Control'; +const EXFILTRATION = 'Exfiltration'; + +const MITRE_ATTACK_TACTICS = [ + RECONNAISSANCE, + INITIAL_ACCESS, + EXECUTION, + PERSISTENCE, + PRIVILEGE_ESCALATION, + DISCOVERY, + LATERAL_MOVEMENT, + COMMAND_AND_CONTROL, + EXFILTRATION, +] as const; + +// NOTE: we ask the LLM for `insight`s. We do NOT use the feature name, `AttackDiscovery`, in the prompt. +export const getOutputParser = () => + StructuredOutputParser.fromZodSchema( + z + .array( + z.object({ + alertIds: z.string().array().describe(`The alert IDs that the insight is based on.`), + detailsMarkdown: z + .string() + .describe( + `A detailed insight with markdown, where each markdown bullet contains a description of what happened that reads like a story of the attack as it played out and always uses special ${SYNTAX} syntax for field names and values from the source data. ${GOOD_SYNTAX_EXAMPLES} ${BAD_SYNTAX_EXAMPLES}` + ), + entitySummaryMarkdown: z + .string() + .optional() + .describe( + `A short (no more than a sentence) summary of the insight featuring only the host.name and user.name fields (when they are applicable), using the same ${SYNTAX} syntax` + ), + mitreAttackTactics: z + .string() + .array() + .optional() + .describe( + `An array of MITRE ATT&CK tactic for the insight, using one of the following values: ${MITRE_ATTACK_TACTICS.join( + ',' + )}` + ), + summaryMarkdown: z + .string() + .describe(`A markdown summary of insight, using the same ${SYNTAX} syntax`), + title: z + .string() + .describe( + 'A short, no more than 7 words, title for the insight, NOT formatted with special syntax or markdown. This must be as brief as possible.' + ), + }) + ) + .describe( + `Insights with markdown that always uses special ${SYNTAX} syntax for field names and values from the source data. ${GOOD_SYNTAX_EXAMPLES} ${BAD_SYNTAX_EXAMPLES}` + ) + ); diff --git a/x-pack/plugins/security_solution/server/assistant/tools/index.ts b/x-pack/plugins/security_solution/server/assistant/tools/index.ts index 1b6e90eb7280f..a704aaa44d0a1 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/index.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/index.ts @@ -10,6 +10,7 @@ import type { AssistantTool } from '@kbn/elastic-assistant-plugin/server'; import { NL_TO_ESQL_TOOL } from './esql/nl_to_esql_tool'; import { ALERT_COUNTS_TOOL } from './alert_counts/alert_counts_tool'; import { OPEN_AND_ACKNOWLEDGED_ALERTS_TOOL } from './open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool'; +import { ATTACK_DISCOVERY_TOOL } from './attack_discovery/attack_discovery_tool'; import { KNOWLEDGE_BASE_RETRIEVAL_TOOL } from './knowledge_base/knowledge_base_retrieval_tool'; import { KNOWLEDGE_BASE_WRITE_TOOL } from './knowledge_base/knowledge_base_write_tool'; import { SECURITY_LABS_KNOWLEDGE_BASE_TOOL } from './security_labs/security_labs_tool'; @@ -21,6 +22,7 @@ export const getAssistantTools = ({ }): AssistantTool[] => { const tools = [ ALERT_COUNTS_TOOL, + ATTACK_DISCOVERY_TOOL, NL_TO_ESQL_TOOL, KNOWLEDGE_BASE_RETRIEVAL_TOOL, KNOWLEDGE_BASE_WRITE_TOOL, diff --git a/x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/mock/mock_anonymization_fields.ts b/x-pack/plugins/security_solution/server/assistant/tools/mock/mock_anonymization_fields.ts similarity index 100% rename from x-pack/plugins/elastic_assistant/server/lib/attack_discovery/graphs/default_attack_discovery_graph/mock/mock_anonymization_fields.ts rename to x-pack/plugins/security_solution/server/assistant/tools/mock/mock_anonymization_fields.ts diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/get_open_and_acknowledged_alerts_query/index.test.ts b/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/get_open_and_acknowledged_alerts_query.test.ts similarity index 96% rename from x-pack/packages/kbn-elastic-assistant-common/impl/alerts/get_open_and_acknowledged_alerts_query/index.test.ts rename to x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/get_open_and_acknowledged_alerts_query.test.ts index 975896f381443..c8b52779d7b42 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/get_open_and_acknowledged_alerts_query/index.test.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/get_open_and_acknowledged_alerts_query.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getOpenAndAcknowledgedAlertsQuery } from '.'; +import { getOpenAndAcknowledgedAlertsQuery } from './get_open_and_acknowledged_alerts_query'; describe('getOpenAndAcknowledgedAlertsQuery', () => { it('returns the expected query', () => { diff --git a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/get_open_and_acknowledged_alerts_query/index.ts b/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/get_open_and_acknowledged_alerts_query.ts similarity index 87% rename from x-pack/packages/kbn-elastic-assistant-common/impl/alerts/get_open_and_acknowledged_alerts_query/index.ts rename to x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/get_open_and_acknowledged_alerts_query.ts index 6f6e196053ca6..4090e71baa371 100644 --- a/x-pack/packages/kbn-elastic-assistant-common/impl/alerts/get_open_and_acknowledged_alerts_query/index.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/get_open_and_acknowledged_alerts_query.ts @@ -5,13 +5,8 @@ * 2.0. */ -import type { AnonymizationFieldResponse } from '../../schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen'; +import type { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen'; -/** - * This query returns open and acknowledged (non-building block) alerts in the last 24 hours. - * - * The alerts are ordered by risk score, and then from the most recent to the oldest. - */ export const getOpenAndAcknowledgedAlertsQuery = ({ alertsIndexPattern, anonymizationFields, diff --git a/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/helpers.test.ts b/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/helpers.test.ts new file mode 100644 index 0000000000000..722936a368b36 --- /dev/null +++ b/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/helpers.test.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + getRawDataOrDefault, + isRawDataValid, + MAX_SIZE, + MIN_SIZE, + sizeIsOutOfRange, +} from './helpers'; + +describe('helpers', () => { + describe('isRawDataValid', () => { + it('returns true for valid raw data', () => { + const rawData = { + field1: [1, 2, 3], // the Fields API may return a number array + field2: ['a', 'b', 'c'], // the Fields API may return a string array + }; + + expect(isRawDataValid(rawData)).toBe(true); + }); + + it('returns true when a field array is empty', () => { + const rawData = { + field1: [1, 2, 3], // the Fields API may return a number array + field2: ['a', 'b', 'c'], // the Fields API may return a string array + field3: [], // the Fields API may return an empty array + }; + + expect(isRawDataValid(rawData)).toBe(true); + }); + + it('returns false when a field does not have an array of values', () => { + const rawData = { + field1: [1, 2, 3], + field2: 'invalid', + }; + + expect(isRawDataValid(rawData)).toBe(false); + }); + + it('returns true for empty raw data', () => { + const rawData = {}; + + expect(isRawDataValid(rawData)).toBe(true); + }); + + it('returns false when raw data is an unexpected type', () => { + const rawData = 1234; + + // @ts-expect-error + expect(isRawDataValid(rawData)).toBe(false); + }); + }); + + describe('getRawDataOrDefault', () => { + it('returns the raw data when it is valid', () => { + const rawData = { + field1: [1, 2, 3], + field2: ['a', 'b', 'c'], + }; + + expect(getRawDataOrDefault(rawData)).toEqual(rawData); + }); + + it('returns an empty object when the raw data is invalid', () => { + const rawData = { + field1: [1, 2, 3], + field2: 'invalid', + }; + + expect(getRawDataOrDefault(rawData)).toEqual({}); + }); + }); + + describe('sizeIsOutOfRange', () => { + it('returns true when size is undefined', () => { + const size = undefined; + + expect(sizeIsOutOfRange(size)).toBe(true); + }); + + it('returns true when size is less than MIN_SIZE', () => { + const size = MIN_SIZE - 1; + + expect(sizeIsOutOfRange(size)).toBe(true); + }); + + it('returns true when size is greater than MAX_SIZE', () => { + const size = MAX_SIZE + 1; + + expect(sizeIsOutOfRange(size)).toBe(true); + }); + + it('returns false when size is exactly MIN_SIZE', () => { + const size = MIN_SIZE; + + expect(sizeIsOutOfRange(size)).toBe(false); + }); + + it('returns false when size is exactly MAX_SIZE', () => { + const size = MAX_SIZE; + + expect(sizeIsOutOfRange(size)).toBe(false); + }); + + it('returns false when size is within the valid range', () => { + const size = MIN_SIZE + 1; + + expect(sizeIsOutOfRange(size)).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/helpers.ts b/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/helpers.ts new file mode 100644 index 0000000000000..dcb30e04e9dbc --- /dev/null +++ b/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/helpers.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; + +export const MIN_SIZE = 10; +export const MAX_SIZE = 10000; + +export type MaybeRawData = SearchResponse['fields'] | undefined; // note: this is the type of the "fields" property in the ES response + +export const isRawDataValid = (rawData: MaybeRawData): rawData is Record<string, unknown[]> => + typeof rawData === 'object' && Object.keys(rawData).every((x) => Array.isArray(rawData[x])); + +export const getRawDataOrDefault = (rawData: MaybeRawData): Record<string, unknown[]> => + isRawDataValid(rawData) ? rawData : {}; + +export const sizeIsOutOfRange = (size?: number): boolean => + size == null || size < MIN_SIZE || size > MAX_SIZE; diff --git a/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.test.ts b/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.test.ts index 45587b65f5f4c..09bae1639f1b1 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.test.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.test.ts @@ -10,13 +10,12 @@ import type { KibanaRequest } from '@kbn/core-http-server'; import type { DynamicTool } from '@langchain/core/tools'; import { OPEN_AND_ACKNOWLEDGED_ALERTS_TOOL } from './open_and_acknowledged_alerts_tool'; +import { MAX_SIZE } from './helpers'; import type { RetrievalQAChain } from 'langchain/chains'; import { mockAlertsFieldsApi } from '@kbn/elastic-assistant-plugin/server/__mocks__/alerts'; import type { ExecuteConnectorRequestBody } from '@kbn/elastic-assistant-common/impl/schemas/actions_connector/post_actions_connector_execute_route.gen'; import { loggerMock } from '@kbn/logging-mocks'; -const MAX_SIZE = 10000; - describe('OpenAndAcknowledgedAlertsTool', () => { const alertsIndexPattern = 'alerts-index'; const esClient = { diff --git a/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.ts b/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.ts index cab015183f4a2..d6b0ad58d8adb 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/open_and_acknowledged_alerts/open_and_acknowledged_alerts_tool.ts @@ -7,17 +7,13 @@ import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types'; import type { Replacements } from '@kbn/elastic-assistant-common'; -import { - getAnonymizedValue, - getOpenAndAcknowledgedAlertsQuery, - getRawDataOrDefault, - sizeIsOutOfRange, - transformRawData, -} from '@kbn/elastic-assistant-common'; +import { getAnonymizedValue, transformRawData } from '@kbn/elastic-assistant-common'; import { DynamicStructuredTool } from '@langchain/core/tools'; import { requestHasRequiredAnonymizationParams } from '@kbn/elastic-assistant-plugin/server/lib/langchain/helpers'; import { z } from '@kbn/zod'; import type { AssistantTool, AssistantToolParams } from '@kbn/elastic-assistant-plugin/server'; +import { getOpenAndAcknowledgedAlertsQuery } from './get_open_and_acknowledged_alerts_query'; +import { getRawDataOrDefault, sizeIsOutOfRange } from './helpers'; import { APP_UI_ID } from '../../../../common'; export interface OpenAndAcknowledgedAlertsToolParams extends AssistantToolParams { diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index ce79bd061548f..0d369f3c620c4 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -205,6 +205,7 @@ "@kbn/search-types", "@kbn/field-utils", "@kbn/core-saved-objects-api-server-mocks", + "@kbn/langchain", "@kbn/core-analytics-browser", "@kbn/core-i18n-browser", "@kbn/core-theme-browser",