diff --git a/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts b/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts index 647d215f4512d..86b88f51a66c4 100644 --- a/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts +++ b/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts @@ -61,12 +61,8 @@ export const ELASTIC_MODEL_DEFINITIONS = { export const MODEL_STATE = { ...DEPLOYMENT_STATE, - DOWNLOADING: i18n.translate('xpack.ml.trainedModels.modelsList.downloadingStateLabel', { - defaultMessage: 'downloading', - }), - DOWNLOADED: i18n.translate('xpack.ml.trainedModels.modelsList.downloadedStateLabel', { - defaultMessage: 'downloaded', - }), + DOWNLOADING: 'downloading', + DOWNLOADED: 'downloaded', } as const; -export type ModelState = typeof MODEL_STATE[keyof typeof MODEL_STATE]; +export type ModelState = typeof MODEL_STATE[keyof typeof MODEL_STATE] | null; diff --git a/x-pack/plugins/fleet/public/components/with_guided_onboarding_tour.tsx b/x-pack/plugins/fleet/public/components/with_guided_onboarding_tour.tsx index 4f3af10a9d7b1..0af3c451e1da1 100644 --- a/x-pack/plugins/fleet/public/components/with_guided_onboarding_tour.tsx +++ b/x-pack/plugins/fleet/public/components/with_guided_onboarding_tour.tsx @@ -19,8 +19,7 @@ const getTourConfig = (packageKey: string, tourType: TourType) => { defaultMessage: 'Add Elastic Defend', }), description: i18n.translate('xpack.fleet.guidedOnboardingTour.endpointButton.description', { - defaultMessage: - 'In just a few steps, add your data with our recommended defaults. You can change this later.', + defaultMessage: `In this workflow, we'll be using Elastic Defend only to collect data for SIEM. Installing this will not conflict with existing endpoint security products.`, }), }; } diff --git a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts index 4fd489b5bbebd..63691b878eaa7 100644 --- a/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts +++ b/x-pack/plugins/fleet/server/services/elastic_agent_manifest.ts @@ -77,6 +77,15 @@ spec: # - BPF # (since Linux 5.8) allows loading of BPF programs, create most map types, load BTF, iterate programs and maps. # - PERFMON # (since Linux 5.8) allows attaching of BPF programs used for performance metrics and observability operations. # - SYS_RESOURCE # Allow use of special resources or raising of resource limits. Used by 'Defend for Containers' to modify 'rlimit_memlock' + ######################################################################################## + # The following capabilities are needed for Universal Profiling. + # More fine graded capabilities are only available for newer Linux kernels. + # If you are using the Universal Profiling integration, please uncomment these lines before applying. + #procMount: "Unmasked" + #privileged: true + #capabilities: + # add: + # - SYS_ADMIN resources: limits: memory: 700Mi @@ -113,6 +122,9 @@ spec: mountPath: /sys/kernel/debug - name: elastic-agent-state mountPath: /usr/share/elastic-agent/state + # If you are using the Universal Profiling integration, please uncomment these lines before applying. + #- name: universal-profiling-cache + # mountPath: /var/cache/Elastic volumes: - name: datastreams configMap: @@ -142,8 +154,8 @@ spec: - name: var-lib hostPath: path: /var/lib - # Needed for 'Defend for containers' integration (cloud-defend) - # If you are not using this integration, then these volumes and the corresponding + # Needed for 'Defend for containers' integration (cloud-defend) and Universal Profiling + # If you are not using one of these integrations, then these volumes and the corresponding # mounts can be removed. - name: sys-kernel-debug hostPath: @@ -154,6 +166,12 @@ spec: hostPath: path: /var/lib/elastic-agent/kube-system/state type: DirectoryOrCreate + # Mount required for Universal Profiling. + # If you are using the Universal Profiling integration, please uncomment these lines before applying. + #- name: universal-profiling-cache + # hostPath: + # path: /var/cache/Elastic + # type: DirectoryOrCreate --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding @@ -380,6 +398,15 @@ spec: # - BPF # (since Linux 5.8) allows loading of BPF programs, create most map types, load BTF, iterate programs and maps. # - PERFMON # (since Linux 5.8) allows attaching of BPF programs used for performance metrics and observability operations. # - SYS_RESOURCE # Allow use of special resources or raising of resource limits. Used by 'Defend for Containers' to modify 'rlimit_memlock' + ######################################################################################## + # The following capabilities are needed for Universal Profiling. + # More fine graded capabilities are only available for newer Linux kernels. + # If you are using the Universal Profiling integration, please uncomment these lines before applying. + #procMount: "Unmasked" + #privileged: true + #capabilities: + # add: + # - SYS_ADMIN resources: limits: memory: 700Mi @@ -412,6 +439,9 @@ spec: mountPath: /sys/kernel/debug - name: elastic-agent-state mountPath: /usr/share/elastic-agent/state + # If you are using the Universal Profiling integration, please uncomment these lines before applying. + #- name: universal-profiling-cache + # mountPath: /var/cache/Elastic volumes: - name: proc hostPath: @@ -440,8 +470,8 @@ spec: hostPath: path: /etc/machine-id type: File - # Needed for 'Defend for containers' integration (cloud-defend) - # If you are not using this integration, then these volumes and the corresponding + # Needed for 'Defend for containers' integration (cloud-defend) and Universal Profiling + # If you are not using one of these integrations, then these volumes and the corresponding # mounts can be removed. - name: sys-kernel-debug hostPath: @@ -452,6 +482,12 @@ spec: hostPath: path: /var/lib/elastic-agent-managed/kube-system/state type: DirectoryOrCreate + # Mount required for Universal Profiling. + # If you are using the Universal Profiling integration, please uncomment these lines before applying. + #- name: universal-profiling-cache + # hostPath: + # path: /var/cache/Elastic + # type: DirectoryOrCreate --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx index 310c10aaa1581..9af52e3c724c7 100644 --- a/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx +++ b/x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useReducer } from 'react'; +import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import styled from 'styled-components'; import { HttpStart } from '@kbn/core/public'; @@ -34,6 +34,7 @@ import { } from '@kbn/securitysolution-list-utils'; import { DataViewBase } from '@kbn/es-query'; import type { AutocompleteStart } from '@kbn/unified-search-plugin/public'; +import deepEqual from 'fast-deep-equal'; import { AndOrBadge } from '../and_or_badge'; @@ -41,7 +42,6 @@ import { BuilderExceptionListItemComponent } from './exception_item_renderer'; import { BuilderLogicButtons } from './logic_buttons'; import { getTotalErrorExist } from './selectors'; import { EntryFieldError, State, exceptionsBuilderReducer } from './reducer'; - const MyInvisibleAndBadge = styled(EuiFlexItem)` visibility: hidden; `; @@ -131,6 +131,7 @@ export const ExceptionBuilderComponent = ({ disableNested: isNestedDisabled, disableOr: isOrDisabled, }); + const [areAllEntriesDeleted, setAreAllEntriesDeleted] = useState(false); const { addNested, @@ -252,6 +253,7 @@ export const ExceptionBuilderComponent = ({ // just add a default entry to it if (updatedExceptions.length === 0) { setDefaultExceptions(item); + setAreAllEntriesDeleted(true); } else if (updatedExceptions.length > 0 && exceptionListItemSchema.is(item)) { setUpdateExceptionsToDelete([...exceptionsToDelete, item]); } else { @@ -394,12 +396,36 @@ export const ExceptionBuilderComponent = ({ } }, [exceptions, handleAddNewExceptionItem]); + /** + * This component relies on the "exceptionListItems" to pre-fill its entries, + * but any subsequent updates to the entries are not reflected back to + * the "exceptionListItems". To ensure correct behavior, we need to only + * fill the entries from the "exceptionListItems" during initialization. + * + * In the initialization phase, if there are "exceptionListItems" with + * pre-filled entries, the exceptions array will be empty. However, + * there are cases where the "exceptionListItems" may not be sent + * correctly during initialization, leading to the exceptions + * array being filled with empty entries. Therefore, we need to + * check if the exception is correctly populated with a valid + * "field" when the "exceptionListItems" has entries. that's why + * "exceptionsEntriesPopulated" is used + * + * It's important to differentiate this case from when the user + * deletes all the entries and the "exceptionListItems" has pre-filled values. + * that's why "allEntriesDeleted" is used + * + * deepEqual(exceptionListItems, exceptions) to handle the exceptionListItems in + * the EventFiltersFlyout + */ useEffect(() => { - if (exceptionListItems.length > 0) { + if (!exceptionListItems.length || deepEqual(exceptionListItems, exceptions)) return; + const exceptionsEntriesPopulated = exceptions.some((exception) => + exception.entries.some((entry) => entry.field) + ); + if (!exceptionsEntriesPopulated && !areAllEntriesDeleted) setUpdateExceptions(exceptionListItems); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [areAllEntriesDeleted, exceptionListItems, exceptions, setUpdateExceptions]); return ( diff --git a/x-pack/plugins/ml/common/types/trained_models.ts b/x-pack/plugins/ml/common/types/trained_models.ts index 76ebda90c4dd4..0b636b70071dc 100644 --- a/x-pack/plugins/ml/common/types/trained_models.ts +++ b/x-pack/plugins/ml/common/types/trained_models.ts @@ -202,6 +202,7 @@ export interface AllocatedModel { throughput_last_minute: number; number_of_allocations: number; threads_per_allocation: number; + error_count?: number; }; } diff --git a/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/allocated_models.tsx b/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/allocated_models.tsx index 6b86287b6faa1..5cdc922824ae7 100644 --- a/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/allocated_models.tsx +++ b/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/allocated_models.tsx @@ -220,6 +220,16 @@ export const AllocatedModels: FC = ({ return v.node.number_of_pending_requests; }, }, + { + name: i18n.translate('xpack.ml.trainedModels.nodesList.modelsList.errorCountHeader', { + defaultMessage: 'Errors', + }), + width: '60px', + 'data-test-subj': 'mlAllocatedModelsTableErrorCount', + render: (v: AllocatedModel) => { + return v.node.error_count ?? 0; + }, + }, ].filter((v) => !hideColumns.includes(v.id!)); return ( diff --git a/x-pack/plugins/ml/public/application/model_management/expanded_row.tsx b/x-pack/plugins/ml/public/application/model_management/expanded_row.tsx index 4571628dfd9d9..3b262cc0b8222 100644 --- a/x-pack/plugins/ml/public/application/model_management/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/model_management/expanded_row.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, useMemo, useCallback } from 'react'; +import React, { type FC, useCallback, useMemo } from 'react'; import { omit, pick } from 'lodash'; import { EuiBadge, @@ -27,7 +27,6 @@ import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { isDefined } from '@kbn/ml-is-defined'; import { TRAINED_MODEL_TYPE } from '@kbn/ml-trained-models-utils'; -import type { PartialBy } from '../../../common/types/common'; import type { ModelItemFull } from './models_list'; import { ModelPipelines } from './pipelines'; import { AllocatedModels } from '../memory_usage/nodes_overview/allocated_models'; @@ -132,30 +131,34 @@ export const ExpandedRow: FC = ({ item }) => { description, } = item; - const inferenceStats = useMemo(() => { - if (!isPopulatedObject(stats.inference_stats)) return; + const inferenceStats = useMemo(() => { + if (!isPopulatedObject(stats.inference_stats) || item.model_type === TRAINED_MODEL_TYPE.PYTORCH) + return; - const result = { ...stats.inference_stats } as PartialBy< - Exclude, - 'cache_miss_count' - >; - if (item.model_type === TRAINED_MODEL_TYPE.PYTORCH) { - delete result.cache_miss_count; - } - return result; + return stats.inference_stats; }, [stats.inference_stats, item.model_type]); const { analytics_config: analyticsConfig, ...restMetaData } = metadata ?? {}; - const details = { + const details = useMemo(() => { + return { + description, + tags, + version, + estimated_operations, + estimated_heap_memory_usage_bytes, + default_field_map, + license_level, + }; + }, [ + default_field_map, description, - tags, - version, - estimated_operations, estimated_heap_memory_usage_bytes, - default_field_map, + estimated_operations, license_level, - }; + tags, + version, + ]); const deploymentStatItems: AllocatedModel[] = useMemo(() => { const deploymentStats = stats.deployment_stats; @@ -181,6 +184,7 @@ export const ExpandedRow: FC = ({ item }) => { 'throughput_last_minute', 'number_of_allocations', 'threads_per_allocation', + 'error_count', ]), name: nodeName, } as AllocatedModel['node'], @@ -191,46 +195,28 @@ export const ExpandedRow: FC = ({ item }) => { return items; }, [stats]); - const tabs: EuiTabbedContentTab[] = [ - { - id: 'details', - 'data-test-subj': 'mlTrainedModelDetails', - name: ( - - ), - content: ( -
- - - - - -
- -
-
- - -
-
- {isPopulatedObject(restMetaData) ? ( + const tabs = useMemo(() => { + return [ + { + id: 'details', + 'data-test-subj': 'mlTrainedModelDetails', + name: ( + + ), + content: ( +
+ +
@@ -238,58 +224,56 @@ export const ExpandedRow: FC = ({ item }) => {
- ) : null} -
-
- ), - }, - ...(inferenceConfig - ? [ - { - id: 'config', - 'data-test-subj': 'mlTrainedModelInferenceConfig', - name: ( - - ), - content: ( -
- - - - - -
- -
-
- - -
-
- {analyticsConfig && ( + {isPopulatedObject(restMetaData) ? ( + + + +
+ +
+
+ + +
+
+ ) : null} +
+
+ ), + }, + ...(inferenceConfig + ? [ + { + id: 'config', + 'data-test-subj': 'mlTrainedModelInferenceConfig', + name: ( + + ), + content: ( +
+ +
@@ -297,133 +281,167 @@ export const ExpandedRow: FC = ({ item }) => {
- )} -
-
- ), - }, - ] - : []), - ...(isPopulatedObject(omit(stats, ['pipeline_count', 'ingest'])) - ? [ - { - id: 'stats', - 'data-test-subj': 'mlTrainedModelStats', - name: ( - - ), - content: ( -
- - - {!!deploymentStatItems?.length ? ( - <> - - -
- + + +
+ +
+
+ + -
-
- - -
- - - ) : null} + + + )} + +
+ ), + }, + ] + : []), + ...(isPopulatedObject(omit(stats, ['pipeline_count', 'ingest'])) + ? [ + { + id: 'stats', + 'data-test-subj': 'mlTrainedModelStats', + name: ( + + ), + content: ( +
+ - - {inferenceStats ? ( - + {!!deploymentStatItems?.length ? ( + <>
- +
-
+ + ) : null} - {isPopulatedObject(stats.model_size_stats) && - !isPopulatedObject(inferenceStats) ? ( - - - -
- -
-
- - -
-
+ + + {inferenceStats ? ( + + + +
+ +
+
+ + +
+
+ ) : null} + {isPopulatedObject(stats.model_size_stats) && + !isPopulatedObject(inferenceStats) ? ( + + + +
+ +
+
+ + +
+
+ ) : null} +
+
+ ), + }, + ] + : []), + ...((isPopulatedObject(pipelines) && Object.keys(pipelines).length > 0) || stats.ingest + ? [ + { + id: 'pipelines', + 'data-test-subj': 'mlTrainedModelPipelines', + name: ( + <> + + {isPopulatedObject(pipelines) ? ( + {Object.keys(pipelines).length} ) : null} -
-
- ), - }, - ] - : []), - ...((isPopulatedObject(pipelines) && Object.keys(pipelines).length > 0) || stats.ingest - ? [ - { - id: 'pipelines', - 'data-test-subj': 'mlTrainedModelPipelines', - name: ( - <> - - {isPopulatedObject(pipelines) ? ( - {Object.keys(pipelines).length} - ) : null} - - ), - content: ( -
- - -
- ), - }, - ] - : []), - ]; + + ), + content: ( +
+ + +
+ ), + }, + ] + : []), + ]; + }, [ + analyticsConfig, + deploymentStatItems, + details, + formatToListItems, + inferenceConfig, + inferenceStats, + pipelines, + restMetaData, + stats, + ]); + + const initialSelectedTab = + item.state === 'started' ? tabs.find((t) => t.id === 'stats') : tabs[0]; return ( {}} data-test-subj={'mlTrainedModelRowDetails'} /> ); diff --git a/x-pack/plugins/ml/public/application/model_management/models_list.tsx b/x-pack/plugins/ml/public/application/model_management/models_list.tsx index b2032b43ebbc3..d5f15c10aa2c6 100644 --- a/x-pack/plugins/ml/public/application/model_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/model_management/models_list.tsx @@ -283,7 +283,7 @@ export const ModelsList: FC = ({ (v) => v.state === DEPLOYMENT_STATE.STARTED ) ? DEPLOYMENT_STATE.STARTED - : ''; + : null; }); const elasticModels = models.filter((model) => diff --git a/x-pack/plugins/profiling/public/components/topn_functions/index.tsx b/x-pack/plugins/profiling/public/components/topn_functions/index.tsx index c43ecd2ac0cec..7320f11d24ec1 100644 --- a/x-pack/plugins/profiling/public/components/topn_functions/index.tsx +++ b/x-pack/plugins/profiling/public/components/topn_functions/index.tsx @@ -46,19 +46,28 @@ interface Row { } function TotalSamplesStat({ - totalSamples, - newSamples, + baselineTotalSamples, + baselineScaleFactor, + comparisonTotalSamples, + comparisonScaleFactor, }: { - totalSamples: number; - newSamples: number | undefined; + baselineTotalSamples: number; + baselineScaleFactor?: number; + comparisonTotalSamples?: number; + comparisonScaleFactor?: number; }) { - const value = totalSamples.toLocaleString(); + const scaledBaselineTotalSamples = scaleValue({ + value: baselineTotalSamples, + scaleFactor: baselineScaleFactor, + }); + + const value = scaledBaselineTotalSamples.toLocaleString(); const sampleHeader = i18n.translate('xpack.profiling.functionsView.totalSampleCountLabel', { defaultMessage: ' Total sample estimate: ', }); - if (newSamples === undefined || newSamples === 0) { + if (comparisonTotalSamples === undefined || comparisonTotalSamples === 0) { return ( {value}} @@ -67,8 +76,13 @@ function TotalSamplesStat({ ); } - const diffSamples = totalSamples - newSamples; - const percentDelta = (diffSamples / (totalSamples - diffSamples)) * 100; + const scaledComparisonTotalSamples = scaleValue({ + value: comparisonTotalSamples, + scaleFactor: comparisonScaleFactor, + }); + + const diffSamples = scaledBaselineTotalSamples - scaledComparisonTotalSamples; + const percentDelta = (diffSamples / (scaledBaselineTotalSamples - diffSamples)) * 100; return ( diff --git a/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts index 7497316927ee3..f333376b40231 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts @@ -7,8 +7,10 @@ import { deleteAlertsAndRules } from '../../../tasks/common'; import { + expandFirstAlert, goToClosedAlertsOnRuleDetailsPage, goToOpenedAlertsOnRuleDetailsPage, + openAddEndpointExceptionFromAlertActionButton, openAddEndpointExceptionFromFirstAlert, } from '../../../tasks/alerts'; import { login, visitWithoutDateRange } from '../../../tasks/login'; @@ -26,13 +28,22 @@ import { } from '../../../tasks/es_archiver'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; import { + addExceptionEntryFieldValue, + addExceptionEntryFieldValueValue, addExceptionFlyoutItemName, + editExceptionFlyoutItemName, selectCloseSingleAlerts, submitNewExceptionItem, validateExceptionConditionField, } from '../../../tasks/exceptions'; import { ALERTS_COUNT, EMPTY_ALERT_TABLE } from '../../../screens/alerts'; -import { NO_EXCEPTIONS_EXIST_PROMPT } from '../../../screens/exceptions'; +import { + ADD_AND_BTN, + EXCEPTION_CARD_ITEM_CONDITIONS, + EXCEPTION_CARD_ITEM_NAME, + EXCEPTION_ITEM_VIEWER_CONTAINER, + NO_EXCEPTIONS_EXIST_PROMPT, +} from '../../../screens/exceptions'; import { removeException, goToAlertsTab, @@ -41,10 +52,11 @@ import { describe('Endpoint Exceptions workflows from Alert', () => { const expectedNumberOfAlerts = 1; - before(() => { - esArchiverResetKibana(); - }); + const ITEM_NAME = 'Sample Exception List Item'; + const ITEM_NAME_EDIT = 'Sample Exception List Item'; + const ADDITIONAL_ENTRY = 'host.hostname'; beforeEach(() => { + esArchiverResetKibana(); login(); deleteAlertsAndRules(); esArchiverLoad('endpoint'); @@ -69,7 +81,7 @@ describe('Endpoint Exceptions workflows from Alert', () => { validateExceptionConditionField('file.Ext.code_signature'); selectCloseSingleAlerts(); - addExceptionFlyoutItemName('Sample Exception'); + addExceptionFlyoutItemName(ITEM_NAME); submitNewExceptionItem(); // Alerts table should now be empty from having added exception and closed @@ -100,4 +112,39 @@ describe('Endpoint Exceptions workflows from Alert', () => { cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlerts} alert`); }); + + it('Should be able to create Endpoint exception from Alerts take action button, and change multiple exception items without resetting to initial auto-prefilled entries', () => { + // Open first Alert Summary + expandFirstAlert(); + + // The Endpoint should populated with predefined fields + openAddEndpointExceptionFromAlertActionButton(); + + // As the endpoint.alerts-* is used to trigger the alert the + // file.Ext.code_signature will be auto-populated + validateExceptionConditionField('file.Ext.code_signature'); + addExceptionFlyoutItemName(ITEM_NAME); + + cy.get(ADD_AND_BTN).click(); + // edit conditions + addExceptionEntryFieldValue(ADDITIONAL_ENTRY, 6); + addExceptionEntryFieldValueValue('foo', 4); + + // Change the name again + editExceptionFlyoutItemName(ITEM_NAME_EDIT); + + // validate the condition is still "agent.name" or got rest after the name is changed + validateExceptionConditionField(ADDITIONAL_ENTRY); + + selectCloseSingleAlerts(); + submitNewExceptionItem(); + + // Endpoint Exception will move to Endpoint List under Exception tab of rule + goToEndpointExceptionsTab(); + + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', ITEM_NAME_EDIT); + cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).contains('span', ADDITIONAL_ENTRY); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions.cy.ts index 935da49546b49..59af4592d2ebe 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions.cy.ts @@ -12,8 +12,10 @@ import { createRule } from '../../../tasks/api_calls/rules'; import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; import { addExceptionFromFirstAlert, + expandFirstAlert, goToClosedAlertsOnRuleDetailsPage, goToOpenedAlertsOnRuleDetailsPage, + openAddRuleExceptionFromAlertActionButton, } from '../../../tasks/alerts'; import { addExceptionEntryFieldValue, @@ -26,6 +28,9 @@ import { validateExceptionItemAffectsTheCorrectRulesInRulePage, validateExceptionConditionField, validateExceptionCommentCountAndText, + editExceptionFlyoutItemName, + validateHighlightedFieldsPopulatedAsExceptionConditions, + validateEmptyExceptionConditionField, } from '../../../tasks/exceptions'; import { esArchiverLoad, @@ -42,26 +47,44 @@ import { import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; import { postDataView, deleteAlertsAndRules } from '../../../tasks/common'; -import { NO_EXCEPTIONS_EXIST_PROMPT } from '../../../screens/exceptions'; +import { + ADD_AND_BTN, + ENTRY_DELETE_BTN, + EXCEPTION_CARD_ITEM_CONDITIONS, + EXCEPTION_CARD_ITEM_NAME, + EXCEPTION_ITEM_VIEWER_CONTAINER, + NO_EXCEPTIONS_EXIST_PROMPT, +} from '../../../screens/exceptions'; import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; +const loadEndpointRuleAndAlerts = () => { + esArchiverLoad('endpoint'); + login(); + createRule(getEndpointRule()); + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + waitForAlertsToPopulate(); +}; + describe('Rule Exceptions workflows from Alert', () => { const EXPECTED_NUMBER_OF_ALERTS = '1 alert'; - const ITEM_NAME = 'Sample Exception List Item'; + const ITEM_NAME = 'Sample Exception Item'; + const ITEM_NAME_EDIT = 'Sample Exception Item Edit'; + const ADDITIONAL_ENTRY = 'host.hostname'; const newRule = getNewRule(); beforeEach(() => { esArchiverResetKibana(); - deleteAlertsAndRules(); }); after(() => { esArchiverUnload('exceptions'); + deleteAlertsAndRules(); }); afterEach(() => { esArchiverUnload('exceptions_2'); }); - it('Creates an exception item from alert actions overflow menu and close all matching alerts', () => { + it('Should create a Rule exception item from alert actions overflow menu and close all matching alerts', () => { esArchiverLoad('exceptions'); login(); postDataView('exceptions-*'); @@ -119,14 +142,8 @@ describe('Rule Exceptions workflows from Alert', () => { cy.get(ALERTS_COUNT).should('have.text', '2 alerts'); }); - - it('Creates an exception item from alert actions overflow menu and auto populate the conditions using alert Highlighted fields ', () => { - esArchiverLoad('endpoint'); - login(); - createRule(getEndpointRule()); - visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - goToRuleDetails(); - waitForAlertsToPopulate(); + it('Should create a Rule exception item from alert actions overflow menu and auto populate the conditions using alert Highlighted fields', () => { + loadEndpointRuleAndAlerts(); cy.get(LOADING_INDICATOR).should('not.exist'); addExceptionFromFirstAlert(); @@ -144,9 +161,45 @@ describe('Rule Exceptions workflows from Alert', () => { * fields are based on the alert document that should be generated * when the endpoint rule runs */ - highlightedFieldsBasedOnAlertDoc.forEach((field, index) => { - validateExceptionConditionField(field); - }); + validateHighlightedFieldsPopulatedAsExceptionConditions(highlightedFieldsBasedOnAlertDoc); + + /** + * Validate that the comments are opened by default with one comment added + * showing a text contains information about the pre-filled conditions + */ + validateExceptionCommentCountAndText( + 1, + 'Exception conditions are pre-filled with relevant data from alert with "id"' + ); + + addExceptionFlyoutItemName(ITEM_NAME); + submitNewExceptionItem(); + }); + it('Should create a Rule exception from Alerts take action button and change multiple exception items without resetting to initial auto-prefilled entries', () => { + loadEndpointRuleAndAlerts(); + + cy.get(LOADING_INDICATOR).should('not.exist'); + + // Open first Alert Summary + expandFirstAlert(); + + // The Rule exception should populated with highlighted fields + openAddRuleExceptionFromAlertActionButton(); + + const highlightedFieldsBasedOnAlertDoc = [ + 'host.name', + 'agent.id', + 'user.name', + 'process.executable', + 'file.path', + ]; + + /** + * Validate the highlighted fields are auto populated, these + * fields are based on the alert document that should be generated + * when the endpoint rule runs + */ + validateHighlightedFieldsPopulatedAsExceptionConditions(highlightedFieldsBasedOnAlertDoc); /** * Validate that the comments are opened by default with one comment added @@ -154,10 +207,74 @@ describe('Rule Exceptions workflows from Alert', () => { */ validateExceptionCommentCountAndText( 1, - 'Exception conditions are pre-filled with relevant data from' + 'Exception conditions are pre-filled with relevant data from alert with "id"' ); addExceptionFlyoutItemName(ITEM_NAME); + + cy.get(ADD_AND_BTN).click(); + + // edit conditions + addExceptionEntryFieldValue(ADDITIONAL_ENTRY, 5); + addExceptionEntryFieldValueValue('foo', 5); + + // Change the name again + editExceptionFlyoutItemName(ITEM_NAME_EDIT); + + // validate the condition is still 'host.hostname' or got rest after the name is changed + validateExceptionConditionField(ADDITIONAL_ENTRY); + submitNewExceptionItem(); + + goToExceptionsTab(); + + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', ITEM_NAME_EDIT); + cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).contains('span', 'host.hostname'); + }); + it('Should delete all prefilled exception entries when creating a Rule exception from Alerts take action button without resetting to initial auto-prefilled entries', () => { + loadEndpointRuleAndAlerts(); + + cy.get(LOADING_INDICATOR).should('not.exist'); + + // Open first Alert Summary + expandFirstAlert(); + + // The Rule exception should populated with highlighted fields + openAddRuleExceptionFromAlertActionButton(); + + const highlightedFieldsBasedOnAlertDoc = [ + 'host.name', + 'agent.id', + 'user.name', + 'process.executable', + 'file.path', + ]; + + /** + * Validate the highlighted fields are auto populated, these + * fields are based on the alert document that should be generated + * when the endpoint rule runs + */ + validateHighlightedFieldsPopulatedAsExceptionConditions(highlightedFieldsBasedOnAlertDoc); + + /** + * Delete all the highlighted fields to see if any condition + * will prefuilled again. + */ + const highlightedFieldsCount = highlightedFieldsBasedOnAlertDoc.length - 1; + highlightedFieldsBasedOnAlertDoc.forEach((_, index) => + cy + .get(ENTRY_DELETE_BTN) + .eq(highlightedFieldsCount - index) + .click() + ); + + /** + * Validate that there are no highlighted fields are auto populated + * after the deletion + */ + validateEmptyExceptionConditionField(); }); }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index c5d2aebee7c54..67d61925fa164 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -92,16 +92,18 @@ export const openAddEndpointExceptionFromFirstAlert = () => { cy.get(FIELD_INPUT).should('be.visible'); }; -export const openAddExceptionFromAlertDetails = () => { - cy.get(EXPAND_ALERT_BTN).first().click({ force: true }); - +export const openAddRuleExceptionFromAlertActionButton = () => { cy.get(TAKE_ACTION_BTN).click(); cy.get(TAKE_ACTION_MENU).should('be.visible'); - cy.get(ADD_EXCEPTION_BTN).click(); - cy.get(ADD_EXCEPTION_BTN).should('not.be.visible'); + cy.get(ADD_EXCEPTION_BTN, { timeout: 10000 }).first().click(); }; +export const openAddEndpointExceptionFromAlertActionButton = () => { + cy.get(TAKE_ACTION_BTN).click(); + cy.get(TAKE_ACTION_MENU).should('be.visible'); + cy.get(ADD_ENDPOINT_EXCEPTION_BTN, { timeout: 10000 }).first().click(); +}; export const closeFirstAlert = () => { expandFirstAlertActions(); cy.get(CLOSE_ALERT_BTN).click(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts b/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts index 884c4521985fc..9195153d46f6f 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts @@ -167,6 +167,9 @@ export const addExceptionConditions = (exception: Exception) => { export const validateExceptionConditionField = (value: string) => { cy.get(EXCEPTION_ITEM_CONTAINER).contains('span', value); }; +export const validateEmptyExceptionConditionField = () => { + cy.get(FIELD_INPUT).should('be.empty'); +}; export const submitNewExceptionItem = () => { cy.get(CONFIRM_BTN).click(); cy.get(CONFIRM_BTN).should('not.exist'); @@ -279,3 +282,8 @@ export const deleteFirstExceptionItemInListDetailPage = () => { // Delete exception cy.get(EXCEPTION_ITEM_OVERFLOW_ACTION_DELETE).click(); }; +export const validateHighlightedFieldsPopulatedAsExceptionConditions = ( + highlightedFields: string[] +) => { + return highlightedFields.every((field) => validateExceptionConditionField(field)); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/translations.ts index 4b4ad1dac1b5e..215bb3fc29923 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/translations.ts @@ -89,6 +89,7 @@ export const ADD_RULE_EXCEPTION_FROM_ALERT_COMMENT = (alertId: string) => 'xpack.securitySolution.ruleExceptions.addExceptionFlyout.addRuleExceptionFromAlertComment', { values: { alertId }, - defaultMessage: 'Exception conditions are pre-filled with relevant data from {alertId}.', + defaultMessage: + 'Exception conditions are pre-filled with relevant data from alert with "id" {alertId}.', } ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx index 38ece2d1ba43c..0f32e2b4d1ab8 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx @@ -96,7 +96,7 @@ export const ExceptionItemComments = memo(function ExceptionItemComments({ } else { return null; } - }, [shouldShowComments, exceptionItemComments]); + }, [exceptionItemComments, shouldShowComments]); const formattedComments = useMemo((): EuiCommentProps[] => { if (exceptionItemComments && exceptionItemComments.length > 0) { @@ -105,11 +105,10 @@ export const ExceptionItemComments = memo(function ExceptionItemComments({ return []; } }, [exceptionItemComments]); - return (