diff --git a/.buildkite/pipelines/pull_request/security_solution.yml b/.buildkite/pipelines/pull_request/security_solution.yml index 2b73e9482b156..7e06d4f48c9ea 100644 --- a/.buildkite/pipelines/pull_request/security_solution.yml +++ b/.buildkite/pipelines/pull_request/security_solution.yml @@ -11,4 +11,15 @@ steps: - exit_status: '*' limit: 1 artifact_paths: - - "target/kibana-security-solution/**/*" \ No newline at end of file + - "target/kibana-security-solution/**/*" + + - command: .buildkite/scripts/steps/functional/security_solution_burn.sh + label: 'Security Solution Cypress tests, burning changed specs' + agents: + queue: n2-4-spot + depends_on: build + timeout_in_minutes: 120 + parallelism: 1 + soft_fail: true + artifact_paths: + - "target/kibana-security-solution/**/*" \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 89e4a2c008727..684f2ffd3b548 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1283,7 +1283,11 @@ x-pack/plugins/security_solution/server/lib/telemetry/ @elastic/security-data-an ## Security Solution sub teams - security-engineering-productivity x-pack/test/security_solution_cypress/cypress/README.md @elastic/security-engineering-productivity -x-pack/test/security_solution_cypress @elastic/security-engineering-productivity +x-pack/test/security_solution_cypress/es_archives @elastic/security-engineering-productivity +x-pack/test/security_solution_cypress/cli_config.ts @elastic/security-engineering-productivity +x-pack/test/security_solution_cypress/config.ts @elastic/security-engineering-productivity +x-pack/test/security_solution_cypress/runner.ts @elastic/security-engineering-productivity +x-pack/test/security_solution_cypress/serverless_config.ts @elastic/security-engineering-productivity ## Security Solution sub teams - adaptive-workload-protection x-pack/plugins/security_solution/public/common/components/sessions_viewer @elastic/sec-cloudnative-integrations diff --git a/package.json b/package.json index a4a18ab175ad0..b6411725081f4 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "@elastic/apm-rum-react": "^1.4.4", "@elastic/charts": "59.1.0", "@elastic/datemath": "5.0.3", - "@elastic/elasticsearch": "npm:@elastic/elasticsearch@8.9.0", + "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.9.1-canary.1", "@elastic/ems-client": "8.4.0", "@elastic/eui": "86.0.0", "@elastic/filesaver": "1.1.2", @@ -1355,7 +1355,7 @@ "@types/source-map-support": "^0.5.3", "@types/stats-lite": "^2.2.0", "@types/styled-components": "^5.1.0", - "@types/supertest": "^2.0.5", + "@types/supertest": "^2.0.12", "@types/tapable": "^1.0.6", "@types/tar": "^6.1.5", "@types/tempy": "^0.2.0", @@ -1537,8 +1537,8 @@ "style-loader": "^1.1.3", "stylelint": "^14.9.1", "stylelint-scss": "^4.3.0", - "superagent": "^3.8.2", - "supertest": "^3.1.0", + "superagent": "^8.1.2", + "supertest": "^6.3.3", "supports-color": "^7.0.0", "svgo": "^2.8.0", "tape": "^5.0.1", diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 0268757c12d78..23121d9fa5e9d 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -138,6 +138,8 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { connectorsConfluence: `${ENTERPRISE_SEARCH_DOCS}connectors-confluence.html`, connectorsDropbox: `${ENTERPRISE_SEARCH_DOCS}connectors-dropbox.html`, connectorsContentExtraction: `${ENTERPRISE_SEARCH_DOCS}connectors-content-extraction.html`, + connectorsGithub: `${ENTERPRISE_SEARCH_DOCS}connectors-github.html`, + connectorsGmail: `${ENTERPRISE_SEARCH_DOCS}connectors-gmail.html`, connectorsGoogleCloudStorage: `${ENTERPRISE_SEARCH_DOCS}connectors-google-cloud.html`, connectorsGoogleDrive: `${ENTERPRISE_SEARCH_DOCS}connectors-google-drive.html`, connectorsJira: `${ENTERPRISE_SEARCH_DOCS}connectors-jira.html`, @@ -146,12 +148,15 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { connectorsMySQL: `${ENTERPRISE_SEARCH_DOCS}connectors-mysql.html`, connectorsNative: `${ENTERPRISE_SEARCH_DOCS}connectors.html#connectors-native`, connectorsNetworkDrive: `${ENTERPRISE_SEARCH_DOCS}connectors-network-drive.html`, + connectorsOneDrive: `${ENTERPRISE_SEARCH_DOCS}connectors-onedrive.html`, connectorsOracle: `${ENTERPRISE_SEARCH_DOCS}connectors-oracle.html`, connectorsPostgreSQL: `${ENTERPRISE_SEARCH_DOCS}connectors-postgresql.html`, connectorsS3: `${ENTERPRISE_SEARCH_DOCS}connectors-s3.html`, + connectorsSalesforce: `${ENTERPRISE_SEARCH_DOCS}connectors-salesforce.html`, connectorsServiceNow: `${ENTERPRISE_SEARCH_DOCS}connectors-servicenow.html`, connectorsSharepoint: `${ENTERPRISE_SEARCH_DOCS}connectors-sharepoint.html`, connectorsSharepointOnline: `${ENTERPRISE_SEARCH_DOCS}connectors-sharepoint-online.html`, + connectorsSlack: `${ENTERPRISE_SEARCH_DOCS}connectors-slack.html`, connectorsWorkplaceSearch: `${ENTERPRISE_SEARCH_DOCS}workplace-search-connectors.html`, crawlerExtractionRules: `${ENTERPRISE_SEARCH_DOCS}crawler-extraction-rules.html`, crawlerManaging: `${ENTERPRISE_SEARCH_DOCS}crawler-managing.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 339b8c89263a5..6dd1fc6d2cea0 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -122,20 +122,25 @@ export interface DocLinks { readonly connectorsConfluence: string; readonly connectorsContentExtraction: string; readonly connectorsDropbox: string; + readonly connectorsGithub: string; readonly connectorsGoogleCloudStorage: string; readonly connectorsGoogleDrive: string; + readonly connectorsGmail: string; readonly connectorsJira: string; readonly connectorsMicrosoftSQL: string; readonly connectorsMongoDB: string; readonly connectorsMySQL: string; readonly connectorsNative: string; readonly connectorsNetworkDrive: string; + readonly connectorsOneDrive: string; readonly connectorsOracle: string; readonly connectorsPostgreSQL: string; readonly connectorsS3: string; + readonly connectorsSalesforce: string; readonly connectorsServiceNow: string; readonly connectorsSharepoint: string; readonly connectorsSharepointOnline: string; + readonly connectorsSlack: string; readonly connectorsWorkplaceSearch: string; readonly crawlerExtractionRules: string; readonly crawlerManaging: string; diff --git a/packages/kbn-securitysolution-utils/src/transform_data_to_ndjson/index.test.ts b/packages/kbn-securitysolution-utils/src/transform_data_to_ndjson/index.test.ts index 14f6201cc8283..32300a2e66c96 100644 --- a/packages/kbn-securitysolution-utils/src/transform_data_to_ndjson/index.test.ts +++ b/packages/kbn-securitysolution-utils/src/transform_data_to_ndjson/index.test.ts @@ -20,6 +20,7 @@ const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE) => ({ enabled: true, false_positives: ['false positive 1', 'false positive 2'], from: 'now-6m', + investigation_fields: ['custom.field1', 'custom.field2'], immutable: false, name: 'Query with a rule id', query: 'user.name: root or user.name: admin', diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts index 1fc7b2fcd775c..f492b49b8849d 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.test.ts @@ -75,8 +75,7 @@ const createSearchFnMock = (nrOfHits: number) => { const dataViewMock = buildDataViewMock({ name: 'the-data-view', fields: deepMockedFields }); -// FLAKY: https://github.com/elastic/kibana/issues/162997 -describe.skip('saved search embeddable', () => { +describe('saved search embeddable', () => { let mountpoint: HTMLDivElement; let servicesMock: jest.Mocked<DiscoverServices>; @@ -322,7 +321,8 @@ describe.skip('saved search embeddable', () => { expect(search).toHaveBeenCalledTimes(1); }); - it('should not reload when the input title doesnt change', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/162997 + it.skip('should not reload when the input title doesnt change', async () => { const search = jest.fn().mockReturnValue(getSearchResponse(1)); const { embeddable } = createEmbeddable({ searchMock: search, customTitle: 'custom title' }); await waitOneTick(); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx index eb05133c70835..773b46bdb2ab2 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_header/index.tsx @@ -80,7 +80,11 @@ export const AssistantHeader: React.FC<Props> = ({ justifyContent={'spaceBetween'} > <EuiFlexItem grow={false}> - <AssistantTitle {...currentTitle} docLinks={docLinks} /> + <AssistantTitle + {...currentTitle} + docLinks={docLinks} + selectedConversation={currentConversation} + /> </EuiFlexItem> <EuiFlexItem diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.test.tsx index 9bf50e5f084aa..e24d8fe48291b 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.test.tsx @@ -14,18 +14,29 @@ const testProps = { title: 'Test Title', titleIcon: 'globe', docLinks: { ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', DOC_LINK_VERSION: '7.15' }, + selectedConversation: undefined, }; + describe('AssistantTitle', () => { it('the component renders correctly with valid props', () => { - const { getByText, container } = render(<AssistantTitle {...testProps} />); + const { getByText, container } = render( + <TestProviders> + <AssistantTitle {...testProps} /> + </TestProviders> + ); expect(getByText('Test Title')).toBeInTheDocument(); expect(container.querySelector('[data-euiicon-type="globe"]')).not.toBeNull(); }); it('clicking on the popover button opens the popover with the correct link', () => { - const { getByTestId, queryByTestId } = render(<AssistantTitle {...testProps} />, { - wrapper: TestProviders, - }); + const { getByTestId, queryByTestId } = render( + <TestProviders> + <AssistantTitle {...testProps} /> + </TestProviders>, + { + wrapper: TestProviders, + } + ); expect(queryByTestId('tooltipContent')).not.toBeInTheDocument(); fireEvent.click(getByTestId('tooltipIcon')); expect(getByTestId('tooltipContent')).toBeInTheDocument(); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.tsx index 719a02aaee132..f2d77c9fb6716 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/assistant_title/index.tsx @@ -15,10 +15,14 @@ import { EuiModalHeaderTitle, EuiPopover, EuiText, + EuiTitle, } from '@elastic/eui'; import type { DocLinksStart } from '@kbn/core-doc-links-browser'; import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; import * as i18n from '../translations'; +import type { Conversation } from '../../..'; +import { ConnectorSelectorInline } from '../../connectorland/connector_selector_inline/connector_selector_inline'; /** * Renders a header title with an icon, a tooltip button, and a popover with @@ -28,7 +32,10 @@ export const AssistantTitle: React.FC<{ title: string | JSX.Element; titleIcon: string; docLinks: Omit<DocLinksStart, 'links'>; -}> = ({ title, titleIcon, docLinks }) => { + selectedConversation: Conversation | undefined; +}> = ({ title, titleIcon, docLinks, selectedConversation }) => { + const selectedConnectorId = selectedConversation?.apiConfig?.connectorId; + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const url = `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/security-assistant.html`; @@ -66,32 +73,57 @@ export const AssistantTitle: React.FC<{ return ( <EuiModalHeaderTitle> - <EuiFlexGroup gutterSize="xs" alignItems="center"> - <EuiFlexItem grow={false}> + <EuiFlexGroup gutterSize="m"> + <EuiFlexItem + grow={false} + css={css` + margin-top: 3px; + `} + > <EuiIcon data-test-subj="titleIcon" type={titleIcon} size="xl" /> </EuiFlexItem> - <EuiFlexItem grow={false}>{title}</EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiPopover - button={ - <EuiButtonIcon - aria-label={i18n.TOOLTIP_ARIA_LABEL} - data-test-subj="tooltipIcon" - iconSize="l" - iconType="iInCircle" - onClick={onButtonClick} - /> - } - isOpen={isPopoverOpen} - closePopover={closePopover} - anchorPosition="upCenter" - > - <EuiText data-test-subj="tooltipContent" grow={false} css={{ maxWidth: '400px' }}> - <h4>{i18n.TOOLTIP_TITLE}</h4> - <p>{content}</p> - </EuiText> - </EuiPopover> - </EuiFlexItem> + <EuiFlexGroup direction="column" gutterSize="none" justifyContent="center"> + <EuiFlexItem grow={false}> + <EuiFlexGroup gutterSize="xs" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiTitle size={'s'}> + <h3>{title}</h3> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiPopover + button={ + <EuiButtonIcon + aria-label={i18n.TOOLTIP_ARIA_LABEL} + data-test-subj="tooltipIcon" + iconType="iInCircle" + onClick={onButtonClick} + /> + } + isOpen={isPopoverOpen} + closePopover={closePopover} + anchorPosition="rightUp" + > + <EuiText data-test-subj="tooltipContent" grow={false} css={{ maxWidth: '400px' }}> + <h4>{i18n.TOOLTIP_TITLE}</h4> + <EuiText size={'s'}> + <p>{content}</p> + </EuiText> + </EuiText> + </EuiPopover> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <ConnectorSelectorInline + isDisabled={selectedConversation === undefined} + onConnectorModalVisibilityChange={() => {}} + onConnectorSelectionChange={() => {}} + selectedConnectorId={selectedConnectorId} + selectedConversation={selectedConversation} + /> + </EuiFlexItem> + </EuiFlexGroup> </EuiFlexGroup> </EuiModalHeaderTitle> ); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.tsx index 671cac8ea01c6..1c56e005892c5 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_missing_callout/index.tsx @@ -46,7 +46,7 @@ export const ConnectorMissingCallout: React.FC<Props> = React.memo( <p> {' '} <FormattedMessage - defaultMessage="Select a connector from the {link} to continue" + defaultMessage="Select a connector above or from the {link} to continue" id="xpack.elasticAssistant.assistant.connectors.connectorMissingCallout.calloutDescription" values={{ link: ( diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.test.tsx new file mode 100644 index 0000000000000..774098eba8b2e --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.test.tsx @@ -0,0 +1,116 @@ +/* + * 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 React from 'react'; +import { render } from '@testing-library/react'; + +import { noop } from 'lodash/fp'; +import { TestProviders } from '../../mock/test_providers/test_providers'; +import { ConnectorSelectorInline } from './connector_selector_inline'; +import * as i18n from '../translations'; +import { Conversation } from '../../..'; +import { useLoadConnectors } from '../use_load_connectors'; + +jest.mock('@kbn/triggers-actions-ui-plugin/public/common/constants', () => ({ + loadActionTypes: jest.fn(() => { + return Promise.resolve([ + { + id: '.gen-ai', + name: 'Gen AI', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, + ]); + }), +})); + +jest.mock('../use_load_connectors', () => ({ + useLoadConnectors: jest.fn(() => { + return { + data: [], + error: null, + isSuccess: true, + }; + }), +})); + +const mockConnectors = [ + { + id: 'connectorId', + name: 'Captain Connector', + isMissingSecrets: false, + actionTypeId: '.gen-ai', + config: { + apiProvider: 'OpenAI', + }, + }, +]; + +(useLoadConnectors as jest.Mock).mockReturnValue({ + data: mockConnectors, + error: null, + isSuccess: true, +}); + +describe('ConnectorSelectorInline', () => { + it('renders empty view if no selected conversation is provided', () => { + const { getByText } = render( + <TestProviders> + <ConnectorSelectorInline + isDisabled={false} + onConnectorModalVisibilityChange={noop} + onConnectorSelectionChange={noop} + selectedConnectorId={undefined} + selectedConversation={undefined} + /> + </TestProviders> + ); + expect(getByText(i18n.INLINE_CONNECTOR_PLACEHOLDER)).toBeInTheDocument(); + }); + + it('renders empty view if selectedConnectorId is NOT in list of connectors', () => { + const conversation: Conversation = { + id: 'conversation_id', + messages: [], + apiConfig: {}, + }; + const { getByText } = render( + <TestProviders> + <ConnectorSelectorInline + isDisabled={false} + onConnectorModalVisibilityChange={noop} + onConnectorSelectionChange={noop} + selectedConnectorId={'missing-connector-id'} + selectedConversation={conversation} + /> + </TestProviders> + ); + expect(getByText(i18n.INLINE_CONNECTOR_PLACEHOLDER)).toBeInTheDocument(); + }); + + it('renders selected connector if selected selectedConnectorId is in list of connectors', () => { + const conversation: Conversation = { + id: 'conversation_id', + messages: [], + apiConfig: {}, + }; + const { getByText } = render( + <TestProviders> + <ConnectorSelectorInline + isDisabled={false} + onConnectorModalVisibilityChange={noop} + onConnectorSelectionChange={noop} + selectedConnectorId={mockConnectors[0].id} + selectedConversation={conversation} + /> + </TestProviders> + ); + expect(getByText(mockConnectors[0].name)).toBeInTheDocument(); + }); +}); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.tsx b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.tsx new file mode 100644 index 0000000000000..09b77c642ffcf --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/connector_selector_inline/connector_selector_inline.tsx @@ -0,0 +1,286 @@ +/* + * 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSuperSelect, EuiText } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; + +import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public'; + +import { ActionConnectorProps } from '@kbn/triggers-actions-ui-plugin/public/types'; +import { ConnectorAddModal } from '@kbn/triggers-actions-ui-plugin/public/common/constants'; +import { + GEN_AI_CONNECTOR_ID, + OpenAiProviderType, +} from '@kbn/stack-connectors-plugin/public/common'; +import { css } from '@emotion/css/dist/emotion-css.cjs'; +import { Conversation } from '../../..'; +import { useLoadConnectors } from '../use_load_connectors'; +import * as i18n from '../translations'; +import { useLoadActionTypes } from '../use_load_action_types'; +import { useAssistantContext } from '../../assistant_context'; +import { useConversation } from '../../assistant/use_conversation'; + +export const ADD_NEW_CONNECTOR = 'ADD_NEW_CONNECTOR'; +interface Props { + isDisabled?: boolean; + onConnectorSelectionChange: (connectorId: string, provider: OpenAiProviderType) => void; + selectedConnectorId?: string; + selectedConversation?: Conversation; + onConnectorModalVisibilityChange?: (isVisible: boolean) => void; +} + +interface Config { + apiProvider: string; +} + +const inputContainerClassName = css` + height: 32px; + + .euiSuperSelect { + width: 400px; + } + + .euiSuperSelectControl { + border: none; + box-shadow: none; + background: none; + padding-left: 0; + } + + .euiFormControlLayoutIcons { + right: 14px; + top: 2px; + } +`; + +const inputDisplayClassName = css` + overflow: hidden; + text-overflow: ellipsis; + max-width: 400px; +`; + +const placeholderButtonClassName = css` + overflow: hidden; + text-overflow: ellipsis; + max-width: 400px; + font-weight: normal; + padding-bottom: 5px; + padding-left: 0; + padding-top: 2px; +`; + +/** + * A minimal and connected version of the ConnectorSelector component used in the Settings modal. + */ +export const ConnectorSelectorInline: React.FC<Props> = React.memo( + ({ + isDisabled = false, + onConnectorModalVisibilityChange, + selectedConnectorId, + selectedConversation, + onConnectorSelectionChange, + }) => { + const [isOpen, setIsOpen] = useState<boolean>(false); + const { actionTypeRegistry, http } = useAssistantContext(); + const { setApiConfig } = useConversation(); + // Connector Modal State + const [isConnectorModalVisible, setIsConnectorModalVisible] = useState<boolean>(false); + const { data: actionTypes } = useLoadActionTypes({ http }); + const actionType = actionTypes?.find((at) => at.id === GEN_AI_CONNECTOR_ID) ?? { + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'platinum', + supportedFeatureIds: ['general'], + isSystemActionType: false, + id: '.gen-ai', + name: 'Generative AI', + enabled: true, + }; + + const { + data: connectors, + isLoading: isLoadingActionTypes, + isFetching: isFetchingActionTypes, + refetch: refetchConnectors, + } = useLoadConnectors({ http }); + const isLoading = isLoadingActionTypes || isFetchingActionTypes; + const selectedConnectorName = + connectors?.find((c) => c.id === selectedConnectorId)?.name ?? + i18n.INLINE_CONNECTOR_PLACEHOLDER; + + const addNewConnectorOption = useMemo(() => { + return { + value: ADD_NEW_CONNECTOR, + inputDisplay: i18n.ADD_NEW_CONNECTOR, + dropdownDisplay: ( + <EuiFlexGroup gutterSize="none" key={ADD_NEW_CONNECTOR}> + <EuiFlexItem grow={true}> + <EuiButtonEmpty data-test-subj="addNewConnectorButton" iconType="plus" size="xs"> + {i18n.ADD_NEW_CONNECTOR} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + {/* Right offset to compensate for 'selected' icon of EuiSuperSelect since native footers aren't supported*/} + <div style={{ width: '24px' }} /> + </EuiFlexItem> + </EuiFlexGroup> + ), + }; + }, []); + + const connectorOptions = useMemo(() => { + return ( + connectors?.map((connector) => { + const apiProvider: string | undefined = ( + connector as ActionConnectorProps<Config, unknown> + )?.config?.apiProvider; + return { + value: connector.id, + inputDisplay: ( + <EuiText className={inputDisplayClassName} size="xs"> + {connector.name} + </EuiText> + ), + dropdownDisplay: ( + <React.Fragment key={connector.id}> + <strong>{connector.name}</strong> + {apiProvider && ( + <EuiText size="xs" color="subdued"> + <p>{apiProvider}</p> + </EuiText> + )} + </React.Fragment> + ), + }; + }) ?? [] + ); + }, [connectors]); + + const cleanupAndCloseModal = useCallback(() => { + onConnectorModalVisibilityChange?.(false); + setIsConnectorModalVisible(false); + }, [onConnectorModalVisibilityChange]); + + const onConnectorClick = useCallback(() => { + setIsOpen(!isOpen); + }, [isOpen]); + + const handleOnBlur = useCallback(() => setIsOpen(false), []); + + const onChange = useCallback( + (connectorId: string, apiProvider?: OpenAiProviderType) => { + setIsOpen(false); + + if (connectorId === ADD_NEW_CONNECTOR) { + onConnectorModalVisibilityChange?.(true); + setIsConnectorModalVisible(true); + return; + } + + const provider = + apiProvider ?? + ((connectors?.find((c) => c.id === connectorId) as ActionConnectorProps<Config, unknown>) + ?.config.apiProvider as OpenAiProviderType); + + if (selectedConversation != null) { + setApiConfig({ + conversationId: selectedConversation.id, + apiConfig: { + ...selectedConversation.apiConfig, + connectorId, + provider, + }, + }); + } + + onConnectorSelectionChange(connectorId, provider); + }, + [ + connectors, + selectedConversation, + onConnectorSelectionChange, + onConnectorModalVisibilityChange, + setApiConfig, + ] + ); + + const placeholderComponent = useMemo( + () => ( + <EuiText color="default" size={'xs'}> + {i18n.INLINE_CONNECTOR_PLACEHOLDER} + </EuiText> + ), + [] + ); + + return ( + <EuiFlexGroup + alignItems="center" + className={inputContainerClassName} + direction="row" + gutterSize="xs" + justifyContent={'flexStart'} + responsive={false} + > + <EuiFlexItem grow={false}> + <EuiText size="xs" color="subdued"> + {i18n.INLINE_CONNECTOR_LABEL} + </EuiText> + </EuiFlexItem> + <EuiFlexItem> + {isOpen ? ( + <EuiSuperSelect + aria-label={i18n.CONNECTOR_SELECTOR_TITLE} + compressed={true} + disabled={isDisabled} + hasDividers={true} + isLoading={isLoading} + isOpen={isOpen} + onBlur={handleOnBlur} + onChange={onChange} + options={[...connectorOptions, addNewConnectorOption]} + placeholder={placeholderComponent} + valueOfSelected={selectedConnectorId} + /> + ) : ( + <span> + <EuiButtonEmpty + className={placeholderButtonClassName} + color={'text'} + data-test-subj="connectorSelectorPlaceholderButton" + iconSide={'right'} + iconType="arrowDown" + isDisabled={isDisabled} + onClick={onConnectorClick} + size="xs" + > + {selectedConnectorName} + </EuiButtonEmpty> + </span> + )} + {isConnectorModalVisible && ( + <ConnectorAddModal + actionType={actionType} + onClose={cleanupAndCloseModal} + postSaveEventHandler={(savedAction: ActionConnector) => { + const provider = (savedAction as ActionConnectorProps<Config, unknown>)?.config + .apiProvider as OpenAiProviderType; + onChange(savedAction.id, provider); + onConnectorSelectionChange(savedAction.id, provider); + refetchConnectors?.(); + cleanupAndCloseModal(); + }} + actionTypeRegistry={actionTypeRegistry} + /> + )} + </EuiFlexItem> + </EuiFlexGroup> + ); + } +); + +ConnectorSelectorInline.displayName = 'ConnectorSelectorInline'; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/translations.ts b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/translations.ts index df6343f62b4e2..2c8523f2966b9 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/connectorland/translations.ts +++ b/x-pack/packages/kbn-elastic-assistant/impl/connectorland/translations.ts @@ -45,6 +45,20 @@ export const ADD_NEW_CONNECTOR = i18n.translate( } ); +export const INLINE_CONNECTOR_LABEL = i18n.translate( + 'xpack.elasticAssistant.assistant.connectors.connectorSelectorInline.connectorLabel', + { + defaultMessage: 'Connector:', + } +); + +export const INLINE_CONNECTOR_PLACEHOLDER = i18n.translate( + 'xpack.elasticAssistant.assistant.connectors.connectorSelectorInline.connectorPlaceholder', + { + defaultMessage: 'Select a Connector', + } +); + export const ADD_CONNECTOR_TITLE = i18n.translate( 'xpack.elasticAssistant.assistant.connectors.addConnectorButton.title', { diff --git a/x-pack/plugins/apm/public/components/app/onboarding/agent_config_table.tsx b/x-pack/plugins/apm/public/components/app/onboarding/agent_config_table.tsx index 8f73bef94e581..1c232f14e7187 100644 --- a/x-pack/plugins/apm/public/components/app/onboarding/agent_config_table.tsx +++ b/x-pack/plugins/apm/public/components/app/onboarding/agent_config_table.tsx @@ -53,7 +53,12 @@ function ConfigurationValueColumn({ </EuiText> {value && ( <EuiButtonIcon - aria-label="Copy to clipboard" + aria-label={i18n.translate( + 'xpack.apm.onboarding.column.value.copyIconText', + { + defaultMessage: 'Copy to clipboard', + } + )} color="text" iconType="copy" onClick={() => copyToClipboard(value)} diff --git a/x-pack/plugins/apm/public/components/app/onboarding/instructions/otel_agent.tsx b/x-pack/plugins/apm/public/components/app/onboarding/instructions/otel_agent.tsx index 6d00acd9af7be..9ae2df7fb5382 100644 --- a/x-pack/plugins/apm/public/components/app/onboarding/instructions/otel_agent.tsx +++ b/x-pack/plugins/apm/public/components/app/onboarding/instructions/otel_agent.tsx @@ -7,9 +7,11 @@ import { i18n } from '@kbn/i18n'; import { + copyToClipboard, EuiBasicTable, EuiBasicTableColumn, EuiButton, + EuiButtonIcon, EuiLink, EuiMarkdownFormat, EuiSpacer, @@ -144,9 +146,24 @@ function ConfigurationValueColumn({ } return ( - <EuiText size="s" color="accent"> - {value} - </EuiText> + <> + <EuiText size="s" color="accent"> + {value} + </EuiText> + {value && ( + <EuiButtonIcon + aria-label={i18n.translate( + 'xpack.apm.onboarding.otel.column.value.copyIconText', + { + defaultMessage: 'Copy to clipboard', + } + )} + color="text" + iconType="copy" + onClick={() => copyToClipboard(value)} + /> + )} + </> ); } diff --git a/x-pack/plugins/apm/public/components/app/onboarding/instructions_set.tsx b/x-pack/plugins/apm/public/components/app/onboarding/instructions_set.tsx index f7fc8eed040cc..786770091f0f7 100644 --- a/x-pack/plugins/apm/public/components/app/onboarding/instructions_set.tsx +++ b/x-pack/plugins/apm/public/components/app/onboarding/instructions_set.tsx @@ -14,6 +14,7 @@ import { EuiSpacer, } from '@elastic/eui'; import React, { useState } from 'react'; +import { useEuiTheme } from '@elastic/eui'; import { INSTRUCTION_VARIANT, getDisplayText, @@ -44,10 +45,11 @@ export function InstructionsSet({ const onSelectedTabChange = (tab: string) => { setSelectedTab(tab); }; + const { euiTheme } = useEuiTheme(); function InstructionTabs({ agentTabs }: { agentTabs: AgentTab[] }) { return ( - <EuiTabs> + <EuiTabs style={{ padding: `0 ${euiTheme.size.l}` }}> {agentTabs.map((tab) => ( <EuiTab key={tab.id} diff --git a/x-pack/plugins/apm/public/components/app/onboarding/serverless_instructions.ts b/x-pack/plugins/apm/public/components/app/onboarding/serverless_instructions.ts index 568ad67bc3fe3..d04b9dfb06621 100644 --- a/x-pack/plugins/apm/public/components/app/onboarding/serverless_instructions.ts +++ b/x-pack/plugins/apm/public/components/app/onboarding/serverless_instructions.ts @@ -48,7 +48,7 @@ export function serverlessInstructions( const displayApiKeyErrorCallout = error && Boolean(errorMessage); const commonOptions: AgentInstructions = { baseUrl, - apmServerUrl: config.managedServiceUrl, + apmServerUrl: `${config.managedServiceUrl}:443`, checkAgentStatus, agentStatus, agentStatusLoading, diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/opentelemetry_instructions.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/opentelemetry_instructions.tsx index 4d1332df16877..2ea2a993816b7 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/opentelemetry_instructions.tsx +++ b/x-pack/plugins/apm/public/tutorial/config_agent/opentelemetry_instructions.tsx @@ -13,6 +13,8 @@ import { EuiSpacer, EuiText, EuiBasicTableColumn, + EuiButtonIcon, + copyToClipboard, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { ValuesType } from 'utility-types'; @@ -75,9 +77,24 @@ export function OpenTelemetryInstructions({ } ), render: (_, { value }) => ( - <EuiText size="s" color="accent"> - {value} - </EuiText> + <> + <EuiText size="s" color="accent"> + {value} + </EuiText> + {value && ( + <EuiButtonIcon + aria-label={i18n.translate( + 'xpack.apm.tutorial.config_otel.column.value.copyIconText', + { + defaultMessage: 'Copy to clipboard', + } + )} + color="text" + iconType="copy" + onClick={() => copyToClipboard(value)} + /> + )} + </> ), }, { diff --git a/x-pack/plugins/apm/scripts/diagnostics_bundle/cli.ts b/x-pack/plugins/apm/scripts/diagnostics_bundle/cli.ts index 30fb927bf5162..6d46fe8fd230a 100644 --- a/x-pack/plugins/apm/scripts/diagnostics_bundle/cli.ts +++ b/x-pack/plugins/apm/scripts/diagnostics_bundle/cli.ts @@ -8,30 +8,34 @@ /* eslint-disable no-console */ import datemath from '@elastic/datemath'; +import { errors } from '@elastic/elasticsearch'; +import { AxiosError } from 'axios'; import yargs from 'yargs'; import { initDiagnosticsBundle } from './diagnostics_bundle'; const { argv } = yargs(process.argv.slice(2)) .option('esHost', { - demandOption: true, type: 'string', description: 'Elasticsearch host name', }) .option('kbHost', { - demandOption: true, type: 'string', description: 'Kibana host name', }) .option('username', { - demandOption: true, type: 'string', description: 'Kibana host name', }) .option('password', { - demandOption: true, type: 'string', description: 'Kibana host name', }) + .option('cloudId', { + type: 'string', + }) + .option('apiKey', { + type: 'string', + }) .option('rangeFrom', { type: 'string', description: 'Time-range start', @@ -48,10 +52,20 @@ const { argv } = yargs(process.argv.slice(2)) }) .help(); -const { esHost, kbHost, password, username, kuery } = argv; +const { esHost, kbHost, password, username, kuery, apiKey, cloudId } = argv; const rangeFrom = argv.rangeFrom as unknown as number; const rangeTo = argv.rangeTo as unknown as number; +if ((!esHost || !kbHost) && !cloudId) { + console.error('Either esHost and kbHost or cloudId must be provided'); + process.exit(1); +} + +if ((!username || !password) && !apiKey) { + console.error('Either username and password or apiKey must be provided'); + process.exit(1); +} + if (rangeFrom) { console.log(`rangeFrom = ${new Date(rangeFrom).toISOString()}`); } @@ -64,6 +78,8 @@ initDiagnosticsBundle({ esHost, kbHost, password, + apiKey, + cloudId, username, start: rangeFrom, end: rangeTo, @@ -73,7 +89,20 @@ initDiagnosticsBundle({ console.log(res); }) .catch((err) => { - console.log(err); + process.exitCode = 1; + if (err instanceof AxiosError && err.response?.data) { + console.error(err.response.data); + return; + } + + // @ts-expect-error + if (err instanceof errors.ResponseError && err.meta.body.error.reason) { + // @ts-expect-error + console.error(err.meta.body.error.reason); + return; + } + + console.error(err); }); function convertDate(dateString: string): number { diff --git a/x-pack/plugins/apm/scripts/diagnostics_bundle/diagnostics_bundle.ts b/x-pack/plugins/apm/scripts/diagnostics_bundle/diagnostics_bundle.ts index af1de9a98988e..92fe9f08e260b 100644 --- a/x-pack/plugins/apm/scripts/diagnostics_bundle/diagnostics_bundle.ts +++ b/x-pack/plugins/apm/scripts/diagnostics_bundle/diagnostics_bundle.ts @@ -19,27 +19,43 @@ type DiagnosticsBundle = APIReturnType<'GET /internal/apm/diagnostics'>; export async function initDiagnosticsBundle({ esHost, kbHost, + cloudId, username, password, + apiKey, start, end, kuery, }: { - esHost: string; - kbHost: string; - username: string; - password: string; + esHost?: string; + kbHost?: string; + cloudId?: string; start: number | undefined; end: number | undefined; kuery: string | undefined; + username?: string; + password?: string; + apiKey?: string; }) { - const esClient = new Client({ node: esHost, auth: { username, password } }); + const auth = username && password ? { username, password } : undefined; + const apiKeyHeader = apiKey ? { Authorization: `ApiKey ${apiKey}` } : {}; + const { kibanaHost } = parseCloudId(cloudId); + + const esClient = new Client({ + ...(esHost ? { node: esHost } : {}), + ...(cloudId ? { cloud: { id: cloudId } } : {}), + auth, + headers: { ...apiKeyHeader }, + }); const kibanaClient = axios.create({ - baseURL: kbHost, - auth: { username, password }, + baseURL: kbHost ?? kibanaHost, + auth, + // @ts-expect-error + headers: { 'kbn-xsrf': 'true', ...apiKeyHeader }, }); const apmIndices = await getApmIndices(kibanaClient); + const bundle = await getDiagnosticsBundle({ esClient, apmIndices, @@ -99,3 +115,19 @@ async function getKibanaVersion(kibanaClient: AxiosInstance) { const res = await kibanaClient.get('/api/status'); return res.data.version.number; } + +function parseCloudId(cloudId?: string) { + if (!cloudId) { + return {}; + } + + const [instanceAlias, encodedString] = cloudId.split(':'); + const decodedString = Buffer.from(encodedString, 'base64').toString('utf8'); + const [hostname, esId, kbId] = decodedString.split('$'); + + return { + kibanaHost: `https://${kbId}.${hostname}`, + esHost: `https://${esId}.${hostname}`, + instanceAlias, + }; +} diff --git a/x-pack/plugins/cases/public/components/case_view/components/edit_category.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/edit_category.test.tsx index e41067dddc1ca..33c893d6f6b46 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/edit_category.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/edit_category.test.tsx @@ -134,6 +134,26 @@ describe('EditCategory ', () => { await waitFor(() => expect(onSubmit).toBeCalledWith('new')); }); + it('should trim category', async () => { + appMockRender.render(<EditCategory {...defaultProps} />); + + userEvent.click(screen.getByTestId('category-edit-button')); + + await waitFor(() => { + expect(screen.getByTestId('categories-list')).toBeInTheDocument(); + }); + + userEvent.type(screen.getByRole('combobox'), 'category-with-space {enter}'); + + await waitFor(() => { + expect(screen.getByTestId('edit-category-submit')).not.toBeDisabled(); + }); + + userEvent.click(screen.getByTestId('edit-category-submit')); + + await waitFor(() => expect(onSubmit).toBeCalledWith('category-with-space')); + }); + it('should not save category on cancel click', async () => { appMockRender.render(<EditCategory {...defaultProps} />); diff --git a/x-pack/plugins/cases/public/components/case_view/components/edit_category.tsx b/x-pack/plugins/cases/public/components/case_view/components/edit_category.tsx index df3c3e538e71a..f121c16f3efe8 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/edit_category.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/edit_category.tsx @@ -95,9 +95,7 @@ export const EditCategory = React.memo(({ isLoading, onSubmit, category }: EditC const { isValid, data } = await formState.submit(); if (isValid) { - const newCategory = data.category != null ? data.category : null; - - onSubmit(newCategory); + onSubmit(data.category?.trim() ?? null); } setIsEditCategory(false); diff --git a/x-pack/plugins/cases/public/components/case_view/components/edit_tags.test.tsx b/x-pack/plugins/cases/public/components/case_view/components/edit_tags.test.tsx index 28198f7f9c28c..8b87e80884ad3 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/edit_tags.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/edit_tags.test.tsx @@ -82,6 +82,22 @@ describe('EditTags ', () => { await waitFor(() => expect(onSubmit).toBeCalledWith(['dude'])); }); + it('trims the tags on submit', async () => { + appMockRender.render(<EditTags {...defaultProps} />); + + userEvent.click(screen.getByTestId('tag-list-edit-button')); + + await waitFor(() => { + expect(screen.getByTestId('edit-tags')).toBeInTheDocument(); + }); + + userEvent.type(screen.getByRole('combobox'), 'dude {enter}'); + + userEvent.click(screen.getByTestId('edit-tags-submit')); + + await waitFor(() => expect(onSubmit).toBeCalledWith(['dude'])); + }); + it('cancels on cancel', async () => { appMockRender.render(<EditTags {...defaultProps} />); diff --git a/x-pack/plugins/cases/public/components/case_view/components/edit_tags.tsx b/x-pack/plugins/cases/public/components/case_view/components/edit_tags.tsx index 66db38bb500c5..07bf8ac11c39f 100644 --- a/x-pack/plugins/cases/public/components/case_view/components/edit_tags.tsx +++ b/x-pack/plugins/cases/public/components/case_view/components/edit_tags.tsx @@ -79,7 +79,9 @@ export const EditTags = React.memo(({ isLoading, onSubmit, tags }: EditTagsProps const onSubmitTags = useCallback(async () => { const { isValid, data: newData } = await submit(); if (isValid && newData.tags) { - onSubmit(newData.tags); + const trimmedTags = newData.tags.map((tag: string) => tag.trim()); + + onSubmit(trimmedTags); form.reset({ defaultValue: newData }); setIsEditTags(false); } diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index 758d9d950ce9f..3703ba05fe486 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -306,34 +306,6 @@ describe('Create case', () => { }); }); - it('does not submits the title when the length is longer than 160 characters', async () => { - const longTitle = 'a'.repeat(161); - - appMockRender.render( - <FormContext onSuccess={onFormSubmitSuccess}> - <CreateCaseFormFields {...defaultCreateCaseForm} /> - <SubmitCaseButton /> - </FormContext> - ); - - await waitForFormToRender(screen); - - const titleInput = within(screen.getByTestId('caseTitle')).getByTestId('input'); - userEvent.paste(titleInput, longTitle); - - userEvent.click(screen.getByTestId('create-case-submit')); - - await waitFor(() => { - expect( - screen.getByText( - 'The length of the name is too long. The maximum length is 160 characters.' - ) - ).toBeInTheDocument(); - }); - - expect(postCase).not.toHaveBeenCalled(); - }); - it('should toggle sync settings', async () => { useGetConnectorsMock.mockReturnValue({ ...sampleConnectorData, diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx index f8c1a50392889..4df39cb2d52ac 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -71,6 +71,24 @@ export const FormContext: React.FC<Props> = ({ const { startTransaction } = useCreateCaseWithAttachmentsTransaction(); const availableOwners = useAvailableCasesOwners(); + const trimUserFormData = (userFormData: CaseUI) => { + let formData = { + ...userFormData, + title: userFormData.title.trim(), + description: userFormData.description.trim(), + }; + + if (userFormData.category) { + formData = { ...formData, category: userFormData.category.trim() }; + } + + if (userFormData.tags) { + formData = { ...formData, tags: userFormData.tags.map((tag: string) => tag.trim()) }; + } + + return formData; + }; + const submitCase = useCallback( async ( { @@ -92,9 +110,11 @@ export const FormContext: React.FC<Props> = ({ ? normalizeActionConnector(caseConnector, fields) : getNoneConnector(); + const trimmedData = trimUserFormData(userFormData); + const theCase = await postCase({ request: { - ...userFormData, + ...trimmedData, connector: connectorToUpdate, settings: { syncAlerts }, owner: selectedOwner ?? defaultOwner, diff --git a/x-pack/plugins/cases/public/components/description/index.test.tsx b/x-pack/plugins/cases/public/components/description/index.test.tsx index 5515753736274..e077c0896d67d 100644 --- a/x-pack/plugins/cases/public/components/description/index.test.tsx +++ b/x-pack/plugins/cases/public/components/description/index.test.tsx @@ -27,7 +27,13 @@ const defaultProps = { isLoadingDescription: false, }; -describe('Description', () => { +// FLAKY: https://github.com/elastic/kibana/issues/164049 +// FLAKY: https://github.com/elastic/kibana/issues/164048 +// FLAKY: https://github.com/elastic/kibana/issues/164047 +// FLAKY: https://github.com/elastic/kibana/issues/164046 +// FLAKY: https://github.com/elastic/kibana/issues/164045 +// FLAKY: https://github.com/elastic/kibana/issues/164044 +describe.skip('Description', () => { const onUpdateField = jest.fn(); let appMockRender: AppMockRenderer; @@ -91,6 +97,27 @@ describe('Description', () => { }); }); + it('trims the description correctly when saved', async () => { + const descriptionWithSpaces = 'New updated description '; + const res = appMockRender.render( + <Description {...defaultProps} onUpdateField={onUpdateField} /> + ); + + userEvent.click(res.getByTestId('description-edit-icon')); + + userEvent.clear(screen.getByTestId('euiMarkdownEditorTextArea')); + userEvent.type(screen.getByTestId('euiMarkdownEditorTextArea'), descriptionWithSpaces); + + userEvent.click(screen.getByTestId('editable-save-markdown')); + + await waitFor(() => { + expect(onUpdateField).toHaveBeenCalledWith({ + key: 'description', + value: descriptionWithSpaces.trim(), + }); + }); + }); + it('keeps the old description correctly when canceled', async () => { const editedDescription = 'New updated description'; const res = appMockRender.render( @@ -177,7 +204,8 @@ describe('Description', () => { expect(screen.queryByTestId('description-edit-icon')).not.toBeInTheDocument(); }); - describe('draft message', () => { + // FLAKY: https://github.com/elastic/kibana/issues/164050 + describe.skip('draft message', () => { const draftStorageKey = `cases.testAppId.basic-case-id.description.markdownEditor`; beforeEach(() => { diff --git a/x-pack/plugins/cases/public/components/description/index.tsx b/x-pack/plugins/cases/public/components/description/index.tsx index 3574f21f87ce3..a3e074f6f5867 100644 --- a/x-pack/plugins/cases/public/components/description/index.tsx +++ b/x-pack/plugins/cases/public/components/description/index.tsx @@ -113,7 +113,7 @@ export const Description = ({ const handleOnSave = useCallback( (content: string) => { - onUpdateField({ key: DESCRIPTION_ID, value: content }); + onUpdateField({ key: DESCRIPTION_ID, value: content.trim() }); setIsEditable(false); }, [onUpdateField, setIsEditable] diff --git a/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx b/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx index f7e4df4d6a2e2..9f75a94274aaf 100644 --- a/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx +++ b/x-pack/plugins/cases/public/components/header_page/editable_title.test.tsx @@ -185,6 +185,33 @@ describe('EditableTitle', () => { ).toBe(true); }); + it('trims the title before submit', () => { + const newTitle = 'new test title with spaces '; + + const wrapper = mount( + <TestProviders> + <EditableTitle {...defaultProps} /> + </TestProviders> + ); + + wrapper.find('button[data-test-subj="editable-title-header-value"]').simulate('click'); + wrapper.update(); + + wrapper + .find('input[data-test-subj="editable-title-input-field"]') + .last() + .simulate('change', { target: { value: newTitle } }); + + wrapper.find('button[data-test-subj="editable-title-submit-btn"]').simulate('click'); + wrapper.update(); + + expect(submitTitle).toHaveBeenCalled(); + expect(submitTitle.mock.calls[0][0]).toEqual(newTitle.trim()); + expect( + wrapper.find('button[data-test-subj="editable-title-header-value"]').first().exists() + ).toBe(true); + }); + it('does not submit the title when the length is longer than 160 characters', () => { const longTitle = 'a'.repeat(161); diff --git a/x-pack/plugins/cases/server/client/cases/create.test.ts b/x-pack/plugins/cases/server/client/cases/create.test.ts index 61672e70d8e3d..b53d12980c78a 100644 --- a/x-pack/plugins/cases/server/client/cases/create.test.ts +++ b/x-pack/plugins/cases/server/client/cases/create.test.ts @@ -16,7 +16,7 @@ import { SECURITY_SOLUTION_OWNER } from '../../../common'; import { mockCases } from '../../mocks'; import { createCasesClientMockArgs } from '../mocks'; import { create } from './create'; -import { CaseSeverity, ConnectorTypes } from '../../../common/types/domain'; +import { CaseSeverity, CaseStatuses, ConnectorTypes } from '../../../common/types/domain'; describe('create', () => { const theCase = { @@ -151,6 +151,31 @@ describe('create', () => { 'Failed to create case: Error: The title field cannot be an empty string.' ); }); + + it('should trim title', async () => { + await create({ ...theCase, title: 'title with spaces ' }, clientArgs); + + expect(clientArgs.services.caseService.postNewCase).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: { + ...theCase, + closed_by: null, + closed_at: null, + title: 'title with spaces', + created_at: expect.any(String), + created_by: expect.any(Object), + updated_at: null, + updated_by: null, + external_service: null, + duration: null, + status: CaseStatuses.open, + category: null, + }, + id: expect.any(String), + refresh: false, + }) + ); + }); }); describe('description', () => { @@ -188,6 +213,34 @@ describe('create', () => { 'Failed to create case: Error: The description field cannot be an empty string.' ); }); + + it('should trim description', async () => { + await create( + { ...theCase, description: 'this is a description with spaces!! ' }, + clientArgs + ); + + expect(clientArgs.services.caseService.postNewCase).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: { + ...theCase, + closed_by: null, + closed_at: null, + description: 'this is a description with spaces!!', + created_at: expect.any(String), + created_by: expect.any(Object), + updated_at: null, + updated_by: null, + external_service: null, + duration: null, + status: CaseStatuses.open, + category: null, + }, + id: expect.any(String), + refresh: false, + }) + ); + }); }); describe('tags', () => { @@ -235,6 +288,31 @@ describe('create', () => { `Failed to create case: Error: The length of the tag is too long. The maximum length is ${MAX_LENGTH_PER_TAG}.` ); }); + + it('should trim tags', async () => { + await create({ ...theCase, tags: ['pepsi ', 'coke'] }, clientArgs); + + expect(clientArgs.services.caseService.postNewCase).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: { + ...theCase, + closed_by: null, + closed_at: null, + tags: ['pepsi', 'coke'], + created_at: expect.any(String), + created_by: expect.any(Object), + updated_at: null, + updated_by: null, + external_service: null, + duration: null, + status: CaseStatuses.open, + category: null, + }, + id: expect.any(String), + refresh: false, + }) + ); + }); }); describe('Category', () => { @@ -269,5 +347,29 @@ describe('create', () => { 'Failed to create case: Error: The category field cannot be an empty string.,Invalid value " " supplied to "category"' ); }); + + it('should trim category', async () => { + await create({ ...theCase, category: 'reporting ' }, clientArgs); + + expect(clientArgs.services.caseService.postNewCase).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: { + ...theCase, + closed_by: null, + closed_at: null, + category: 'reporting', + created_at: expect.any(String), + created_by: expect.any(Object), + updated_at: null, + updated_by: null, + external_service: null, + duration: null, + status: CaseStatuses.open, + }, + id: expect.any(String), + refresh: false, + }) + ); + }); }); }); diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 30524c1db6595..f1ea52dcb45c9 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -61,10 +61,22 @@ export const create = async (data: CasePostRequest, clientArgs: CasesClientArgs) licensingService.notifyUsage(LICENSING_CASE_ASSIGNMENT_FEATURE); } + /** + * Trim title, category, description and tags before saving to ES + */ + + const trimmedQuery = { + ...query, + title: query.title.trim(), + description: query.description.trim(), + category: query.category?.trim() ?? null, + tags: query.tags?.map((tag) => tag.trim()) ?? [], + }; + const newCase = await caseService.postNewCase({ attributes: transformNewCase({ user, - newCase: query, + newCase: trimmedQuery, }), id: savedObjectID, refresh: false, diff --git a/x-pack/plugins/cases/server/client/cases/update.test.ts b/x-pack/plugins/cases/server/client/cases/update.test.ts index 17b51658b1233..f78e198ddeb52 100644 --- a/x-pack/plugins/cases/server/client/cases/update.test.ts +++ b/x-pack/plugins/cases/server/client/cases/update.test.ts @@ -394,6 +394,41 @@ describe('update', () => { 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The category field cannot be an empty string.,Invalid value " " supplied to "cases,category"' ); }); + + it('should trim category', async () => { + await update( + { + cases: [ + { + id: mockCases[0].id, + version: mockCases[0].version ?? '', + category: 'security ', + }, + ], + }, + clientArgs + ); + + expect(clientArgs.services.caseService.patchCases).toHaveBeenCalledWith( + expect.objectContaining({ + cases: [ + { + caseId: mockCases[0].id, + version: mockCases[0].version, + originalCase: { + ...mockCases[0], + }, + updatedAttributes: { + category: 'security', + updated_at: expect.any(String), + updated_by: expect.any(Object), + }, + }, + ], + refresh: false, + }) + ); + }); }); describe('Title', () => { @@ -488,6 +523,41 @@ describe('update', () => { 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The title field cannot be an empty string.' ); }); + + it('should trim title', async () => { + await update( + { + cases: [ + { + id: mockCases[0].id, + version: mockCases[0].version ?? '', + title: 'title with spaces ', + }, + ], + }, + clientArgs + ); + + expect(clientArgs.services.caseService.patchCases).toHaveBeenCalledWith( + expect.objectContaining({ + cases: [ + { + caseId: mockCases[0].id, + version: mockCases[0].version, + originalCase: { + ...mockCases[0], + }, + updatedAttributes: { + title: 'title with spaces', + updated_at: expect.any(String), + updated_by: expect.any(Object), + }, + }, + ], + refresh: false, + }) + ); + }); }); describe('Description', () => { @@ -585,6 +655,41 @@ describe('update', () => { 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The description field cannot be an empty string.' ); }); + + it('should trim description', async () => { + await update( + { + cases: [ + { + id: mockCases[0].id, + version: mockCases[0].version ?? '', + description: 'This is a description with spaces!! ', + }, + ], + }, + clientArgs + ); + + expect(clientArgs.services.caseService.patchCases).toHaveBeenCalledWith( + expect.objectContaining({ + cases: [ + { + caseId: mockCases[0].id, + version: mockCases[0].version, + originalCase: { + ...mockCases[0], + }, + updatedAttributes: { + description: 'This is a description with spaces!!', + updated_at: expect.any(String), + updated_by: expect.any(Object), + }, + }, + ], + refresh: false, + }) + ); + }); }); describe('Tags', () => { @@ -724,6 +829,41 @@ describe('update', () => { 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The tag field cannot be an empty string.' ); }); + + it('should trim tags', async () => { + await update( + { + cases: [ + { + id: mockCases[0].id, + version: mockCases[0].version ?? '', + tags: ['coke ', 'pepsi'], + }, + ], + }, + clientArgs + ); + + expect(clientArgs.services.caseService.patchCases).toHaveBeenCalledWith( + expect.objectContaining({ + cases: [ + { + caseId: mockCases[0].id, + version: mockCases[0].version, + originalCase: { + ...mockCases[0], + }, + updatedAttributes: { + tags: ['coke', 'pepsi'], + updated_at: expect.any(String), + updated_by: expect.any(Object), + }, + }, + ], + refresh: false, + }) + ); + }); }); describe('Validation', () => { diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 121fd2a8e3aa5..64c0f170517fa 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -462,6 +462,36 @@ export const update = async ( } }; +const trimCaseAttributes = ( + updateCaseAttributes: Omit<CasePatchRequest, 'id' | 'version' | 'owner' | 'assignees'> +) => { + let trimmedAttributes = { ...updateCaseAttributes }; + + if (updateCaseAttributes.title) { + trimmedAttributes = { ...trimmedAttributes, title: updateCaseAttributes.title.trim() }; + } + + if (updateCaseAttributes.description) { + trimmedAttributes = { + ...trimmedAttributes, + description: updateCaseAttributes.description.trim(), + }; + } + + if (updateCaseAttributes.category) { + trimmedAttributes = { ...trimmedAttributes, category: updateCaseAttributes.category.trim() }; + } + + if (updateCaseAttributes.tags) { + trimmedAttributes = { + ...trimmedAttributes, + tags: updateCaseAttributes.tags.map((tag: string) => tag.trim()), + }; + } + + return trimmedAttributes; +}; + const createPatchCasesPayload = ({ casesToUpdate, user, @@ -478,19 +508,21 @@ const createPatchCasesPayload = ({ const dedupedAssignees = dedupAssignees(assignees); + const trimmedCaseAttributes = trimCaseAttributes(updateCaseAttributes); + return { caseId, originalCase, updatedAttributes: { - ...updateCaseAttributes, + ...trimmedCaseAttributes, ...(dedupedAssignees && { assignees: dedupedAssignees }), ...getClosedInfoForUpdate({ user, closedDate: updatedDt, - status: updateCaseAttributes.status, + status: trimmedCaseAttributes.status, }), ...getDurationForUpdate({ - status: updateCaseAttributes.status, + status: trimmedCaseAttributes.status, closedAt: updatedDt, createdAt: originalCase.attributes.created_at, }), diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts index 7b9db8cfe5dbe..5ba3648638e61 100644 --- a/x-pack/plugins/cloud_security_posture/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/common/constants.ts @@ -21,6 +21,11 @@ export const BENCHMARKS_API_CURRENT_VERSION = '1'; export const FIND_CSP_RULE_TEMPLATE_ROUTE_PATH = '/internal/cloud_security_posture/rules/_find'; export const FIND_CSP_RULE_TEMPLATE_API_CURRENT_VERSION = '1'; +export const DETECTION_RULE_ALERTS_STATUS_API_CURRENT_VERSION = '1'; + +export const GET_DETECTION_RULE_ALERTS_STATUS_PATH = + '/internal/cloud_security_posture/detection_engine_rules/alerts/_status'; + export const CLOUD_SECURITY_POSTURE_PACKAGE_NAME = 'cloud_security_posture'; // TODO: REMOVE CSP_LATEST_FINDINGS_DATA_VIEW and replace it with LATEST_FINDINGS_INDEX_PATTERN export const CSP_LATEST_FINDINGS_DATA_VIEW = 'logs-cloud_security_posture.findings_latest-*'; @@ -136,3 +141,5 @@ export const AWS_CREDENTIALS_TYPE_TO_FIELDS_MAP: AwsCredentialsTypeFieldMap = { export const SETUP_ACCESS_CLOUD_SHELL = 'google_cloud_shell'; export const SETUP_ACCESS_MANUAL = 'manual'; + +export const DETECTION_ENGINE_ALERTS_INDEX_DEFAULT = '.alerts-security.alerts-default'; diff --git a/x-pack/plugins/cloud_security_posture/common/utils/get_safe_vulnerabilities_query_filter.ts b/x-pack/plugins/cloud_security_posture/common/utils/get_safe_vulnerabilities_query_filter.ts index dace996d86906..bcbe41cdc96bc 100644 --- a/x-pack/plugins/cloud_security_posture/common/utils/get_safe_vulnerabilities_query_filter.ts +++ b/x-pack/plugins/cloud_security_posture/common/utils/get_safe_vulnerabilities_query_filter.ts @@ -15,7 +15,6 @@ export const getSafeVulnerabilitiesQueryFilter = (query?: QueryDslQueryContainer { exists: { field: 'vulnerability.score.base' } }, { exists: { field: 'vulnerability.score.version' } }, { exists: { field: 'resource.id' } }, - { exists: { field: 'resource.name' } }, ], }, }); diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/create_detection_rule.ts b/x-pack/plugins/cloud_security_posture/public/common/api/create_detection_rule.ts index 8a584be1bdbac..a73e75d706e72 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/api/create_detection_rule.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/api/create_detection_rule.ts @@ -5,46 +5,11 @@ * 2.0. */ import { HttpSetup } from '@kbn/core/public'; +import { RuleCreateProps, RuleResponse } from '../types'; const DETECTION_ENGINE_URL = '/api/detection_engine' as const; const DETECTION_ENGINE_RULES_URL = `${DETECTION_ENGINE_URL}/rules` as const; -interface RuleCreateProps { - type: string; - language: string; - license: string; - author: string[]; - filters: any[]; - false_positives: any[]; - risk_score: number; - risk_score_mapping: any[]; - severity: string; - severity_mapping: any[]; - threat: any[]; - interval: string; - from: string; - to: string; - timestamp_override: string; - timestamp_override_fallback_disabled: boolean; - actions: any[]; - enabled: boolean; - alert_suppression: { - group_by: string[]; - missing_fields_strategy: string; - }; - index: string[]; - query: string; - references: string[]; - name: string; - description: string; - tags: string[]; - max_signals: number; -} - -export interface RuleResponse extends RuleCreateProps { - id: string; -} - export const createDetectionRule = async ({ http, rule, diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/index.ts b/x-pack/plugins/cloud_security_posture/public/common/api/index.ts index fb3caf4fa9814..29320516d2842 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/api/index.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/api/index.ts @@ -6,3 +6,4 @@ */ export * from './use_stats_api'; +export * from './use_fetch_detection_rules_by_tags'; diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/use_fetch_detection_rules_alerts_status.ts b/x-pack/plugins/cloud_security_posture/public/common/api/use_fetch_detection_rules_alerts_status.ts new file mode 100644 index 0000000000000..efea6629b7743 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/api/use_fetch_detection_rules_alerts_status.ts @@ -0,0 +1,36 @@ +/* + * 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 { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useQuery } from '@tanstack/react-query'; +import { + DETECTION_RULE_ALERTS_STATUS_API_CURRENT_VERSION, + GET_DETECTION_RULE_ALERTS_STATUS_PATH, +} from '../../../common/constants'; +import { DETECTION_ENGINE_ALERTS_KEY } from '../constants'; + +interface AlertStatus { + acknowledged: number; + closed: number; + open: number; + total: number; +} + +export const useFetchDetectionRulesAlertsStatus = (tags: string[]) => { + const { http } = useKibana().services; + + if (!http) { + throw new Error('Kibana http service is not available'); + } + + return useQuery<AlertStatus, Error>([DETECTION_ENGINE_ALERTS_KEY, tags], () => + http.get<AlertStatus>(GET_DETECTION_RULE_ALERTS_STATUS_PATH, { + version: DETECTION_RULE_ALERTS_STATUS_API_CURRENT_VERSION, + query: { tags }, + }) + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/use_fetch_detection_rules_by_tags.ts b/x-pack/plugins/cloud_security_posture/public/common/api/use_fetch_detection_rules_by_tags.ts new file mode 100644 index 0000000000000..953b31b1b5428 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/common/api/use_fetch_detection_rules_by_tags.ts @@ -0,0 +1,53 @@ +/* + * 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 { CoreStart } from '@kbn/core/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { useQuery } from '@tanstack/react-query'; +import { RuleResponse } from '../types'; +import { DETECTION_ENGINE_RULES_KEY } from '../constants'; + +/* + * 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 interface FetchRulesResponse { + page: number; + perPage: number; + total: number; + data: RuleResponse[]; +} + +export const TAGS_FIELD = 'alert.attributes.tags'; + +const DETECTION_ENGINE_URL = '/api/detection_engine' as const; +const DETECTION_ENGINE_RULES_URL = `${DETECTION_ENGINE_URL}/rules` as const; +export const DETECTION_ENGINE_RULES_URL_FIND = `${DETECTION_ENGINE_RULES_URL}/_find` as const; + +export function convertRuleTagsToKQL(tags: string[]): string { + return `${TAGS_FIELD}:(${tags.map((tag) => `"${tag}"`).join(' AND ')})`; +} + +export const useFetchDetectionRulesByTags = (tags: string[]) => { + const { http } = useKibana<CoreStart>().services; + + const query = { + page: 1, + per_page: 1, + filter: convertRuleTagsToKQL(tags), + }; + + return useQuery([DETECTION_ENGINE_RULES_KEY, tags], () => + http.fetch<FetchRulesResponse>(DETECTION_ENGINE_RULES_URL_FIND, { + method: 'GET', + query, + }) + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/common/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/constants.ts index 57cefa02344ea..8f51456c009e4 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/constants.ts @@ -217,3 +217,6 @@ export const FINDINGS_DOCS_URL = 'https://ela.st/findings'; export const MIN_VERSION_GCP_CIS = '1.5.0'; export const NO_FINDINGS_STATUS_REFRESH_INTERVAL_MS = 10000; + +export const DETECTION_ENGINE_RULES_KEY = 'detection_engine_rules'; +export const DETECTION_ENGINE_ALERTS_KEY = 'detection_engine_alerts'; diff --git a/x-pack/plugins/cloud_security_posture/public/common/types.ts b/x-pack/plugins/cloud_security_posture/public/common/types.ts index 0933d8cbda260..d9bdc58cd3bb3 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/types.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/types.ts @@ -36,3 +36,46 @@ export interface CspFindingsQueryData { } export type Sort<T> = NonNullable<Criteria<T>['sort']>; + +interface RuleSeverityMapping { + field: string; + value: string; + operator: 'equals'; + severity: string; +} + +export interface RuleCreateProps { + type: string; + language: string; + license: string; + author: string[]; + filters: unknown[]; + false_positives: unknown[]; + risk_score: number; + risk_score_mapping: unknown[]; + severity: string; + severity_mapping: RuleSeverityMapping[]; + threat: unknown[]; + interval: string; + from: string; + to: string; + timestamp_override: string; + timestamp_override_fallback_disabled: boolean; + actions: unknown[]; + enabled: boolean; + alert_suppression: { + group_by: string[]; + missing_fields_strategy: string; + }; + index: string[]; + query: string; + references: string[]; + name: string; + description: string; + tags: string[]; + max_signals: number; +} + +export interface RuleResponse extends RuleCreateProps { + id: string; +} diff --git a/x-pack/plugins/cloud_security_posture/public/common/utils/get_vulnerability_reference_url.ts b/x-pack/plugins/cloud_security_posture/public/common/utils/get_vulnerability_reference_url.ts index c4d1e00450873..b8c6adf2063a7 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/utils/get_vulnerability_reference_url.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/utils/get_vulnerability_reference_url.ts @@ -5,17 +5,13 @@ * 2.0. */ -import type { CspVulnerabilityFinding } from '../../../common/schemas'; +import type { Vulnerability } from '../../../common/schemas'; -export const getVulnerabilityReferenceUrl = ( - finding: CspVulnerabilityFinding -): string | undefined => { +export const getVulnerabilityReferenceUrl = (vulnerability: Vulnerability): string | undefined => { const nvdDomain = 'https://nvd'; - const nvdWebsite = `${nvdDomain}.nist.gov/vuln/detail/${finding?.vulnerability?.id}`; + const nvdWebsite = `${nvdDomain}.nist.gov/vuln/detail/${vulnerability?.id}`; - const vulnerabilityReference = finding.vulnerability?.cvss?.nvd - ? nvdWebsite - : finding.vulnerability?.reference; + const vulnerabilityReference = vulnerability?.cvss?.nvd ? nvdWebsite : vulnerability?.reference; return vulnerabilityReference; }; diff --git a/x-pack/plugins/cloud_security_posture/public/components/detection_rule_counter.tsx b/x-pack/plugins/cloud_security_posture/public/components/detection_rule_counter.tsx new file mode 100644 index 0000000000000..0ee3cd24d36e1 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/detection_rule_counter.tsx @@ -0,0 +1,123 @@ +/* + * 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 React, { useCallback, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiLink, EuiLoadingSpinner, EuiSkeletonText, EuiText } from '@elastic/eui'; +import type { HttpSetup } from '@kbn/core/public'; +import { useHistory } from 'react-router-dom'; +import useSessionStorage from 'react-use/lib/useSessionStorage'; +import { useQueryClient } from '@tanstack/react-query'; +import { useFetchDetectionRulesAlertsStatus } from '../common/api/use_fetch_detection_rules_alerts_status'; +import { useFetchDetectionRulesByTags } from '../common/api/use_fetch_detection_rules_by_tags'; +import { RuleResponse } from '../common/types'; +import { useKibana } from '../common/hooks/use_kibana'; +import { showSuccessToast } from './take_action'; +import { DETECTION_ENGINE_ALERTS_KEY, DETECTION_ENGINE_RULES_KEY } from '../common/constants'; + +const RULES_PAGE_PATH = '/rules/management'; +const ALERTS_PAGE_PATH = '/alerts'; + +const RULES_TABLE_SESSION_STORAGE_KEY = 'securitySolution.rulesTable'; + +interface DetectionRuleCounterProps { + tags: string[]; + createRuleFn: (http: HttpSetup) => Promise<RuleResponse>; +} + +export const DetectionRuleCounter = ({ tags, createRuleFn }: DetectionRuleCounterProps) => { + const { data: rulesData, isLoading: ruleIsLoading } = useFetchDetectionRulesByTags(tags); + const { data: alertsData, isLoading: alertsIsLoading } = useFetchDetectionRulesAlertsStatus(tags); + + const [isCreateRuleLoading, setIsCreateRuleLoading] = useState(false); + + const queryClient = useQueryClient(); + const { http, notifications } = useKibana().services; + + const history = useHistory(); + + const [, setRulesTable] = useSessionStorage(RULES_TABLE_SESSION_STORAGE_KEY); + + const rulePageNavigation = useCallback(async () => { + await setRulesTable({ + tags, + }); + history.push({ + pathname: RULES_PAGE_PATH, + }); + }, [history, setRulesTable, tags]); + + const alertsPageNavigation = useCallback(() => { + history.push({ + pathname: ALERTS_PAGE_PATH, + }); + }, [history]); + + const createDetectionRuleOnClick = useCallback(async () => { + setIsCreateRuleLoading(true); + const ruleResponse = await createRuleFn(http); + setIsCreateRuleLoading(false); + showSuccessToast(notifications, http, ruleResponse); + // Triggering a refetch of rules and alerts to update the UI + queryClient.invalidateQueries([DETECTION_ENGINE_RULES_KEY]); + queryClient.invalidateQueries([DETECTION_ENGINE_ALERTS_KEY]); + }, [createRuleFn, http, notifications, queryClient]); + + return ( + <EuiSkeletonText lines={1} size="m" isLoading={ruleIsLoading || alertsIsLoading}> + {rulesData?.total === 0 ? ( + <> + <EuiText size="s"> + {isCreateRuleLoading ? ( + <> + <FormattedMessage + id="xpack.csp.findingsFlyout.alerts.creatingRule" + defaultMessage="Creating detection rule" + />{' '} + <EuiLoadingSpinner size="s" /> + </> + ) : ( + <> + <EuiLink onClick={createDetectionRuleOnClick}> + <FormattedMessage + id="xpack.csp.findingsFlyout.alerts.createRuleAction" + defaultMessage="Create a detection rule" + /> + </EuiLink>{' '} + <FormattedMessage + id="xpack.csp.findingsFlyout.alerts.createRuleDescription" + defaultMessage="to generate alerts." + /> + </> + )} + </EuiText> + </> + ) : ( + <> + <EuiLink onClick={alertsPageNavigation}> + <FormattedMessage + id="xpack.csp.findingsFlyout.alerts.alertCount" + defaultMessage="{alertCount, plural, one {# alert} other {# alerts}}" + values={{ alertCount: alertsData?.total || 0 }} + /> + </EuiLink>{' '} + <FormattedMessage + id="xpack.csp.findingsFlyout.alerts.detectedBy" + defaultMessage="detected by" + />{' '} + <EuiLink onClick={rulePageNavigation}> + <FormattedMessage + id="xpack.csp.findingsFlyout.alerts.ruleCount" + defaultMessage="{ruleCount, plural, one {# rule} other {# rules}}" + values={{ ruleCount: rulesData?.total || 0 }} + /> + </EuiLink> + </> + )} + </EuiSkeletonText> + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx index 1472a48faccfc..7fafe17113c84 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/aws_credentials_form/aws_credentials_form.tsx @@ -18,7 +18,7 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import type { NewPackagePolicy } from '@kbn/fleet-plugin/public'; -import { PackageInfo } from '@kbn/fleet-plugin/common'; +import { NewPackagePolicyInput, PackageInfo } from '@kbn/fleet-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; @@ -34,6 +34,7 @@ import { NewPackagePolicyPostureInput, } from '../utils'; import { SetupFormat, useAwsCredentialsForm } from './hooks'; +import { AWS_ORGANIZATION_ACCOUNT } from '../policy_template_form'; import { AwsCredentialsType } from '../../../../common/types'; interface AWSSetupInfoContentProps { @@ -106,8 +107,10 @@ interface Props { const CloudFormationSetup = ({ hasCloudFormationTemplate, + input, }: { hasCloudFormationTemplate: boolean; + input: NewPackagePolicyInput; }) => { if (!hasCloudFormationTemplate) { return ( @@ -119,6 +122,9 @@ const CloudFormationSetup = ({ </EuiCallOut> ); } + + const accountType = input.streams?.[0]?.vars?.['aws.account_type']?.value; + return ( <> <EuiText color="subdued" size="s"> @@ -127,12 +133,21 @@ const CloudFormationSetup = ({ list-style: auto; `} > - <li> - <FormattedMessage - id="xpack.csp.awsIntegration.cloudFormationSetupStep.login" - defaultMessage="Log in as an admin to the AWS Account you want to onboard" - /> - </li> + {accountType === AWS_ORGANIZATION_ACCOUNT ? ( + <li> + <FormattedMessage + id="xpack.csp.awsIntegration.cloudFormationSetupStep.organizationLogin" + defaultMessage="Log in as an admin in your organization's AWS management account" + /> + </li> + ) : ( + <li> + <FormattedMessage + id="xpack.csp.awsIntegration.cloudFormationSetupStep.login" + defaultMessage="Log in as an admin to the AWS Account you want to onboard" + /> + </li> + )} <li> <FormattedMessage id="xpack.csp.awsIntegration.cloudFormationSetupStep.save" @@ -224,7 +239,7 @@ export const AwsCredentialsForm = ({ /> <EuiSpacer size="l" /> {setupFormat === 'cloud_formation' && ( - <CloudFormationSetup hasCloudFormationTemplate={hasCloudFormationTemplate} /> + <CloudFormationSetup hasCloudFormationTemplate={hasCloudFormationTemplate} input={input} /> )} {setupFormat === 'manual' && ( <> diff --git a/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx b/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx index 57684d02fd157..f4035f1532b7c 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/take_action.tsx @@ -17,22 +17,66 @@ import { useGeneratedHtmlId, } from '@elastic/eui'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import type { HttpSetup } from '@kbn/core/public'; +import type { HttpSetup, NotificationsStart } from '@kbn/core/public'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useQueryClient } from '@tanstack/react-query'; +import type { RuleResponse } from '../common/types'; import { CREATE_RULE_ACTION_SUBJ, TAKE_ACTION_SUBJ } from './test_subjects'; import { useKibana } from '../common/hooks/use_kibana'; -import type { RuleResponse } from '../common/api/create_detection_rule'; +import { DETECTION_ENGINE_ALERTS_KEY, DETECTION_ENGINE_RULES_KEY } from '../common/constants'; const RULE_PAGE_PATH = '/app/security/rules/id/'; interface TakeActionProps { createRuleFn: (http: HttpSetup) => Promise<RuleResponse>; } + +export const showSuccessToast = ( + notifications: NotificationsStart, + http: HttpSetup, + ruleResponse: RuleResponse +) => { + return notifications.toasts.addSuccess({ + toastLifeTimeMs: 10000, + color: 'success', + iconType: '', + text: toMountPoint( + <div> + <EuiText size="m"> + <strong>{ruleResponse.name}</strong> + {` `} + <FormattedMessage + id="xpack.csp.flyout.ruleCreatedToastTitle" + defaultMessage="detection rule was created." + /> + </EuiText> + <EuiText size="s"> + <FormattedMessage + id="xpack.csp.flyout.ruleCreatedToast" + defaultMessage="Add rule actions to get notified when alerts are generated." + /> + </EuiText> + <EuiFlexGroup justifyContent="flexEnd" gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiButton size="s" href={http.basePath.prepend(RULE_PAGE_PATH + ruleResponse.id)}> + <FormattedMessage + id="xpack.csp.flyout.ruleCreatedToastViewRuleButton" + defaultMessage="View rule" + /> + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </div> + ), + }); +}; + /* * This component is used to create a detection rule from Flyout. * It accepts a createRuleFn parameter which is used to create a rule in a generic way. */ export const TakeAction = ({ createRuleFn }: TakeActionProps) => { + const queryClient = useQueryClient(); const [isPopoverOpen, setPopoverOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const closePopover = () => { @@ -45,42 +89,6 @@ export const TakeAction = ({ createRuleFn }: TakeActionProps) => { const { http, notifications } = useKibana().services; - const showSuccessToast = (ruleResponse: RuleResponse) => { - return notifications.toasts.addSuccess({ - toastLifeTimeMs: 10000, - color: 'success', - iconType: '', - text: toMountPoint( - <div> - <EuiText size="m"> - <strong>{ruleResponse.name}</strong> - {` `} - <FormattedMessage - id="xpack.csp.flyout.ruleCreatedToastTitle" - defaultMessage="detection rule was created." - /> - </EuiText> - <EuiText size="s"> - <FormattedMessage - id="xpack.csp.flyout.ruleCreatedToast" - defaultMessage="Add rule actions to get notified when alerts are generated." - /> - </EuiText> - <EuiFlexGroup justifyContent="flexEnd" gutterSize="s"> - <EuiFlexItem grow={false}> - <EuiButton size="s" href={http.basePath.prepend(RULE_PAGE_PATH + ruleResponse.id)}> - <FormattedMessage - id="xpack.csp.flyout.ruleCreatedToastViewRuleButton" - defaultMessage="View rule" - /> - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </div> - ), - }); - }; - const button = ( <EuiButton isLoading={isLoading} @@ -113,7 +121,10 @@ export const TakeAction = ({ createRuleFn }: TakeActionProps) => { setIsLoading(true); const ruleResponse = await createRuleFn(http); setIsLoading(false); - showSuccessToast(ruleResponse); + showSuccessToast(notifications, http, ruleResponse); + // Triggering a refetch of rules and alerts to update the UI + queryClient.invalidateQueries([DETECTION_ENGINE_RULES_KEY]); + queryClient.invalidateQueries([DETECTION_ENGINE_ALERTS_KEY]); }} data-test-subj={CREATE_RULE_ACTION_SUBJ} > diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_detection_rule_counter.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_detection_rule_counter.tsx new file mode 100644 index 0000000000000..5586f2a20126c --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/findings_detection_rule_counter.tsx @@ -0,0 +1,21 @@ +/* + * 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 { HttpSetup } from '@kbn/core/public'; +import React from 'react'; +import { CspFinding } from '../../../../common/schemas/csp_finding'; +import { DetectionRuleCounter } from '../../../components/detection_rule_counter'; +import { createDetectionRuleFromFinding } from '../utils/create_detection_rule_from_finding'; + +export const FindingsDetectionRuleCounter = ({ finding }: { finding: CspFinding }) => { + const createMisconfigurationRuleFn = async (http: HttpSetup) => + await createDetectionRuleFromFinding(http, finding); + + return ( + <DetectionRuleCounter tags={finding.rule.tags} createRuleFn={createMisconfigurationRuleFn} /> + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/overview_tab.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/overview_tab.tsx index 81f160d03d820..cb906b99ef21b 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/overview_tab.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/findings_flyout/overview_tab.tsx @@ -29,6 +29,7 @@ import { useLatestFindingsDataView } from '../../../common/api/use_latest_findin import { useKibana } from '../../../common/hooks/use_kibana'; import { CspFinding } from '../../../../common/schemas/csp_finding'; import { CisKubernetesIcons, CspFlyoutMarkdown, CodeBlock } from './findings_flyout'; +import { FindingsDetectionRuleCounter } from './findings_detection_rule_counter'; type Accordion = Pick<EuiAccordionProps, 'title' | 'id' | 'initialIsOpen'> & Pick<EuiDescriptionListProps, 'listItems'>; @@ -40,6 +41,12 @@ const getDetailsList = (data: CspFinding, discoverIndexLink: string | undefined) }), description: data.rule.name, }, + { + title: i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.alertsTitle', { + defaultMessage: 'Alerts', + }), + description: <FindingsDetectionRuleCounter finding={data} />, + }, { title: i18n.translate('xpack.csp.findings.findingsFlyout.overviewTab.ruleTagsTitle', { defaultMessage: 'Rule Tags', diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/generate_findings_tags.ts b/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/generate_findings_tags.ts new file mode 100644 index 0000000000000..66da177e1cea8 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/utils/generate_findings_tags.ts @@ -0,0 +1,30 @@ +/* + * 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 { CspFinding } from '../../../../common/schemas/csp_finding'; + +const CSP_RULE_TAG = 'Cloud Security'; +const CNVM_RULE_TAG_USE_CASE = 'Use Case: Configuration Audit'; +const CNVM_RULE_TAG_DATA_SOURCE_PREFIX = 'Data Source: '; + +const STATIC_RULE_TAGS = [CSP_RULE_TAG, CNVM_RULE_TAG_USE_CASE]; + +export const generateFindingsTags = (finding: CspFinding) => { + return [STATIC_RULE_TAGS] + .concat(finding.rule.tags) + .concat( + finding.rule.benchmark.posture_type + ? [ + `${CNVM_RULE_TAG_DATA_SOURCE_PREFIX}${finding.rule.benchmark.posture_type.toUpperCase()}`, + ] + : [] + ) + .concat( + finding.rule.benchmark.posture_type === 'cspm' ? ['Domain: Cloud'] : ['Domain: Container'] + ) + .flat(); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts index c8cd677041b16..35a6147f539b2 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/create_detection_rule_from_vulnerability.ts @@ -8,7 +8,7 @@ import { HttpSetup } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { getVulnerabilityReferenceUrl } from '../../../common/utils/get_vulnerability_reference_url'; -import type { CspVulnerabilityFinding } from '../../../../common/schemas'; +import type { Vulnerability } from '../../../../common/schemas'; import { LATEST_VULNERABILITIES_RETENTION_POLICY, VULNERABILITIES_INDEX_PATTERN, @@ -54,33 +54,33 @@ const STATIC_RULE_TAGS = [ CNVM_RULE_TAG_OS, ]; -const generateVulnerabilitiesTags = (finding: CspVulnerabilityFinding) => { - return [...STATIC_RULE_TAGS, finding.vulnerability.id]; +const generateVulnerabilitiesTags = (vulnerability: Vulnerability) => { + return [...STATIC_RULE_TAGS, vulnerability.id]; }; -const getVulnerabilityRuleName = (finding: CspVulnerabilityFinding) => { +const getVulnerabilityRuleName = (vulnerability: Vulnerability) => { return i18n.translate('xpack.csp.vulnerabilities.detectionRuleNamePrefix', { defaultMessage: 'Vulnerability: {vulnerabilityId}', values: { - vulnerabilityId: finding.vulnerability.id, + vulnerabilityId: vulnerability.id, }, }); }; -const generateVulnerabilitiesRuleQuery = (finding: CspVulnerabilityFinding) => { +const generateVulnerabilitiesRuleQuery = (vulnerability: Vulnerability) => { const currentTimestamp = new Date().toISOString(); - return `vulnerability.id: "${finding.vulnerability.id}" AND event.ingested >= "${currentTimestamp}"`; + return `vulnerability.id: "${vulnerability.id}" AND event.ingested >= "${currentTimestamp}"`; }; /* - * Creates a detection rule from a CspVulnerabilityFinding + * Creates a detection rule from a Vulnerability */ export const createDetectionRuleFromVulnerabilityFinding = async ( http: HttpSetup, - finding: CspVulnerabilityFinding + vulnerability: Vulnerability ) => { - const referenceUrl = getVulnerabilityReferenceUrl(finding); + const referenceUrl = getVulnerabilityReferenceUrl(vulnerability); return await createDetectionRule({ http, @@ -140,11 +140,11 @@ export const createDetectionRuleFromVulnerabilityFinding = async ( missing_fields_strategy: AlertSuppressionMissingFieldsStrategy.Suppress, }, index: [VULNERABILITIES_INDEX_PATTERN], - query: generateVulnerabilitiesRuleQuery(finding), + query: generateVulnerabilitiesRuleQuery(vulnerability), references: referenceUrl ? [referenceUrl] : [], - name: getVulnerabilityRuleName(finding), - description: finding.vulnerability.description, - tags: generateVulnerabilitiesTags(finding), + name: getVulnerabilityRuleName(vulnerability), + description: vulnerability.description, + tags: generateVulnerabilitiesTags(vulnerability), }, }); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/get_vulnerabilities_grid_cell_actions.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/get_vulnerabilities_grid_cell_actions.tsx index 17e1469fcd60e..b92adf84d70ab 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/get_vulnerabilities_grid_cell_actions.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/utils/get_vulnerabilities_grid_cell_actions.tsx @@ -40,7 +40,10 @@ export const getVulnerabilitiesGridCellActions = < if (columnId === columns.cvss) { return vulnerabilityRow.vulnerability?.score.base; } - if (columnId === columns.resource) { + if (columnId === columns.resourceId) { + return vulnerabilityRow.resource?.id; + } + if (columnId === columns.resourceName) { return vulnerabilityRow.resource?.name; } if (columnId === columns.severity) { @@ -52,15 +55,9 @@ export const getVulnerabilitiesGridCellActions = < if (columnId === columns.version) { return vulnerabilityRow.vulnerability?.package?.version; } - if (columnId === columns.fix_version) { + if (columnId === columns.fixedVersion) { return vulnerabilityRow.vulnerability?.package?.fixed_version; } - if (columnId === columns.resource_id) { - return vulnerabilityRow.resource?.id; - } - if (columnId === columns.resource_name) { - return vulnerabilityRow.resource?.name; - } if (columnId === columns.region) { return vulnerabilityRow.cloud?.region; } diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx index 070fd9ea16242..efb2b97cc6891 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities.tsx @@ -254,9 +254,12 @@ const VulnerabilitiesDataGrid = ({ /> ); } - if (columnId === vulnerabilitiesColumns.resource) { + if (columnId === vulnerabilitiesColumns.resourceName) { return <>{vulnerabilityRow.resource?.name}</>; } + if (columnId === vulnerabilitiesColumns.resourceId) { + return <>{vulnerabilityRow.resource?.id}</>; + } if (columnId === vulnerabilitiesColumns.severity) { if (!vulnerabilityRow.vulnerability.severity) { return null; @@ -270,7 +273,7 @@ const VulnerabilitiesDataGrid = ({ if (columnId === vulnerabilitiesColumns.version) { return <>{vulnerabilityRow.vulnerability?.package?.version}</>; } - if (columnId === vulnerabilitiesColumns.fix_version) { + if (columnId === vulnerabilitiesColumns.fixedVersion) { return <>{vulnerabilityRow.vulnerability?.package?.fixed_version}</>; } diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/resource_vulnerabilities/resource_vulnerabilities.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/resource_vulnerabilities/resource_vulnerabilities.tsx index e18e3b855b1cb..ee67c8e073b3e 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/resource_vulnerabilities/resource_vulnerabilities.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/resource_vulnerabilities/resource_vulnerabilities.tsx @@ -146,6 +146,7 @@ const ResourceVulnerabilitiesDataGrid = ({ if (!data?.page) { return []; } + return getVulnerabilitiesGridCellActions({ columnGridFn: getVulnerabilitiesColumnsGrid, columns: vulnerabilitiesColumns, @@ -154,7 +155,11 @@ const ResourceVulnerabilitiesDataGrid = ({ data: data.page, setUrlQuery, filters: urlQuery.filters, - }).filter((column) => column.id !== vulnerabilitiesColumns.resource); + }).filter( + (column) => + column.id !== vulnerabilitiesColumns.resourceName && + column.id !== vulnerabilitiesColumns.resourceId + ); }, [data?.page, dataView, pageSize, setUrlQuery, urlQuery.filters]); const flyoutVulnerabilityIndex = urlQuery?.vulnerabilityIndex; @@ -219,9 +224,6 @@ const ResourceVulnerabilitiesDataGrid = ({ /> ); } - if (columnId === vulnerabilitiesColumns.resource) { - return <>{vulnerabilityRow.resource?.name}</>; - } if (columnId === vulnerabilitiesColumns.severity) { if (!vulnerabilityRow.vulnerability.severity) { return null; @@ -235,7 +237,7 @@ const ResourceVulnerabilitiesDataGrid = ({ if (columnId === vulnerabilitiesColumns.version) { return <>{vulnerabilityRow.vulnerability?.package?.version}</>; } - if (columnId === vulnerabilitiesColumns.fix_version) { + if (columnId === vulnerabilitiesColumns.fixedVersion) { return <>{vulnerabilityRow.vulnerability?.package?.fixed_version}</>; } diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource.tsx index 8fd5eab3aab89..caa5cca3a3ac1 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource.tsx @@ -109,7 +109,7 @@ const VulnerabilitiesByResourceDataGrid = ({ if (isFetching) return null; if (!resourceVulnerabilityRow?.resource?.id) return null; - if (columnId === vulnerabilitiesByResourceColumns.resource_id) { + if (columnId === vulnerabilitiesByResourceColumns.resourceId) { return ( <Link to={generatePath(findingsNavigation.resource_vulnerabilities.path, { @@ -122,7 +122,7 @@ const VulnerabilitiesByResourceDataGrid = ({ </Link> ); } - if (columnId === vulnerabilitiesByResourceColumns.resource_name) { + if (columnId === vulnerabilitiesByResourceColumns.resourceName) { return <>{resourceVulnerabilityRow?.resource?.name}</>; } if (columnId === vulnerabilitiesByResourceColumns.region) { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource_table_columns.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource_table_columns.ts index 959d6db682959..42196f151bd07 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource_table_columns.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_by_resource/vulnerabilities_by_resource_table_columns.ts @@ -9,8 +9,8 @@ import { EuiDataGridColumn, EuiDataGridColumnCellAction } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; export const vulnerabilitiesByResourceColumns = { - resource_id: 'resource.id', - resource_name: 'resource.name', + resourceId: 'resource.id', + resourceName: 'resource.name', region: 'cloud.region', vulnerabilities_count: 'vulnerabilities_count', severity_map: 'severity_map', @@ -33,7 +33,7 @@ export const getVulnerabilitiesByResourceColumnsGrid = ( ): EuiDataGridColumn[] => [ { ...defaultColumnProps(), - id: vulnerabilitiesByResourceColumns.resource_id, + id: vulnerabilitiesByResourceColumns.resourceId, displayAsText: i18n.translate('xpack.csp.vulnerabilityByResourceTable.column.resourceId', { defaultMessage: 'Resource ID', }), @@ -41,7 +41,7 @@ export const getVulnerabilitiesByResourceColumnsGrid = ( }, { ...defaultColumnProps(), - id: vulnerabilitiesByResourceColumns.resource_name, + id: vulnerabilitiesByResourceColumns.resourceName, displayAsText: i18n.translate('xpack.csp.vulnerabilityByResourceTable.column.resourceName', { defaultMessage: 'Resource Name', }), diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_detection_rule_counter.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_detection_rule_counter.tsx new file mode 100644 index 0000000000000..4773e080dce47 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_detection_rule_counter.tsx @@ -0,0 +1,30 @@ +/* + * 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 React from 'react'; +import type { HttpSetup } from '@kbn/core/public'; +import { Vulnerability } from '../../../../common/schemas'; +import { DetectionRuleCounter } from '../../../components/detection_rule_counter'; +import { createDetectionRuleFromVulnerabilityFinding } from '../utils/create_detection_rule_from_vulnerability'; + +const CNVM_TAG = 'CNVM'; + +export const VulnerabilityDetectionRuleCounter = ({ + vulnerability, +}: { + vulnerability: Vulnerability; +}) => { + const createVulnerabilityRuleFn = async (http: HttpSetup) => + await createDetectionRuleFromVulnerabilityFinding(http, vulnerability); + + return ( + <DetectionRuleCounter + tags={[CNVM_TAG, vulnerability.id]} + createRuleFn={createVulnerabilityRuleFn} + /> + ); +}; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.test.tsx index 8d4e4eb3ea34a..516a25586b0ac 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.test.tsx @@ -39,7 +39,7 @@ describe('<VulnerabilityFindingFlyout/>', () => { getByText(mockVulnerabilityHit.vulnerability.description); const descriptionList = getByTestId(FINDINGS_VULNERABILITY_FLYOUT_DESCRIPTION_LIST); expect(descriptionList.textContent).toEqual( - `Resource:${mockVulnerabilityHit.resource?.name}Package:${mockVulnerabilityHit.vulnerability.package.name}Version:${mockVulnerabilityHit.vulnerability.package.version}` + `Resource ID:${mockVulnerabilityHit.resource?.id}Resource Name:${mockVulnerabilityHit.resource?.name}Package:${mockVulnerabilityHit.vulnerability.package.name}Version:${mockVulnerabilityHit.vulnerability.package.version}` ); getByText(mockVulnerabilityHit.vulnerability.severity); }); diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx index 26aaf9926c0a5..ac2212c208dad 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_finding_flyout.tsx @@ -50,10 +50,17 @@ const getFlyoutDescriptionList = ( vulnerabilityRecord: CspVulnerabilityFinding ): EuiDescriptionListProps['listItems'] => [ + vulnerabilityRecord.resource?.id && { + title: i18n.translate( + 'xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.resourceId', + { defaultMessage: 'Resource ID' } + ), + description: vulnerabilityRecord.resource.id, + }, vulnerabilityRecord.resource?.name && { title: i18n.translate( - 'xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.resourceTitle', - { defaultMessage: 'Resource' } + 'xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.resourceName', + { defaultMessage: 'Resource Name' } ), description: vulnerabilityRecord.resource.name, }, @@ -151,10 +158,10 @@ export const VulnerabilityFindingFlyout = ({ { defaultMessage: 'Loading' } ); - const vulnerabilityReference = getVulnerabilityReferenceUrl(vulnerabilityRecord); + const vulnerabilityReference = getVulnerabilityReferenceUrl(vulnerabilityRecord.vulnerability); const createVulnerabilityRuleFn = async (http: HttpSetup) => - await createDetectionRuleFromVulnerabilityFinding(http, vulnerabilityRecord); + await createDetectionRuleFromVulnerabilityFinding(http, vulnerabilityRecord.vulnerability); return ( <EuiFlyout onClose={closeFlyout}> diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx index 11cb395879b14..b25b2ae982d0a 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_finding_flyout/vulnerability_overview_tab.tsx @@ -27,6 +27,7 @@ import { CVSScoreProps, Vendor } from '../types'; import { getVectorScoreList } from '../utils/get_vector_score_list'; import { OVERVIEW_TAB_VULNERABILITY_FLYOUT } from '../test_subjects'; import redhatLogo from '../../../assets/icons/redhat_logo.svg'; +import { VulnerabilityDetectionRuleCounter } from './vulnerability_detection_rule_counter'; const cvssVendors: Record<string, Vendor> = { nvd: 'NVD', @@ -239,7 +240,15 @@ export const VulnerabilityOverviewTab = ({ vulnerability }: VulnerabilityTabProp </EuiFlexItem> <EuiHorizontalRule css={horizontalStyle} /> - + <EuiFlexItem> + <h4 css={flyoutSubheadingStyle}> + <FormattedMessage + id="xpack.csp.vulnerabilities.vulnerabilityOverviewTab.alertsTitle" + defaultMessage="Alerts" + /> + </h4> + <VulnerabilityDetectionRuleCounter vulnerability={vulnerability} /> + </EuiFlexItem> <EuiFlexItem> <h4 css={flyoutSubheadingStyle}> <FormattedMessage diff --git a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_table_columns.ts b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_table_columns.ts index ce0b78df31709..d95fca6f51849 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_table_columns.ts +++ b/x-pack/plugins/cloud_security_posture/public/pages/vulnerabilities/vulnerabilities_table_columns.ts @@ -13,11 +13,12 @@ export const vulnerabilitiesColumns = { actions: 'actions', vulnerability: 'vulnerability.id', cvss: 'vulnerability.score.base', - resource: 'resource.name', + resourceName: 'resource.name', + resourceId: 'resource.id', severity: 'vulnerability.severity', package: 'vulnerability.package.name', version: 'vulnerability.package.version', - fix_version: 'vulnerability.package.fixed_version', + fixedVersion: 'vulnerability.package.fixed_version', }; const defaultColumnProps = () => ({ @@ -61,9 +62,17 @@ export const getVulnerabilitiesColumnsGrid = ( }, { ...defaultColumnProps(), - id: vulnerabilitiesColumns.resource, - displayAsText: i18n.translate('xpack.csp.vulnerabilityTable.column.resource', { - defaultMessage: 'Resource', + id: vulnerabilitiesColumns.resourceId, + displayAsText: i18n.translate('xpack.csp.vulnerabilityTable.column.resourceId', { + defaultMessage: 'Resource ID', + }), + cellActions, + }, + { + ...defaultColumnProps(), + id: vulnerabilitiesColumns.resourceName, + displayAsText: i18n.translate('xpack.csp.vulnerabilityTable.column.resourceName', { + defaultMessage: 'Resource Name', }), cellActions, }, @@ -95,7 +104,7 @@ export const getVulnerabilitiesColumnsGrid = ( }, { ...defaultColumnProps(), - id: vulnerabilitiesColumns.fix_version, + id: vulnerabilitiesColumns.fixedVersion, displayAsText: i18n.translate('xpack.csp.vulnerabilityTable.column.fixVersion', { defaultMessage: 'Fix Version', }), diff --git a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/alert_stats_collector.ts b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/alert_stats_collector.ts new file mode 100644 index 0000000000000..7e63af4fb1320 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/alert_stats_collector.ts @@ -0,0 +1,210 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import type { CloudSecurityAlertsStats } from './types'; +import { DETECTION_ENGINE_ALERTS_INDEX_DEFAULT } from '../../../../common/constants'; + +interface AlertsStats { + aggregations: { + cspm: { + rules_count: { + value: number; + }; + alerts_open: { + doc_count: number; + }; + alerts_acknowledged: { + doc_count: number; + }; + alerts_closed: { + doc_count: number; + }; + }; + kspm: { + rules_count: { + value: number; + }; + alerts_open: { + doc_count: number; + }; + alerts_acknowledged: { + doc_count: number; + }; + alerts_closed: { + doc_count: number; + }; + }; + vuln_mgmt: { + rules_count: { + value: number; + }; + alerts_open: { + doc_count: number; + }; + alerts_acknowledged: { + doc_count: number; + }; + alerts_closed: { + doc_count: number; + }; + }; + }; +} + +const getAlertsStatsQuery = (index: string) => ({ + size: 0, + query: { + bool: { + filter: [{ term: { 'kibana.alert.rule.tags': 'Cloud Security' } }], + }, + }, + sort: '@timestamp:desc', + index, + aggs: { + cspm: { + filter: { + term: { + 'kibana.alert.rule.tags': 'CSPM', + }, + }, + aggs: { + rules_count: { + cardinality: { + field: 'kibana.alert.rule.uuid', + }, + }, + alerts_open: { + filter: { + term: { + 'kibana.alert.workflow_status': 'open', + }, + }, + }, + alerts_acknowledged: { + filter: { + term: { + 'kibana.alert.workflow_status': 'acknowledged', + }, + }, + }, + alerts_closed: { + filter: { + term: { + 'kibana.alert.workflow_status': 'closed', + }, + }, + }, + }, + }, + kspm: { + filter: { + term: { + 'kibana.alert.rule.tags': 'KSPM', + }, + }, + aggs: { + rules_count: { + cardinality: { + field: 'kibana.alert.rule.uuid', + }, + }, + alerts_open: { + filter: { + term: { + 'kibana.alert.workflow_status': 'open', + }, + }, + }, + alerts_acknowledged: { + filter: { + term: { + 'kibana.alert.workflow_status': 'acknowledged', + }, + }, + }, + alerts_closed: { + filter: { + term: { + 'kibana.alert.workflow_status': 'closed', + }, + }, + }, + }, + }, + vuln_mgmt: { + filter: { + term: { + 'kibana.alert.rule.tags': 'CNVM', + }, + }, + aggs: { + rules_count: { + cardinality: { + field: 'kibana.alert.rule.uuid', + }, + }, + alerts_open: { + filter: { + term: { + 'kibana.alert.workflow_status': 'open', + }, + }, + }, + alerts_acknowledged: { + filter: { + term: { + 'kibana.alert.workflow_status': 'acknowledged', + }, + }, + }, + alerts_closed: { + filter: { + term: { + 'kibana.alert.workflow_status': 'closed', + }, + }, + }, + }, + }, + }, +}); + +export const getAlertsStats = async ( + esClient: ElasticsearchClient, + logger: Logger +): Promise<CloudSecurityAlertsStats[]> => { + const index = DETECTION_ENGINE_ALERTS_INDEX_DEFAULT; + + try { + const isIndexExists = await esClient.indices.exists({ + index, + }); + + if (isIndexExists) { + const alertsStats = await esClient.search<unknown, AlertsStats>(getAlertsStatsQuery(index)); + + const postureTypes = ['cspm', 'kspm', 'vuln_mgmt'] as const; + + return postureTypes.map((postureType) => ({ + posture_type: postureType, + rules_count: alertsStats.aggregations?.aggregations[postureType].rules_count.value, + alerts_count: alertsStats.aggregations?.aggregations[postureType].alerts_open.doc_count, + alerts_open_count: + alertsStats.aggregations?.aggregations[postureType].alerts_open.doc_count, + alerts_acknowledged_count: + alertsStats.aggregations?.aggregations[postureType].alerts_acknowledged.doc_count, + alerts_closed_count: + alertsStats.aggregations?.aggregations[postureType].alerts_closed.doc_count, + })) as CloudSecurityAlertsStats[]; + } + return []; + } catch (e) { + logger.error(`Failed to get index stats for ${index}: ${e}`); + return []; + } +}; diff --git a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/register.ts b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/register.ts index 1b9b4f0370f6b..c9495c03eccdb 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/register.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/register.ts @@ -15,6 +15,7 @@ import { CspmUsage } from './types'; import { getAccountsStats } from './accounts_stats_collector'; import { getRulesStats } from './rules_stats_collector'; import { getInstallationStats } from './installation_stats_collector'; +import { getAlertsStats } from './alert_stats_collector'; export function registerCspmUsageCollector( logger: Logger, @@ -34,24 +35,31 @@ export function registerCspmUsageCollector( return true; }, fetch: async (collectorFetchContext: CollectorFetchContext) => { - const [indicesStats, accountsStats, resourcesStats, rulesStats, installationStats] = - await Promise.all([ - getIndicesStats( - collectorFetchContext.esClient, - collectorFetchContext.soClient, - coreServices, - logger - ), - getAccountsStats(collectorFetchContext.esClient, logger), - getResourcesStats(collectorFetchContext.esClient, logger), - getRulesStats(collectorFetchContext.esClient, logger), - getInstallationStats( - collectorFetchContext.esClient, - collectorFetchContext.soClient, - coreServices, - logger - ), - ]); + const [ + indicesStats, + accountsStats, + resourcesStats, + rulesStats, + installationStats, + alertsStats, + ] = await Promise.all([ + getIndicesStats( + collectorFetchContext.esClient, + collectorFetchContext.soClient, + coreServices, + logger + ), + getAccountsStats(collectorFetchContext.esClient, logger), + getResourcesStats(collectorFetchContext.esClient, logger), + getRulesStats(collectorFetchContext.esClient, logger), + getInstallationStats( + collectorFetchContext.esClient, + collectorFetchContext.soClient, + coreServices, + logger + ), + getAlertsStats(collectorFetchContext.esClient, logger), + ]); return { indices: indicesStats, @@ -59,6 +67,7 @@ export function registerCspmUsageCollector( resources_stats: resourcesStats, rules_stats: rulesStats, installation_stats: installationStats, + alerts_stats: alertsStats, }; }, schema: cspmUsageSchema, diff --git a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/schema.ts b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/schema.ts index 578f2c17894df..5441992618192 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/schema.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/schema.ts @@ -156,4 +156,15 @@ export const cspmUsageSchema: MakeSchemaFrom<CspmUsage> = { account_type: { type: 'keyword' }, }, }, + alerts_stats: { + type: 'array', + items: { + posture_type: { type: 'keyword' }, + rules_count: { type: 'long' }, + alerts_count: { type: 'long' }, + alerts_open_count: { type: 'long' }, + alerts_closed_count: { type: 'long' }, + alerts_acknowledged_count: { type: 'long' }, + }, + }, }; diff --git a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/types.ts b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/types.ts index 53a94067ed67a..0c04de498509a 100644 --- a/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/types.ts +++ b/x-pack/plugins/cloud_security_posture/server/lib/telemetry/collectors/types.ts @@ -13,6 +13,7 @@ export interface CspmUsage { accounts_stats: CspmAccountsStats[]; rules_stats: CspmRulesStats[]; installation_stats: CloudSecurityInstallationStats[]; + alerts_stats: CloudSecurityAlertsStats[]; } export interface PackageSetupStatus { @@ -88,3 +89,12 @@ export interface CloudSecurityInstallationStats { agent_count: number; account_type?: 'single-account' | 'organization-account'; } + +export interface CloudSecurityAlertsStats { + posture_type: string; + rules_count: number; + alerts_count: number; + alerts_open_count: number; + alerts_closed_count: number; + alerts_acknowledged_count: number; +} diff --git a/x-pack/plugins/cloud_security_posture/server/routes/detection_engine/get_detection_engine_alerts_count_by_rule_tags.ts b/x-pack/plugins/cloud_security_posture/server/routes/detection_engine/get_detection_engine_alerts_count_by_rule_tags.ts new file mode 100644 index 0000000000000..d464563155023 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/server/routes/detection_engine/get_detection_engine_alerts_count_by_rule_tags.ts @@ -0,0 +1,95 @@ +/* + * 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 { SearchResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { schema } from '@kbn/config-schema'; +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { transformError } from '@kbn/securitysolution-es-utils'; +import { + DETECTION_RULE_ALERTS_STATUS_API_CURRENT_VERSION, + GET_DETECTION_RULE_ALERTS_STATUS_PATH, +} from '../../../common/constants'; +import { CspRouter } from '../../types'; + +export interface VulnerabilitiesStatisticsQueryResult { + total: number; +} + +const DEFAULT_ALERTS_INDEX = '.alerts-security.alerts-default' as const; + +export const getDetectionEngineAlertsCountByRuleTags = async ( + esClient: ElasticsearchClient, + tags: string[] +) => { + return await esClient.search<unknown, SearchResponse>({ + size: 0, + query: { + bool: { + filter: [ + { term: { 'kibana.alert.rule.tags': 'Cloud Security' } }, + ...tags.map((tag) => ({ term: { 'kibana.alert.rule.tags': tag } })), + ], + }, + }, + sort: '@timestamp:desc', + index: DEFAULT_ALERTS_INDEX, + }); +}; + +const getDetectionEngineAlertsStatus = async (esClient: ElasticsearchClient, tags: string[]) => { + const alertsCountByTags = await getDetectionEngineAlertsCountByRuleTags(esClient, tags); + + const total = + typeof alertsCountByTags.hits.total === 'number' + ? alertsCountByTags.hits.total + : alertsCountByTags.hits.total?.value; + + return { + total, + }; +}; +export const defineGetDetectionEngineAlertsStatus = (router: CspRouter) => + router.versioned + .get({ + access: 'internal', + path: GET_DETECTION_RULE_ALERTS_STATUS_PATH, + }) + .addVersion( + { + version: DETECTION_RULE_ALERTS_STATUS_API_CURRENT_VERSION, + validate: { + request: { + query: schema.object({ + tags: schema.arrayOf(schema.string()), + }), + }, + }, + }, + async (context, request, response) => { + if (!(await context.fleet).authz.fleet.all) { + return response.forbidden(); + } + + const requestBody = request.query; + const cspContext = await context.csp; + + try { + const alerts = await getDetectionEngineAlertsStatus( + cspContext.esClient.asCurrentUser, + requestBody.tags + ); + return response.ok({ body: alerts }); + } catch (err) { + const error = transformError(err); + cspContext.logger.error(`Failed to fetch csp rules templates ${err}`); + return response.customError({ + body: { message: error.message }, + statusCode: error.statusCode, + }); + } + } + ); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/setup_routes.ts b/x-pack/plugins/cloud_security_posture/server/routes/setup_routes.ts index 426385682180d..a0e33bce73d3f 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/setup_routes.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/setup_routes.ts @@ -18,12 +18,13 @@ import { defineGetVulnerabilitiesDashboardRoute } from './vulnerabilities_dashbo import { defineGetBenchmarksRoute } from './benchmarks/benchmarks'; import { defineGetCspStatusRoute } from './status/status'; import { defineFindCspRuleTemplateRoute } from './csp_rule_template/get_csp_rule_template'; +import { defineGetDetectionEngineAlertsStatus } from './detection_engine/get_detection_engine_alerts_count_by_rule_tags'; /** * 1. Registers routes * 2. Registers routes handler context */ -export function setupRoutes({ +export async function setupRoutes({ core, logger, isPluginInitialized, @@ -38,6 +39,7 @@ export function setupRoutes({ defineGetBenchmarksRoute(router); defineGetCspStatusRoute(router); defineFindCspRuleTemplateRoute(router); + defineGetDetectionEngineAlertsStatus(router); core.http.registerRouteHandlerContext<CspRequestHandlerContext, typeof PLUGIN_ID>( PLUGIN_ID, diff --git a/x-pack/plugins/enterprise_search/common/connectors/connectors.ts b/x-pack/plugins/enterprise_search/common/connectors/connectors.ts index a1d5b87d9f965..3595ca7644770 100644 --- a/x-pack/plugins/enterprise_search/common/connectors/connectors.ts +++ b/x-pack/plugins/enterprise_search/common/connectors/connectors.ts @@ -38,6 +38,17 @@ export const CONNECTOR_DEFINITIONS: ConnectorServerSideDefinition[] = [ }), serviceType: 'confluence', }, + { + iconPath: 'dropbox.svg', + isBeta: true, + isNative: true, + isTechPreview: false, + keywords: ['dropbox', 'connector'], + name: i18n.translate('xpack.enterpriseSearch.content.nativeConnectors.dropbox.name', { + defaultMessage: 'Dropbox', + }), + serviceType: 'dropbox', + }, { iconPath: 'jira_cloud.svg', isBeta: true, @@ -128,6 +139,17 @@ export const CONNECTOR_DEFINITIONS: ConnectorServerSideDefinition[] = [ }), serviceType: 'postgresql', }, + { + iconPath: 'servicenow.svg', + isBeta: true, + isNative: true, + isTechPreview: false, + keywords: ['servicenow', 'cloud', 'connector'], + name: i18n.translate('xpack.enterpriseSearch.content.nativeConnectors.serviceNow.name', { + defaultMessage: 'ServiceNow', + }), + serviceType: 'servicenow', + }, { iconPath: 'sharepoint_online.svg', isBeta: false, @@ -139,17 +161,6 @@ export const CONNECTOR_DEFINITIONS: ConnectorServerSideDefinition[] = [ }), serviceType: 'sharepoint_online', }, - { - iconPath: 'dropbox.svg', - isBeta: true, - isNative: true, - isTechPreview: false, - keywords: ['dropbox', 'connector'], - name: i18n.translate('xpack.enterpriseSearch.content.nativeConnectors.dropbox.name', { - defaultMessage: 'Dropbox', - }), - serviceType: 'dropbox', - }, { iconPath: 'gmail.svg', isBeta: false, @@ -171,6 +182,16 @@ export const CONNECTOR_DEFINITIONS: ConnectorServerSideDefinition[] = [ }), serviceType: 'oracle', }, + { + iconPath: 'onedrive.svg', + isBeta: true, + isNative: false, + keywords: ['network', 'drive', 'file', 'connector'], + name: i18n.translate('xpack.enterpriseSearch.content.nativeConnectors.oneDrive.name', { + defaultMessage: 'OneDrive', + }), + serviceType: 'onedrive', + }, { iconPath: 's3.svg', isBeta: true, @@ -181,17 +202,6 @@ export const CONNECTOR_DEFINITIONS: ConnectorServerSideDefinition[] = [ }), serviceType: 's3', }, - { - iconPath: 'servicenow.svg', - isBeta: true, - isNative: true, - isTechPreview: false, - keywords: ['servicenow', 'cloud', 'connector'], - name: i18n.translate('xpack.enterpriseSearch.content.nativeConnectors.serviceNow.name', { - defaultMessage: 'ServiceNow', - }), - serviceType: 'servicenow', - }, { iconPath: 'slack.svg', isBeta: false, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/select_connector/select_connector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/select_connector/select_connector.tsx index 1d207db7b077c..1afc443077e15 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/select_connector/select_connector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/select_connector/select_connector.tsx @@ -55,28 +55,37 @@ import { ConnectorCheckable } from './connector_checkable'; export const SelectConnector: React.FC = () => { const { search } = useLocation(); + const { isCloud } = useValues(KibanaLogic); + const { hasPlatinumLicense } = useValues(LicensingLogic); + const hasNativeAccess = isCloud; const { service_type: serviceType } = parseQueryParams(search); const [useNativeFilter, setUseNativeFilter] = useState(false); const [useNonGAFilter, setUseNonGAFilter] = useState(true); const [searchTerm, setSearchTerm] = useState(''); - const filteredConnectors = useMemo( - () => - CONNECTORS.filter((connector) => + const filteredConnectors = useMemo(() => { + const nativeConnectors = hasNativeAccess + ? CONNECTORS.filter((connector) => connector.isNative).sort((a, b) => + a.name.localeCompare(b.name) + ) + : []; + const nonNativeConnectors = hasNativeAccess + ? CONNECTORS.filter((connector) => !connector.isNative).sort((a, b) => + a.name.localeCompare(b.name) + ) + : CONNECTORS.sort((a, b) => a.name.localeCompare(b.name)); + const connectors = [...nativeConnectors, ...nonNativeConnectors]; + return connectors + .filter((connector) => useNonGAFilter ? true : !connector.isBeta && !connector.isTechPreview ) - .filter((connector) => (useNativeFilter ? connector.isNative : true)) - .filter((connector) => - searchTerm ? connector.name.toLowerCase().includes(searchTerm.toLowerCase()) : true - ), - [useNonGAFilter, useNativeFilter, searchTerm] - ); + .filter((connector) => (useNativeFilter ? connector.isNative : true)) + .filter((connector) => + searchTerm ? connector.name.toLowerCase().includes(searchTerm.toLowerCase()) : true + ); + }, [useNonGAFilter, useNativeFilter, searchTerm]); const [selectedConnector, setSelectedConnector] = useState<string | null>( Array.isArray(serviceType) ? serviceType[0] : serviceType ?? null ); - const { isCloud } = useValues(KibanaLogic); - const { hasPlatinumLicense } = useValues(LicensingLogic); - - const hasNativeAccess = isCloud; return ( <EnterpriseSearchContentPageTemplate diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/constants.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/constants.ts index bea446bf528c0..b10069f12c3fe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/constants.ts @@ -37,8 +37,14 @@ export const CONNECTORS_DICT: Record<string, ConnectorClientSideDefinition> = { externalDocsUrl: '', icon: CONNECTOR_ICONS.dropbox, }, + github: { + docsUrl: docLinks.connectorsGithub, + externalAuthDocsUrl: '', + externalDocsUrl: '', + icon: CONNECTOR_ICONS.github, + }, gmail: { - docsUrl: '', // TODO + docsUrl: docLinks.connectorsGmail, externalAuthDocsUrl: '', externalDocsUrl: '', icon: CONNECTOR_ICONS.gmail, @@ -85,6 +91,12 @@ export const CONNECTORS_DICT: Record<string, ConnectorClientSideDefinition> = { externalDocsUrl: '', icon: CONNECTOR_ICONS.network_drive, }, + onedrive: { + docsUrl: docLinks.connectorsOneDrive, + externalAuthDocsUrl: '', + externalDocsUrl: '', + icon: CONNECTOR_ICONS.onedrive, + }, oracle: { docsUrl: docLinks.connectorsOracle, externalAuthDocsUrl: @@ -104,18 +116,24 @@ export const CONNECTORS_DICT: Record<string, ConnectorClientSideDefinition> = { externalDocsUrl: '', icon: CONNECTOR_ICONS.amazon_s3, }, + salesforce: { + docsUrl: docLinks.connectorsSalesforce, + externalAuthDocsUrl: '', + externalDocsUrl: '', + icon: CONNECTOR_ICONS.salesforce, + }, servicenow: { docsUrl: docLinks.connectorsServiceNow, externalAuthDocsUrl: '', externalDocsUrl: '', icon: CONNECTOR_ICONS.servicenow, }, - sharepoint: { + sharepoint_server: { docsUrl: docLinks.connectorsSharepoint, externalAuthDocsUrl: '', externalDocsUrl: '', icon: CONNECTOR_ICONS.sharepoint, - platinumOnly: true, + platinumOnly: false, }, sharepoint_online: { docsUrl: docLinks.connectorsSharepointOnline, @@ -125,11 +143,11 @@ export const CONNECTORS_DICT: Record<string, ConnectorClientSideDefinition> = { platinumOnly: true, }, slack: { - docsUrl: '', // TODO + docsUrl: docLinks.connectorsSlack, externalAuthDocsUrl: '', externalDocsUrl: '', icon: CONNECTOR_ICONS.slack, - platinumOnly: true, + platinumOnly: false, }, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts index a30c968077ace..377d2f9ccd71d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts @@ -67,6 +67,8 @@ class DocLinks { public connectorsConfluence: string; public connectorsContentExtraction: string; public connectorsDropbox: string; + public connectorsGithub: string; + public connectorsGmail: string; public connectorsGoogleCloudStorage: string; public connectorsGoogleDrive: string; public connectorsJira: string; @@ -75,12 +77,15 @@ class DocLinks { public connectorsMySQL: string; public connectorsNative: string; public connectorsNetworkDrive: string; + public connectorsOneDrive: string; public connectorsOracle: string; public connectorsPostgreSQL: string; public connectorsS3: string; + public connectorsSalesforce: string; public connectorsServiceNow: string; public connectorsSharepoint: string; public connectorsSharepointOnline: string; + public connectorsSlack: string; public connectorsWorkplaceSearch: string; public consoleGuide: string; public crawlerExtractionRules: string; @@ -226,6 +231,8 @@ class DocLinks { this.connectorsContentExtraction = ''; this.connectorsClients = ''; this.connectorsDropbox = ''; + this.connectorsGithub = ''; + this.connectorsGmail = ''; this.connectorsGoogleCloudStorage = ''; this.connectorsGoogleDrive = ''; this.connectorsJira = ''; @@ -234,12 +241,15 @@ class DocLinks { this.connectorsMySQL = ''; this.connectorsNative = ''; this.connectorsNetworkDrive = ''; + this.connectorsOneDrive = ''; this.connectorsOracle = ''; this.connectorsPostgreSQL = ''; this.connectorsS3 = ''; + this.connectorsSalesforce = ''; this.connectorsServiceNow = ''; this.connectorsSharepoint = ''; this.connectorsSharepointOnline = ''; + this.connectorsSlack = ''; this.connectorsWorkplaceSearch = ''; this.consoleGuide = ''; this.crawlerExtractionRules = ''; @@ -386,9 +396,11 @@ class DocLinks { this.connectorsContentExtraction = docLinks.links.enterpriseSearch.connectorsContentExtraction; this.connectorsClients = docLinks.links.enterpriseSearch.connectorsClients; this.connectorsDropbox = docLinks.links.enterpriseSearch.connectorsDropbox; + this.connectorsGithub = docLinks.links.enterpriseSearch.connectorsGithub; this.connectorsGoogleCloudStorage = docLinks.links.enterpriseSearch.connectorsGoogleCloudStorage; this.connectorsGoogleDrive = docLinks.links.enterpriseSearch.connectorsGoogleDrive; + this.connectorsGmail = docLinks.links.enterpriseSearch.connectorsGmail; this.connectorsJira = docLinks.links.enterpriseSearch.connectorsJira; this.connectorsMicrosoftSQL = docLinks.links.enterpriseSearch.connectorsMicrosoftSQL; this.connectorsMongoDB = docLinks.links.enterpriseSearch.connectorsMongoDB; @@ -401,6 +413,7 @@ class DocLinks { this.connectorsServiceNow = docLinks.links.enterpriseSearch.connectorsServiceNow; this.connectorsSharepoint = docLinks.links.enterpriseSearch.connectorsSharepoint; this.connectorsSharepointOnline = docLinks.links.enterpriseSearch.connectorsSharepointOnline; + this.connectorsSlack = docLinks.links.enterpriseSearch.connectorsSlack; this.connectorsWorkplaceSearch = docLinks.links.enterpriseSearch.connectorsWorkplaceSearch; this.consoleGuide = docLinks.links.console.guide; this.crawlerExtractionRules = docLinks.links.enterpriseSearch.crawlerExtractionRules; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/icons/connector_icons.ts b/x-pack/plugins/enterprise_search/public/applications/shared/icons/connector_icons.ts index 0f41254093984..ab3dc7a6cfb37 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/icons/connector_icons.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/icons/connector_icons.ts @@ -18,9 +18,11 @@ import mongodb from '../../../assets/source_icons/mongodb.svg'; import microsoft_sql from '../../../assets/source_icons/mssql.svg'; import mysql from '../../../assets/source_icons/mysql.svg'; import network_drive from '../../../assets/source_icons/network_drive.svg'; +import onedrive from '../../../assets/source_icons/onedrive.svg'; import oracle from '../../../assets/source_icons/oracle.svg'; import postgresql from '../../../assets/source_icons/postgresql.svg'; import amazon_s3 from '../../../assets/source_icons/s3.svg'; +import salesforce from '../../../assets/source_icons/salesforce.svg'; import servicenow from '../../../assets/source_icons/servicenow.svg'; import sharepoint from '../../../assets/source_icons/sharepoint.svg'; import sharepoint_online from '../../../assets/source_icons/sharepoint_online.svg'; @@ -41,8 +43,10 @@ export const CONNECTOR_ICONS = { mongodb, mysql, network_drive, + onedrive, oracle, postgresql, + salesforce, servicenow, sharepoint, sharepoint_online, diff --git a/x-pack/plugins/enterprise_search/server/integrations.ts b/x-pack/plugins/enterprise_search/server/integrations.ts index 6d2e67d602548..cedbd4215a656 100644 --- a/x-pack/plugins/enterprise_search/server/integrations.ts +++ b/x-pack/plugins/enterprise_search/server/integrations.ts @@ -34,36 +34,6 @@ const workplaceSearchIntegrations: WorkplaceSearchIntegration[] = [ ), categories: ['enterprise_search', 'workplace_search_content_source'], }, - { - id: 'onedrive', - title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.onedriveName', { - defaultMessage: 'OneDrive', - }), - description: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.integrations.onedriveDescription', - { - defaultMessage: 'Search over your files stored on OneDrive with Workplace Search.', - } - ), - categories: ['enterprise_search', 'azure', 'workplace_search_content_source'], - uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/one_drive', - }, - { - id: 'salesforce_sandbox', - title: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxName', - { - defaultMessage: 'Salesforce Sandbox', - } - ), - description: i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxDescription', - { - defaultMessage: 'Search over your content on Salesforce Sandbox with Workplace Search.', - } - ), - categories: ['enterprise_search', 'workplace_search_content_source'], - }, { id: 'zendesk', title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.zendeskName', { @@ -359,6 +329,40 @@ export const registerEnterpriseSearchIntegrations = ( shipper: 'enterprise_search', isBeta: false, }); + + customIntegrations.registerCustomIntegration({ + id: 'onedrive', + title: i18n.translate('xpack.enterpriseSearch.integrations.oneDriveTitle', { + defaultMessage: 'OneDrive', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.oneDriveDescription', + { + defaultMessage: 'Search over your content on OneDrive.', + } + ), + categories: [ + 'enterprise_search', + 'elastic_stack', + 'custom', + 'datastore', + 'connector', + 'connector_client', + ], + uiInternalPath: + '/app/enterprise_search/content/search_indices/new_index/connector?service_type=salesforce', + icons: [ + { + type: 'svg', + src: http.basePath.prepend( + '/plugins/enterpriseSearch/assets/source_icons/salesforce_sandbox.svg' + ), + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); + customIntegrations.registerCustomIntegration({ id: 'build_a_connector', title: i18n.translate('xpack.enterpriseSearch.integrations.buildAConnectorName', { @@ -417,6 +421,39 @@ export const registerEnterpriseSearchIntegrations = ( isBeta: false, }); + customIntegrations.registerCustomIntegration({ + id: 'salesforce_sandbox', + title: i18n.translate('xpack.enterpriseSearch.integrations.salesforceSandboxTitle', { + defaultMessage: 'Salesforce Sandbox', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxDescription', + { + defaultMessage: 'Search over your content on Salesforce Sandbox.', + } + ), + categories: [ + 'enterprise_search', + 'elastic_stack', + 'custom', + 'datastore', + 'connector', + 'connector_client', + ], + uiInternalPath: + '/app/enterprise_search/content/search_indices/new_index/connector?service_type=salesforce', + icons: [ + { + type: 'svg', + src: http.basePath.prepend( + '/plugins/enterpriseSearch/assets/source_icons/salesforce_sandbox.svg' + ), + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); + customIntegrations.registerCustomIntegration({ id: 'servicenow', title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.serviceNowName', { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_cloud_formation_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_cloud_formation_modal.tsx index 7ed959e40efa0..0ca51d6f595fa 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_cloud_formation_modal.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_cloud_formation_modal.tsx @@ -58,7 +58,7 @@ export const PostInstallCloudFormationModal: React.FunctionComponent<{ </EuiModalHeader> <EuiModalBody> - <CloudFormationGuide /> + <CloudFormationGuide awsAccountType={cloudFormationProps.awsAccountType} /> {error && isError && ( <> <EuiSpacer size="m" /> diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx index 83031548293f7..61ed68a059cab 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/cloud_formation_instructions.tsx @@ -50,7 +50,9 @@ export const CloudFormationInstructions: React.FunctionComponent<Props> = ({ } )} > - <CloudFormationGuide /> + <CloudFormationGuide + awsAccountType={cloudSecurityIntegration?.cloudFormationProps?.awsAccountType} + /> <EuiSpacer size="m" /> <EuiButton color="primary" diff --git a/x-pack/plugins/fleet/public/components/cloud_formation_guide.tsx b/x-pack/plugins/fleet/public/components/cloud_formation_guide.tsx index afcb985645f7c..a62f2de208123 100644 --- a/x-pack/plugins/fleet/public/components/cloud_formation_guide.tsx +++ b/x-pack/plugins/fleet/public/components/cloud_formation_guide.tsx @@ -9,6 +9,8 @@ import React from 'react'; import { EuiLink, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import type { CloudSecurityIntegrationAwsAccountType } from './agent_enrollment_flyout/types'; + const CLOUD_FORMATION_EXTERNAL_DOC_URL = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-whatis-howdoesitwork.html'; @@ -23,7 +25,11 @@ const Link = ({ children, url }: { children: React.ReactNode; url: string }) => </EuiLink> ); -export const CloudFormationGuide = () => { +export const CloudFormationGuide = ({ + awsAccountType, +}: { + awsAccountType?: CloudSecurityIntegrationAwsAccountType; +}) => { return ( <EuiText> <p> @@ -44,12 +50,21 @@ export const CloudFormationGuide = () => { </p> <EuiText size="s" color="subdued"> <ol> - <li> - <FormattedMessage - id="xpack.fleet.cloudFormation.guide.steps.login" - defaultMessage="Ensure you are logged in as an admin in the AWS Account you want to onboard" - /> - </li> + {awsAccountType === 'organization-account' ? ( + <li> + <FormattedMessage + id="xpack.fleet.cloudFormation.guide.steps.organizationLogin" + defaultMessage="Log in as an admin in the management account of the AWS Organization you want to onboard" + /> + </li> + ) : ( + <li> + <FormattedMessage + id="xpack.fleet.cloudFormation.guide.steps.login" + defaultMessage="Log in as an admin in the AWS account you want to onboard" + /> + </li> + )} <li> <FormattedMessage id="xpack.fleet.cloudFormation.guide.steps.launch" diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.ts index 9c3288614fa9d..db46af9091be1 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.ts @@ -55,6 +55,14 @@ export const RuleAuthorArray = t.array(t.string); // should be non-empty strings export type RuleFalsePositiveArray = t.TypeOf<typeof RuleFalsePositiveArray>; export const RuleFalsePositiveArray = t.array(t.string); // should be non-empty strings? +/** + * User defined fields to display in areas such as alert details and exceptions auto-populate + * Field added in PR - https://github.com/elastic/kibana/pull/163235 + * @example const investigationFields: RuleCustomHighlightedFieldArray = ['host.os.name'] + */ +export type RuleCustomHighlightedFieldArray = t.TypeOf<typeof RuleCustomHighlightedFieldArray>; +export const RuleCustomHighlightedFieldArray = t.array(NonEmptyString); + export type RuleReferenceArray = t.TypeOf<typeof RuleReferenceArray>; export const RuleReferenceArray = t.array(t.string); // should be non-empty strings? diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_request_schema.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_request_schema.test.ts index a49112a98f81d..9d15c355df60d 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_request_schema.test.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_request_schema.test.ts @@ -1289,6 +1289,36 @@ describe('rules schema', () => { expect(message.schema).toEqual({}); expect(getPaths(left(message.errors))).toEqual(['invalid keys "data_view_id"']); }); + + test('You can optionally send in an array of investigation_fields', () => { + const payload: RuleCreateProps = { + ...getCreateRulesSchemaMock(), + investigation_fields: ['field1', 'field2'], + }; + + const decoded = RuleCreateProps.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('You cannot send in an array of investigation_fields that are numbers', () => { + const payload = { + ...getCreateRulesSchemaMock(), + investigation_fields: [0, 1, 2], + }; + + const decoded = RuleCreateProps.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "investigation_fields"', + 'Invalid value "1" supplied to "investigation_fields"', + 'Invalid value "2" supplied to "investigation_fields"', + ]); + expect(message.schema).toEqual({}); + }); }); describe('response', () => { diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.mock.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.mock.ts index 521e9918a6521..20d2c11042516 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.mock.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.mock.ts @@ -64,6 +64,7 @@ const getResponseBaseParams = (anchorDate: string = ANCHOR_DATE): SharedResponse timestamp_override: undefined, timestamp_override_fallback_disabled: undefined, namespace: undefined, + investigation_fields: undefined, }); export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): QueryRule => ({ @@ -77,6 +78,7 @@ export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): QueryRule saved_id: undefined, response_actions: undefined, alert_suppression: undefined, + investigation_fields: undefined, }); export const getSavedQuerySchemaMock = (anchorDate: string = ANCHOR_DATE): SavedQueryRule => ({ diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.test.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.test.ts index 0032ee60267c4..cd1562b1c4c48 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.test.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_response_schema.test.ts @@ -232,4 +232,65 @@ describe('Rule response schema', () => { expect(message.schema).toEqual({}); }); }); + + describe('investigation_fields', () => { + test('it should validate rule with empty array for "investigation_fields"', () => { + const payload = getRulesSchemaMock(); + payload.investigation_fields = []; + + const decoded = RuleResponse.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = { ...getRulesSchemaMock(), investigation_fields: [] }; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should validate rule with "investigation_fields"', () => { + const payload = getRulesSchemaMock(); + payload.investigation_fields = ['foo', 'bar']; + + const decoded = RuleResponse.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = { ...getRulesSchemaMock(), investigation_fields: ['foo', 'bar'] }; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should validate undefined for "investigation_fields"', () => { + const payload: RuleResponse = { + ...getRulesSchemaMock(), + investigation_fields: undefined, + }; + + const decoded = RuleResponse.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = { ...getRulesSchemaMock(), investigation_fields: undefined }; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should NOT validate a string for "investigation_fields"', () => { + const payload: Omit<RuleResponse, 'investigation_fields'> & { + investigation_fields: string; + } = { + ...getRulesSchemaMock(), + investigation_fields: 'foo', + }; + + const decoded = RuleResponse.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "foo" supplied to "investigation_fields"', + ]); + expect(message.schema).toEqual({}); + }); + }); }); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.ts index 834607906b2e5..24badba560b8e 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.ts @@ -53,6 +53,7 @@ import { RelatedIntegrationArray, RequiredFieldArray, RuleAuthorArray, + RuleCustomHighlightedFieldArray, RuleDescription, RuleFalsePositiveArray, RuleFilterArray, @@ -116,6 +117,7 @@ export const baseSchema = buildRuleSchemas({ output_index: AlertsIndex, namespace: AlertsIndexNamespace, meta: RuleMetadata, + investigation_fields: RuleCustomHighlightedFieldArray, // Throttle throttle: RuleActionThrottle, }, diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 449092e86130a..8393ef508c097 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -112,7 +112,7 @@ export const allowedExperimentalValues = Object.freeze({ * Enables Discover embedded within timeline * * */ - discoverInTimeline: true, + discoverInTimeline: false, }); type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx index c6a022ff0998f..a92ec9901d7ef 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx @@ -141,6 +141,45 @@ describe('AlertSummaryView', () => { }); }); }); + test('User specified investigation fields appear in summary rows', async () => { + const mockData = mockAlertDetailsData.map((item) => { + if (item.category === 'event' && item.field === 'event.category') { + return { + ...item, + values: ['network'], + originalValue: ['network'], + }; + } + return item; + }); + const renderProps = { + ...props, + investigationFields: ['custom.field'], + data: [ + ...mockData, + { category: 'custom', field: 'custom.field', values: ['blob'], originalValue: 'blob' }, + ] as TimelineEventsDetailsItem[], + }; + await act(async () => { + const { getByText } = render( + <TestProvidersComponent> + <AlertSummaryView {...renderProps} /> + </TestProvidersComponent> + ); + + [ + 'custom.field', + 'host.name', + 'user.name', + 'destination.address', + 'source.address', + 'source.port', + 'process.name', + ].forEach((fieldId) => { + expect(getByText(fieldId)); + }); + }); + }); test('Network event renders the correct summary rows', async () => { const renderProps = { ...props, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx index 5704eab502ed8..4eb81ddf5770f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx @@ -21,10 +21,30 @@ const AlertSummaryViewComponent: React.FC<{ title: string; goToTable: () => void; isReadOnly?: boolean; -}> = ({ browserFields, data, eventId, isDraggable, scopeId, title, goToTable, isReadOnly }) => { + investigationFields?: string[]; +}> = ({ + browserFields, + data, + eventId, + isDraggable, + scopeId, + title, + goToTable, + isReadOnly, + investigationFields, +}) => { const summaryRows = useMemo( - () => getSummaryRows({ browserFields, data, eventId, isDraggable, scopeId, isReadOnly }), - [browserFields, data, eventId, isDraggable, scopeId, isReadOnly] + () => + getSummaryRows({ + browserFields, + data, + eventId, + isDraggable, + scopeId, + isReadOnly, + investigationFields, + }), + [browserFields, data, eventId, isDraggable, scopeId, isReadOnly, investigationFields] ); return ( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index ff5d44d2b9eb9..87f450ecb43b4 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -21,6 +21,8 @@ import styled from 'styled-components'; import { isEmpty } from 'lodash'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; +import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback'; import type { RawEventData } from '../../../../common/types/response_actions'; import { useResponseActionsView } from './response_actions_view'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; @@ -169,6 +171,8 @@ const EventDetailsComponent: React.FC<Props> = ({ const goToTableTab = useCallback(() => setSelectedTabId(EventsViewType.tableView), []); const eventFields = useMemo(() => getEnrichmentFields(data), [data]); + const { ruleId } = useBasicDataFromDetailsData(data); + const { rule: maybeRule } = useRuleWithFallback(ruleId); const existingEnrichments = useMemo( () => isAlert @@ -284,6 +288,7 @@ const EventDetailsComponent: React.FC<Props> = ({ isReadOnly, }} goToTable={goToTableTab} + investigationFields={maybeRule?.investigation_fields ?? []} /> <EuiSpacer size="xl" /> <Insights @@ -337,6 +342,7 @@ const EventDetailsComponent: React.FC<Props> = ({ userRisk, allEnrichments, isEnrichmentsLoading, + maybeRule, ] ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx index 82be0711b75cc..160a91a9874ca 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/get_alert_summary_rows.tsx @@ -215,6 +215,15 @@ function getFieldsByRuleType(ruleType?: string): EventSummaryField[] { } } +/** + * Gets the fields to display based on custom rules and configuration + * @param customs The list of custom-defined fields to display + * @returns The list of custom-defined fields to display + */ +function getHighlightedFieldsOverride(customs: string[]): EventSummaryField[] { + return customs.map((field) => ({ id: field })); +} + /** This function is exported because it is used in the Exception Component to populate the conditions with the Highlighted Fields. Additionally, the new @@ -229,12 +238,15 @@ export function getEventFieldsToDisplay({ eventCategories, eventCode, eventRuleType, + highlightedFieldsOverride, }: { eventCategories: EventCategories; eventCode?: string; eventRuleType?: string; + highlightedFieldsOverride: string[]; }): EventSummaryField[] { const fields = [ + ...getHighlightedFieldsOverride(highlightedFieldsOverride), ...alwaysDisplayedFields, ...getFieldsByCategory(eventCategories), ...getFieldsByEventCode(eventCode, eventCategories), @@ -281,11 +293,13 @@ export const getSummaryRows = ({ eventId, isDraggable = false, isReadOnly = false, + investigationFields, }: { data: TimelineEventsDetailsItem[]; browserFields: BrowserFields; scopeId: string; eventId: string; + investigationFields?: string[]; isDraggable?: boolean; isReadOnly?: boolean; }) => { @@ -306,6 +320,7 @@ export const getSummaryRows = ({ eventCategories, eventCode, eventRuleType, + highlightedFieldsOverride: investigationFields ?? [], }); return data != null diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts index 80a7971aedfc1..5f1a36f931026 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts @@ -555,6 +555,7 @@ describe('helpers', () => { severity_mapping: [], tags: ['tag1', 'tag2'], threat: getThreatMock(), + investigation_fields: ['foo', 'bar'], }; expect(result).toEqual(expected); @@ -635,6 +636,7 @@ describe('helpers', () => { severity_mapping: [], tags: ['tag1', 'tag2'], threat: getThreatMock(), + investigation_fields: ['foo', 'bar'], }; expect(result).toEqual(expected); @@ -659,6 +661,7 @@ describe('helpers', () => { severity_mapping: [], tags: ['tag1', 'tag2'], threat: getThreatMock(), + investigation_fields: ['foo', 'bar'], }; expect(result).toEqual(expected); @@ -702,6 +705,7 @@ describe('helpers', () => { severity_mapping: [], tags: ['tag1', 'tag2'], threat: getThreatMock(), + investigation_fields: ['foo', 'bar'], }; expect(result).toEqual(expected); @@ -754,6 +758,7 @@ describe('helpers', () => { ], }, ], + investigation_fields: ['foo', 'bar'], }; expect(result).toEqual(expected); @@ -782,6 +787,95 @@ describe('helpers', () => { threat: getThreatMock(), timestamp_override: 'event.ingest', timestamp_override_fallback_disabled: true, + investigation_fields: ['foo', 'bar'], + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object if investigation_fields is empty array', () => { + const mockStepData: AboutStepRule = { + ...mockData, + investigationFields: [], + }; + const result = formatAboutStepData(mockStepData); + const expected: AboutStepRuleJson = { + author: ['Elastic'], + description: '24/7', + false_positives: ['test'], + license: 'Elastic License', + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + risk_score: 21, + risk_score_mapping: [], + severity: 'low', + severity_mapping: [], + tags: ['tag1', 'tag2'], + rule_name_override: undefined, + threat_indicator_path: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, + threat: getThreatMock(), + investigation_fields: [], + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with investigation_fields', () => { + const mockStepData: AboutStepRule = { + ...mockData, + investigationFields: ['foo', 'bar'], + }; + const result = formatAboutStepData(mockStepData); + const expected: AboutStepRuleJson = { + author: ['Elastic'], + description: '24/7', + false_positives: ['test'], + license: 'Elastic License', + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + risk_score: 21, + risk_score_mapping: [], + severity: 'low', + severity_mapping: [], + tags: ['tag1', 'tag2'], + threat: getThreatMock(), + investigation_fields: ['foo', 'bar'], + threat_indicator_path: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object if investigation_fields includes empty string', () => { + const mockStepData: AboutStepRule = { + ...mockData, + investigationFields: [' '], + }; + const result = formatAboutStepData(mockStepData); + const expected: AboutStepRuleJson = { + author: ['Elastic'], + description: '24/7', + false_positives: ['test'], + license: 'Elastic License', + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + risk_score: 21, + risk_score_mapping: [], + severity: 'low', + severity_mapping: [], + tags: ['tag1', 'tag2'], + threat: getThreatMock(), + investigation_fields: [], + threat_indicator_path: undefined, + timestamp_override: undefined, + timestamp_override_fallback_disabled: undefined, }; expect(result).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts index ea466771bf8ae..c3cb11e907a14 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts @@ -485,6 +485,7 @@ export const formatAboutStepData = ( const { author, falsePositives, + investigationFields, references, riskScore, severity, @@ -524,6 +525,7 @@ export const formatAboutStepData = ( : {}), false_positives: falsePositives.filter((item) => !isEmpty(item)), references: references.filter((item) => !isEmpty(item)), + investigation_fields: investigationFields.filter((item) => !isEmpty(item.trim())), risk_score: riskScore.value, risk_score_mapping: riskScore.isMappingChecked ? riskScore.mapping.filter((m) => m.field != null && m.field !== '') diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx index 2e173812b0109..6150cb3b6d0e8 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx @@ -662,6 +662,42 @@ describe('When the add exception modal is opened', () => { expect(getByTestId('entryType')).toHaveTextContent('match'); expect(getByTestId('entryValue')).toHaveTextContent('test/path'); }); + + it('should include rule defined custom highlighted fields', () => { + const wrapper = render( + (() => ( + <TestProviders> + <AddExceptionFlyout + rules={[ + { + ...getRulesSchemaMock(), + investigation_fields: ['foo.bar'], + exceptions_list: [], + } as Rule, + ]} + isBulkAction={false} + alertData={{ ...alertDataMock, foo: { bar: 'blob' } } as AlertData} + isAlertDataLoading={false} + alertStatus="open" + isEndpointItem={false} + showAlertCloseOptions + onCancel={jest.fn()} + onConfirm={jest.fn()} + /> + </TestProviders> + ))() + ); + const { getByTestId, getAllByTestId } = wrapper; + expect(getByTestId('alertExceptionBuilder')).toBeInTheDocument(); + expect(getAllByTestId('entryField')[0]).toHaveTextContent('foo.bar'); + expect(getAllByTestId('entryOperator')[0]).toHaveTextContent('included'); + expect(getAllByTestId('entryType')[0]).toHaveTextContent('match'); + expect(getAllByTestId('entryValue')[0]).toHaveTextContent('blob'); + expect(getAllByTestId('entryField')[1]).toHaveTextContent('file.path'); + expect(getAllByTestId('entryOperator')[1]).toHaveTextContent('included'); + expect(getAllByTestId('entryType')[1]).toHaveTextContent('match'); + expect(getAllByTestId('entryValue')[1]).toHaveTextContent('test/path'); + }); }); describe('bulk closeable alert data is passed in', () => { let wrapper: ReactWrapper; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx index 41bcefebeb191..d7081f195fefc 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx @@ -346,6 +346,9 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({ const populatedException = getPrepopulatedRuleExceptionWithHighlightFields({ alertData, exceptionItemName, + // With "rule_default" type, there is only ever one rule associated. + // That is why it's ok to pull just the first item from rules array here. + ruleCustomHighlightedFields: rules?.[0]?.investigation_fields ?? [], }); if (populatedException) { setComment(i18n.ADD_RULE_EXCEPTION_FROM_ALERT_COMMENT(alertData._id)); @@ -354,7 +357,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({ } } } - }, [listType, exceptionItemName, alertData, setInitialExceptionItems, setComment]); + }, [listType, exceptionItemName, alertData, rules, setInitialExceptionItems, setComment]); const osTypesSelection = useMemo((): OsTypeArray => { return hasAlertData ? retrieveAlertOsTypes(alertData) : selectedOs ? [...selectedOs] : []; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx index 33b1a53ad1b15..e7a3d40dd9ad7 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx @@ -1560,6 +1560,27 @@ describe('Exception helpers', () => { }, { field: 'process.name', operator: 'included', type: 'match', value: 'malware writer' }, ]; + const expectedExceptionEntriesWithCustomHighlightedFields = [ + { + field: 'event.type', + operator: 'included', + type: 'match', + value: 'creation', + }, + { + field: 'agent.id', + operator: 'included', + type: 'match', + value: 'f4f86e7c-29bd-4655-b7d0-a3d08ad0c322', + }, + { + field: 'process.executable', + operator: 'included', + type: 'match', + value: 'C:/malware.exe', + }, + { field: 'process.name', operator: 'included', type: 'match', value: 'malware writer' }, + ]; const entriesWithMatchAny = { field: 'Endpoint.capabilities', operator, @@ -1739,12 +1760,12 @@ describe('Exception helpers', () => { }, ]; it('should return the highlighted fields correctly when eventCode, eventCategory and RuleType are in the alertData', () => { - const res = getAlertHighlightedFields(alertData); + const res = getAlertHighlightedFields(alertData, []); expect(res).toEqual(allHighlightFields); }); it('should return highlighted fields without the file.Ext.quarantine_path when "event.code" is not in the alertData', () => { const alertDataWithoutEventCode = { ...alertData, 'event.code': null }; - const res = getAlertHighlightedFields(alertDataWithoutEventCode); + const res = getAlertHighlightedFields(alertDataWithoutEventCode, []); expect(res).toEqual([ ...baseGeneratedAlertHighlightedFields, { @@ -1763,7 +1784,7 @@ describe('Exception helpers', () => { }); it('should return highlighted fields without the file and process props when "event.category" is not in the alertData', () => { const alertDataWithoutEventCategory = { ...alertData, 'event.category': null }; - const res = getAlertHighlightedFields(alertDataWithoutEventCategory); + const res = getAlertHighlightedFields(alertDataWithoutEventCategory, []); expect(res).toEqual([ ...baseGeneratedAlertHighlightedFields, { @@ -1775,7 +1796,7 @@ describe('Exception helpers', () => { }); it('should return the process highlighted fields correctly when eventCategory is an array', () => { const alertDataEventCategoryProcessArray = { ...alertData, 'event.category': ['process'] }; - const res = getAlertHighlightedFields(alertDataEventCategoryProcessArray); + const res = getAlertHighlightedFields(alertDataEventCategoryProcessArray, []); expect(res).not.toEqual( expect.arrayContaining([ { id: 'file.name' }, @@ -1793,20 +1814,20 @@ describe('Exception helpers', () => { }); it('should return all highlighted fields even when the "kibana.alert.rule.type" is not in the alertData', () => { const alertDataWithoutEventCategory = { ...alertData, 'kibana.alert.rule.type': null }; - const res = getAlertHighlightedFields(alertDataWithoutEventCategory); + const res = getAlertHighlightedFields(alertDataWithoutEventCategory, []); expect(res).toEqual(allHighlightFields); }); it('should return all highlighted fields when there are no fields to be filtered out', () => { jest.mock('./highlighted_fields_config', () => ({ highlightedFieldsPrefixToExclude: [] })); - const res = getAlertHighlightedFields(alertData); + const res = getAlertHighlightedFields(alertData, []); expect(res).toEqual(allHighlightFields); }); it('should exclude the "agent.id" from highlighted fields when agent.type is not "endpoint"', () => { jest.mock('./highlighted_fields_config', () => ({ highlightedFieldsPrefixToExclude: [] })); const alertDataWithoutAgentType = { ...alertData, agent: { ...alertData.agent, type: '' } }; - const res = getAlertHighlightedFields(alertDataWithoutAgentType); + const res = getAlertHighlightedFields(alertDataWithoutAgentType, []); expect(res).toEqual(allHighlightFields.filter((field) => field.id !== AGENT_ID)); }); @@ -1814,10 +1835,14 @@ describe('Exception helpers', () => { jest.mock('./highlighted_fields_config', () => ({ highlightedFieldsPrefixToExclude: [] })); const alertDataWithoutRuleUUID = { ...alertData, 'kibana.alert.rule.uuid': '' }; - const res = getAlertHighlightedFields(alertDataWithoutRuleUUID); + const res = getAlertHighlightedFields(alertDataWithoutRuleUUID, []); expect(res).toEqual(allHighlightFields.filter((field) => field.id !== AGENT_ID)); }); + it('should include custom highlighted fields', () => { + const res = getAlertHighlightedFields(alertData, ['event.type']); + expect(res).toEqual([{ id: 'event.type' }, ...allHighlightFields]); + }); }); describe('getPrepopulatedRuleExceptionWithHighlightFields', () => { it('should not create any exception and return null if there are no highlighted fields', () => { @@ -1826,6 +1851,7 @@ describe('Exception helpers', () => { const res = getPrepopulatedRuleExceptionWithHighlightFields({ alertData: defaultAlertData, exceptionItemName: '', + ruleCustomHighlightedFields: [], }); expect(res).toBe(null); }); @@ -1835,6 +1861,7 @@ describe('Exception helpers', () => { const res = getPrepopulatedRuleExceptionWithHighlightFields({ alertData: defaultAlertData, exceptionItemName: '', + ruleCustomHighlightedFields: [], }); expect(res).toBe(null); }); @@ -1842,6 +1869,7 @@ describe('Exception helpers', () => { const exception = getPrepopulatedRuleExceptionWithHighlightFields({ alertData, exceptionItemName: name, + ruleCustomHighlightedFields: [], }); expect(exception?.entries).toEqual( @@ -1849,6 +1877,21 @@ describe('Exception helpers', () => { ); expect(exception?.name).toEqual(name); }); + it('should create a new exception and populate its entries with the custom highlighted fields', () => { + const exception = getPrepopulatedRuleExceptionWithHighlightFields({ + alertData, + exceptionItemName: name, + ruleCustomHighlightedFields: ['event.type'], + }); + + expect(exception?.entries).toEqual( + expectedExceptionEntriesWithCustomHighlightedFields.map((entry) => ({ + ...entry, + id: '123', + })) + ); + expect(exception?.name).toEqual(name); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx index 3235276e650a2..617dd2901363c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx @@ -908,11 +908,13 @@ export const buildExceptionEntriesFromAlertFields = ({ export const getPrepopulatedRuleExceptionWithHighlightFields = ({ alertData, exceptionItemName, + ruleCustomHighlightedFields, }: { alertData: AlertData; exceptionItemName: string; + ruleCustomHighlightedFields: string[]; }): ExceptionsBuilderExceptionItem | null => { - const highlightedFields = getAlertHighlightedFields(alertData); + const highlightedFields = getAlertHighlightedFields(alertData, ruleCustomHighlightedFields); if (!highlightedFields.length) return null; const exceptionEntries = buildExceptionEntriesFromAlertFields({ highlightedFields, alertData }); @@ -951,11 +953,13 @@ export const filterHighlightedFields = ( * * Alert field ids filters * @param alertData The Alert data object */ -export const getAlertHighlightedFields = (alertData: AlertData): EventSummaryField[] => { +export const getAlertHighlightedFields = ( + alertData: AlertData, + ruleCustomHighlightedFields: string[] +): EventSummaryField[] => { const eventCategory = get(alertData, EVENT_CATEGORY); const eventCode = get(alertData, EVENT_CODE); const eventRuleType = get(alertData, KIBANA_ALERT_RULE_TYPE); - const eventCategories = { primaryEventCategory: Array.isArray(eventCategory) ? eventCategory[0] : eventCategory, allEventCategories: [eventCategory], @@ -965,6 +969,7 @@ export const getAlertHighlightedFields = (alertData: AlertData): EventSummaryFie eventCategories, eventCode, eventRuleType, + highlightedFieldsOverride: ruleCustomHighlightedFields, }); return filterHighlightedFields(fieldsToDisplay, highlightedFieldsPrefixToExclude, alertData); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts index 35441d926402b..63a6b70356ea3 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/logic/types.ts @@ -73,6 +73,7 @@ import { TimestampField, TimestampOverride, TimestampOverrideFallbackDisabled, + RuleCustomHighlightedFieldArray, } from '../../../../common/api/detection_engine/model/rule_schema'; import type { @@ -201,6 +202,7 @@ export const RuleSchema = t.intersection([ version: RuleVersion, execution_summary: RuleExecutionSummary, alert_suppression: AlertSuppression, + investigation_fields: RuleCustomHighlightedFieldArray, }), ]); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts index 487052fcbf2ef..d525a894a3af4 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts @@ -194,6 +194,7 @@ export const mockAboutStepRule = (): AboutStepRule => ({ tags: ['tag1', 'tag2'], threat: getThreatMock(), note: '# this is some markdown documentation', + investigationFields: ['foo', 'bar'], }); export const mockActionsStepRule = (enabled = false): ActionsStepRule => ({ diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index c2d477d5a87c6..0e1b32c3c77d9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -15,6 +15,7 @@ import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { get } from 'lodash/fp'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; import { TableId } from '@kbn/securitysolution-data-table'; +import { useRuleWithFallback } from '../../../../detection_engine/rule_management/logic/use_rule_with_fallback'; import { DEFAULT_ACTION_BUTTON_WIDTH } from '../../../../common/components/header_actions'; import { isActiveTimeline } from '../../../../helpers'; import { useOsqueryContextActionItem } from '../../osquery/use_osquery_context_action_item'; @@ -384,6 +385,7 @@ export const AddExceptionFlyoutWrapper: React.FC<AddExceptionFlyoutWrapperProps> alertStatus, }) => { const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); + const { rule: maybeRule, loading: isRuleLoading } = useRuleWithFallback(ruleId); const { loading: isLoadingAlertData, data } = useQueryAlerts<EcsHit, {}>({ query: buildGetAlertByIdQuery(eventId), @@ -429,32 +431,13 @@ export const AddExceptionFlyoutWrapper: React.FC<AddExceptionFlyoutWrapperProps> return ruleDataViewId; }, [enrichedAlert, ruleDataViewId]); - // TODO: Do we want to notify user when they are working off of an older version of a rule - // if they select to add an exception from an alert referencing an older rule version? const memoRule = useMemo(() => { - if (enrichedAlert != null && enrichedAlert['kibana.alert.rule.parameters'] != null) { - return [ - { - ...enrichedAlert['kibana.alert.rule.parameters'], - id: ruleId, - rule_id: ruleRuleId, - name: ruleName, - index: memoRuleIndices, - data_view_id: memoDataViewId, - }, - ] as Rule[]; + if (maybeRule) { + return [maybeRule]; } - return [ - { - id: ruleId, - rule_id: ruleRuleId, - name: ruleName, - index: memoRuleIndices, - data_view_id: memoDataViewId, - }, - ] as Rule[]; - }, [enrichedAlert, memoDataViewId, memoRuleIndices, ruleId, ruleName, ruleRuleId]); + return null; + }, [maybeRule]); const isLoading = (isLoadingAlertData && isSignalIndexLoading) || @@ -466,7 +449,7 @@ export const AddExceptionFlyoutWrapper: React.FC<AddExceptionFlyoutWrapperProps> rules={memoRule} isEndpointItem={exceptionListType === ExceptionListTypeEnum.ENDPOINT} alertData={enrichedAlert} - isAlertDataLoading={isLoading} + isAlertDataLoading={isLoading || isRuleLoading} alertStatus={alertStatus} isBulkAction={false} showAlertCloseOptions diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx index 8ea4e21509f5d..ecfcb5c981235 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx @@ -27,6 +27,7 @@ import { buildUrlsDescription, buildNoteDescription, buildRuleTypeDescription, + buildHighlightedFieldsOverrideDescription, } from './helpers'; import type { ListItems } from './types'; @@ -508,4 +509,28 @@ describe('helpers', () => { expect(result.description).toEqual('Indicator Match'); }); }); + + describe('buildHighlightedFieldsOverrideDescription', () => { + test('returns ListItem with passed in label and custom highlighted fields', () => { + const result: ListItems[] = buildHighlightedFieldsOverrideDescription('Test label', [ + 'foo', + 'bar', + ]); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + const element = wrapper.find( + '[data-test-subj="customHighlightedFieldsStringArrayDescriptionBadgeItem"]' + ); + + expect(result[0].title).toEqual('Test label'); + expect(element.exists()).toBeTruthy(); + expect(element.at(0).text()).toEqual('foo'); + expect(element.at(1).text()).toEqual('bar'); + }); + + test('returns empty array if passed in note is empty string', () => { + const result: ListItems[] = buildHighlightedFieldsOverrideDescription('Test label', []); + + expect(result).toHaveLength(0); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index d3d79b76b1d02..57fe4f72fd19f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -27,15 +27,13 @@ import { FieldIcon } from '@kbn/react-field'; import type { ThreatMapping, Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { FilterBadgeGroup } from '@kbn/unified-search-plugin/public'; +import type { RequiredFieldArray } from '../../../../../common/api/detection_engine/model/rule_schema/common_attributes'; import { MATCHES, AND, OR } from '../../../../common/components/threat_match/translations'; import type { EqlOptionsSelected } from '../../../../../common/search_strategy'; import { assertUnreachable } from '../../../../../common/utility_types'; import * as i18nSeverity from '../severity_mapping/translations'; import * as i18nRiskScore from '../risk_score_mapping/translations'; -import type { - RequiredFieldArray, - Threshold, -} from '../../../../../common/api/detection_engine/model/rule_schema'; +import type { Threshold } from '../../../../../common/api/detection_engine/model/rule_schema'; import * as i18n from './translations'; import type { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './types'; @@ -201,6 +199,38 @@ export const buildUnorderedListArrayDescription = ( return []; }; +export const buildHighlightedFieldsOverrideDescription = ( + label: string, + values: string[] +): ListItems[] => { + if (isEmpty(values)) { + return []; + } + const description = ( + <EuiFlexGroup responsive={false} gutterSize="xs" wrap> + {values.map((val: string) => + isEmpty(val) ? null : ( + <EuiFlexItem grow={false} key={`${label}-${val}`}> + <EuiBadgeWrap + data-test-subj="customHighlightedFieldsStringArrayDescriptionBadgeItem" + color="hollow" + > + {val} + </EuiBadgeWrap> + </EuiFlexItem> + ) + )} + </EuiFlexGroup> + ); + + return [ + { + title: label, + description, + }, + ]; +}; + export const buildStringArrayDescription = ( label: string, field: string, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx index a2206c9c562e7..5ee4bb4bb1f47 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx @@ -262,7 +262,7 @@ describe('description_step', () => { mockLicenseService ); - expect(result.length).toEqual(11); + expect(result.length).toEqual(12); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index d4f7c026d369e..8d7c21e386e40 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -14,11 +14,11 @@ import type { ThreatMapping, Threats, Type } from '@kbn/securitysolution-io-ts-a import type { DataViewBase, Filter } from '@kbn/es-query'; import { FilterStateStore } from '@kbn/es-query'; import { FilterManager } from '@kbn/data-plugin/public'; -import { buildRelatedIntegrationsDescription } from '../related_integrations/integrations_description'; import type { RelatedIntegrationArray, RequiredFieldArray, -} from '../../../../../common/api/detection_engine/model/rule_schema'; +} from '../../../../../common/api/detection_engine/model/rule_schema/common_attributes'; +import { buildRelatedIntegrationsDescription } from '../related_integrations/integrations_description'; import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; import type { EqlOptionsSelected } from '../../../../../common/search_strategy'; import { useKibana } from '../../../../common/lib/kibana'; @@ -47,6 +47,7 @@ import { buildAlertSuppressionDescription, buildAlertSuppressionWindowDescription, buildAlertSuppressionMissingFieldsDescription, + buildHighlightedFieldsOverrideDescription, } from './helpers'; import * as i18n from './translations'; import { buildMlJobsDescription } from './build_ml_jobs_description'; @@ -261,6 +262,9 @@ export const getDescriptionItem = ( } else if (field === 'falsePositives') { const values: string[] = get(field, data); return buildUnorderedListArrayDescription(label, field, values); + } else if (field === 'investigationFields') { + const values: string[] = get(field, data); + return buildHighlightedFieldsOverrideDescription(label, values); } else if (field === 'riskScore') { const values: AboutStepRiskScore = get(field, data); return buildRiskScoreDescription(values); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/group_by_fields/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/multi_select_fields/index.tsx similarity index 62% rename from x-pack/plugins/security_solution/public/detections/components/rules/group_by_fields/index.tsx rename to x-pack/plugins/security_solution/public/detections/components/rules/multi_select_fields/index.tsx index 579167c73f4cb..9dbd49a2f4f07 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/group_by_fields/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/multi_select_fields/index.tsx @@ -11,40 +11,44 @@ import { EuiToolTip } from '@elastic/eui'; import type { DataViewFieldBase } from '@kbn/es-query'; import type { FieldHook } from '../../../../shared_imports'; import { Field } from '../../../../shared_imports'; -import { GROUP_BY_FIELD_PLACEHOLDER, GROUP_BY_FIELD_LICENSE_WARNING } from './translations'; +import { FIELD_PLACEHOLDER } from './translations'; -interface GroupByFieldsProps { +interface MultiSelectAutocompleteProps { browserFields: DataViewFieldBase[]; isDisabled: boolean; field: FieldHook; + fullWidth?: boolean; + disabledText?: string; } const FIELD_COMBO_BOX_WIDTH = 410; -const fieldDescribedByIds = 'detectionEngineStepDefineRuleGroupByField'; +const fieldDescribedByIds = 'detectionEngineMultiSelectAutocompleteField'; -export const GroupByComponent: React.FC<GroupByFieldsProps> = ({ +export const MultiSelectAutocompleteComponent: React.FC<MultiSelectAutocompleteProps> = ({ browserFields, + disabledText, isDisabled, field, -}: GroupByFieldsProps) => { + fullWidth = false, +}: MultiSelectAutocompleteProps) => { const fieldEuiFieldProps = useMemo( () => ({ fullWidth: true, noSuggestions: false, options: browserFields.map((browserField) => ({ label: browserField.name })), - placeholder: GROUP_BY_FIELD_PLACEHOLDER, + placeholder: FIELD_PLACEHOLDER, onCreateOption: undefined, - style: { width: `${FIELD_COMBO_BOX_WIDTH}px` }, + ...(fullWidth ? {} : { style: { width: `${FIELD_COMBO_BOX_WIDTH}px` } }), isDisabled, }), - [browserFields, isDisabled] + [browserFields, isDisabled, fullWidth] ); const fieldComponent = ( <Field field={field} idAria={fieldDescribedByIds} euiFieldProps={fieldEuiFieldProps} /> ); return isDisabled ? ( - <EuiToolTip position="right" content={GROUP_BY_FIELD_LICENSE_WARNING}> + <EuiToolTip position="right" content={disabledText}> {fieldComponent} </EuiToolTip> ) : ( @@ -52,4 +56,4 @@ export const GroupByComponent: React.FC<GroupByFieldsProps> = ({ ); }; -export const GroupByFields = React.memo(GroupByComponent); +export const MultiSelectFieldsAutocomplete = React.memo(MultiSelectAutocompleteComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/group_by_fields/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/multi_select_fields/translations.ts similarity index 54% rename from x-pack/plugins/security_solution/public/detections/components/rules/group_by_fields/translations.ts rename to x-pack/plugins/security_solution/public/detections/components/rules/multi_select_fields/translations.ts index d0df6a7320015..4dd83c607ef05 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/group_by_fields/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/multi_select_fields/translations.ts @@ -7,16 +7,9 @@ import { i18n } from '@kbn/i18n'; -export const GROUP_BY_FIELD_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.placeholderText', +export const FIELD_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.multiSelectFields.placeholderText', { defaultMessage: 'Select a field', } ); - -export const GROUP_BY_FIELD_LICENSE_WARNING = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.licenseWarning', - { - defaultMessage: 'Alert suppression is enabled with Platinum license or above', - } -); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts index 3ae5441d060d0..f8025537f3f17 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts @@ -26,6 +26,7 @@ export const stepAboutDefaultValue: AboutStepRule = { riskScore: { value: 21, mapping: [], isMappingChecked: false }, references: [''], falsePositives: [''], + investigationFields: [], license: '', ruleNameOverride: '', tags: [], diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx index 096de7b836a0b..a9805bf71d3ce 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx @@ -274,6 +274,7 @@ describe('StepAboutRuleComponent', () => { technique: [], }, ], + investigationFields: [], }; await act(async () => { @@ -333,6 +334,7 @@ describe('StepAboutRuleComponent', () => { technique: [], }, ], + investigationFields: [], }; await act(async () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 7052c29ce7881..622153160c9f4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -33,6 +33,7 @@ import { useFetchIndex } from '../../../../common/containers/source'; import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; import { useKibana } from '../../../../common/lib/kibana'; import { useRuleIndices } from '../../../../detection_engine/rule_management/logic/use_rule_indices'; +import { MultiSelectFieldsAutocomplete } from '../multi_select_fields'; const CommonUseField = getUseField({ component: Field }); @@ -237,6 +238,16 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({ }} /> <EuiSpacer size="l" /> + <UseField + path="investigationFields" + component={MultiSelectFieldsAutocomplete} + componentProps={{ + browserFields: indexPattern.fields, + isDisabled: isLoading || indexPatternLoading, + fullWidth: true, + }} + /> + <EuiSpacer size="l" /> <UseField path="note" component={MarkdownEditorForm} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx index e4be41d5d8519..1d23169dec241 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/schema.tsx @@ -160,6 +160,16 @@ export const schema: FormSchema<AboutStepRule> = { ), labelAppend: OptionalFieldLabel, }, + investigationFields: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldCustomHighlightedFieldsLabel', + { + defaultMessage: 'Custom highlighted fields', + } + ), + labelAppend: OptionalFieldLabel, + }, license: { type: FIELD_TYPES.TEXT, label: i18n.translate( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts index f1841430f03ff..007cf4d9dd4c6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/translations.ts @@ -28,6 +28,13 @@ export const ADD_FALSE_POSITIVE = i18n.translate( } ); +export const ADD_CUSTOM_HIGHLIGHTED_FIELD = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.addCustomHighlightedFieldDescription', + { + defaultMessage: 'Add a custom highlighted field', + } +); + export const GLOBAL_ENDPOINT_EXCEPTION_LIST = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRuleForm.endpointExceptionListLabel', { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 779fe5f35a820..a0da91e660566 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -75,7 +75,7 @@ import { NewTermsFields } from '../new_terms_fields'; import { ScheduleItem } from '../schedule_item_form'; import { DocLink } from '../../../../common/components/links_to_docs/doc_link'; import { defaultCustomQuery } from '../../../pages/detection_engine/rules/utils'; -import { GroupByFields } from '../group_by_fields'; +import { MultiSelectFieldsAutocomplete } from '../multi_select_fields'; import { useLicense } from '../../../../common/hooks/use_license'; import { minimumLicenseForSuppression, @@ -752,9 +752,10 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({ > <UseField path="groupByFields" - component={GroupByFields} + component={MultiSelectFieldsAutocomplete} componentProps={{ browserFields: termsAggregationFields, + disabledText: i18n.GROUP_BY_FIELD_LICENSE_WARNING, isDisabled: !license.isAtLeast(minimumLicenseForSuppression) && groupByFields?.length === 0, }} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx index 12b82d427edb1..5afe30b6e046d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx @@ -171,3 +171,10 @@ export const ALERT_SUPPRESSION_MISSING_FIELDS_DO_NOT_SUPPRESS_OPTION = i18n.tran defaultMessage: 'Do not suppress alerts for events with missing fields', } ); + +export const GROUP_BY_FIELD_LICENSE_WARNING = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.licenseWarning', + { + defaultMessage: 'Alert suppression is enabled with Platinum license or above', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index 55a7e97a99edd..517a30df5acf0 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -144,6 +144,7 @@ describe('rule helpers', () => { threat: getThreatMock(), timestampOverride: 'event.ingested', timestampOverrideFallbackDisabled: false, + investigationFields: [], }; const scheduleRuleStepData = { from: '0s', interval: '5m' }; const ruleActionsStepData = { @@ -181,6 +182,14 @@ describe('rule helpers', () => { expect(result.note).toEqual(''); }); + + test('returns customHighlightedField as empty array if property does not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.investigation_fields; + const result: AboutStepRule = getAboutStepsData(mockedRule, false); + + expect(result.investigationFields).toEqual([]); + }); }); describe('determineDetailsValue', () => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index e80ec9591c30c..7ef797d64eeb0 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -200,6 +200,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu severity, false_positives: falsePositives, risk_score: riskScore, + investigation_fields: investigationFields, tags, threat, threat_indicator_path: threatIndicatorPath, @@ -230,6 +231,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu isMappingChecked: riskScoreMapping.length > 0, }, falsePositives, + investigationFields: investigationFields ?? [], threat: threat as Threats, threatIndicatorPath, }; @@ -343,6 +345,7 @@ const commonRuleParamsKeys = [ 'name', 'description', 'false_positives', + 'investigation_fields', 'rule_id', 'max_signals', 'risk_score', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index 4232481eee861..c5b98b1bfd39d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -89,6 +89,7 @@ export interface AboutStepRule { riskScore: AboutStepRiskScore; references: string[]; falsePositives: string[]; + investigationFields: string[]; license: string; ruleNameOverride: string; tags: string[]; @@ -238,6 +239,7 @@ export interface AboutStepRuleJson { timestamp_override?: TimestampOverride; timestamp_override_fallback_disabled?: boolean; note?: string; + investigation_fields?: string[]; } export interface ScheduleStepRuleJson { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts index 791d8ae322260..5f54eea162c77 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts @@ -83,6 +83,7 @@ export const stepAboutDefaultValue: AboutStepRule = { isBuildingBlock: false, severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false }, riskScore: { value: 21, mapping: [], isMappingChecked: false }, + investigationFields: [], references: [''], falsePositives: [''], license: '', diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details.test.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details.test.tsx index ee641e5c69184..6b52bcf921b0a 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details.test.tsx @@ -55,6 +55,7 @@ const contextValue: LeftPanelContext = { scopeId: '', browserFields: null, searchHit: undefined, + investigationFields: [], }; const renderCorrelationDetails = () => { diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details_alerts_table.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details_alerts_table.tsx index 79ce5e8f4bf0d..28d05d3a08d70 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/correlations_details_alerts_table.tsx @@ -10,6 +10,7 @@ import { type Criteria, EuiBasicTable, formatDate, EuiEmptyPrompt } from '@elast import { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; import { isRight } from 'fp-ts/lib/Either'; +import { ALERT_REASON, ALERT_RULE_NAME } from '@kbn/rule-data-utils'; import { SeverityBadge } from '../../../detections/components/rules/severity_badge'; import { usePaginatedAlerts } from '../hooks/use_paginated_alerts'; import { ERROR_MESSAGE, ERROR_TITLE } from '../../shared/translations'; @@ -26,12 +27,12 @@ export const columns = [ render: (value: string) => formatDate(value, TIMESTAMP_DATE_FORMAT), }, { - field: 'kibana.alert.rule.name', + field: ALERT_RULE_NAME, name: i18n.CORRELATIONS_RULE_COLUMN_TITLE, truncateText: true, }, { - field: 'kibana.alert.reason', + field: ALERT_REASON, name: i18n.CORRELATIONS_REASON_COLUMN_TITLE, truncateText: true, }, diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx index 731bfeda95712..8382e13e6fcc6 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/host_details.tsx @@ -12,12 +12,12 @@ import { EuiTitle, EuiSpacer, EuiInMemoryTable, - EuiHorizontalRule, EuiText, EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiIcon, + EuiPanel, } from '@elastic/eui'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { ExpandablePanel } from '../../shared/components/expandable_panel'; @@ -207,7 +207,7 @@ export const HostDetails: React.FC<HostDetailsProps> = ({ hostName, timestamp }) return ( <> <EuiTitle size="xs"> - <h4>{i18n.HOSTS_TITLE}</h4> + <h4>{i18n.HOST_TITLE}</h4> </EuiTitle> <EuiSpacer size="s" /> <ExpandablePanel @@ -250,41 +250,43 @@ export const HostDetails: React.FC<HostDetailsProps> = ({ hostName, timestamp }) /> )} </AnomalyTableProvider> - <EuiHorizontalRule margin="m" /> - <EuiFlexGroup direction="row" gutterSize="xs" alignItems="center"> - <EuiFlexItem grow={false}> - <EuiTitle size="xxs"> - <h5>{i18n.RELATED_USERS_TITLE}</h5> - </EuiTitle> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiToolTip content={i18n.RELATED_USERS_TOOL_TIP}> - <EuiIcon color="subdued" type="iInCircle" className="eui-alignTop" /> - </EuiToolTip> - </EuiFlexItem> - </EuiFlexGroup> <EuiSpacer size="s" /> - <RelatedUsersManage - id={relatedUsersQueryId} - inspect={inspectRelatedUsers} - loading={isRelatedUsersLoading} - setQuery={setQuery} - deleteQuery={deleteQuery} - refetch={refetchRelatedUsers} - > - <EuiInMemoryTable - columns={relatedUsersColumns} - items={relatedUsers} + <EuiPanel hasBorder={true}> + <EuiFlexGroup direction="row" gutterSize="xs" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiTitle size="xxs"> + <h5>{i18n.RELATED_USERS_TITLE}</h5> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiToolTip content={i18n.RELATED_USERS_TOOL_TIP}> + <EuiIcon color="subdued" type="iInCircle" className="eui-alignTop" /> + </EuiToolTip> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="s" /> + <RelatedUsersManage + id={relatedUsersQueryId} + inspect={inspectRelatedUsers} loading={isRelatedUsersLoading} - data-test-subj={HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID} - pagination={pagination} - /> - <InspectButton - queryId={relatedUsersQueryId} - title={i18n.RELATED_USERS_TITLE} - inspectIndex={0} - /> - </RelatedUsersManage> + setQuery={setQuery} + deleteQuery={deleteQuery} + refetch={refetchRelatedUsers} + > + <EuiInMemoryTable + columns={relatedUsersColumns} + items={relatedUsers} + loading={isRelatedUsersLoading} + data-test-subj={HOST_DETAILS_RELATED_USERS_TABLE_TEST_ID} + pagination={pagination} + /> + <InspectButton + queryId={relatedUsersQueryId} + title={i18n.RELATED_USERS_TITLE} + inspectIndex={0} + /> + </RelatedUsersManage> + </EuiPanel> </ExpandablePanel> </> ); diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx index 70fc7554e64cd..504d2d2c9232f 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx @@ -108,7 +108,8 @@ const columns: Array<EuiBasicTableColumn<unknown>> = [ * Prevalence table displayed in the document details expandable flyout left section under the Insights tab */ export const PrevalenceDetails: React.FC = () => { - const { browserFields, dataFormattedForFieldBrowser, eventId, scopeId } = useLeftPanelContext(); + const { browserFields, dataFormattedForFieldBrowser, eventId, scopeId, investigationFields } = + useLeftPanelContext(); const data = useMemo(() => { const summaryRows = getSummaryRows({ @@ -116,6 +117,7 @@ export const PrevalenceDetails: React.FC = () => { data: dataFormattedForFieldBrowser || [], eventId, scopeId, + investigationFields, isReadOnly: false, }); @@ -137,7 +139,7 @@ export const PrevalenceDetails: React.FC = () => { userPrevalence: fields, }; }); - }, [browserFields, dataFormattedForFieldBrowser, eventId, scopeId]); + }, [browserFields, investigationFields, dataFormattedForFieldBrowser, eventId, scopeId]); if (!eventId || !dataFormattedForFieldBrowser || !browserFields || !data || data.length === 0) { return ( diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts index b8c0122a7a595..a83d9911ad32c 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts @@ -21,8 +21,8 @@ export const SESSION_VIEW_ERROR_MESSAGE = i18n.translate( } ); -export const USERS_TITLE = i18n.translate('xpack.securitySolution.flyout.entities.usersTitle', { - defaultMessage: 'Users', +export const USER_TITLE = i18n.translate('xpack.securitySolution.flyout.entities.userTitle', { + defaultMessage: 'User', }); export const USERS_INFO_TITLE = i18n.translate( @@ -60,8 +60,8 @@ export const RELATED_ENTITIES_IP_COLUMN_TITLE = i18n.translate( } ); -export const HOSTS_TITLE = i18n.translate('xpack.securitySolution.flyout.entities.hostsTitle', { - defaultMessage: 'Hosts', +export const HOST_TITLE = i18n.translate('xpack.securitySolution.flyout.entities.hostTitle', { + defaultMessage: 'Host', }); export const HOSTS_INFO_TITLE = i18n.translate( diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx index ea55f811c341a..9e218c7ac94fb 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/user_details.tsx @@ -12,12 +12,12 @@ import { EuiTitle, EuiSpacer, EuiInMemoryTable, - EuiHorizontalRule, EuiText, EuiIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip, + EuiPanel, } from '@elastic/eui'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { ExpandablePanel } from '../../shared/components/expandable_panel'; @@ -208,7 +208,7 @@ export const UserDetails: React.FC<UserDetailsProps> = ({ userName, timestamp }) return ( <> <EuiTitle size="xs"> - <h4>{i18n.USERS_TITLE}</h4> + <h4>{i18n.USER_TITLE}</h4> </EuiTitle> <EuiSpacer size="s" /> <ExpandablePanel @@ -253,41 +253,43 @@ export const UserDetails: React.FC<UserDetailsProps> = ({ userName, timestamp }) /> )} </AnomalyTableProvider> - <EuiHorizontalRule margin="m" /> - <EuiFlexGroup direction="row" gutterSize="xs" alignItems="center"> - <EuiFlexItem grow={false}> - <EuiTitle size="xxs"> - <h5>{i18n.RELATED_HOSTS_TITLE}</h5> - </EuiTitle> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiToolTip content={i18n.RELATED_HOSTS_TOOL_TIP}> - <EuiIcon color="subdued" type="iInCircle" className="eui-alignTop" /> - </EuiToolTip> - </EuiFlexItem> - </EuiFlexGroup> <EuiSpacer size="s" /> - <RelatedHostsManage - id={relatedHostsQueryId} - inspect={inspectRelatedHosts} - loading={isRelatedHostLoading} - setQuery={setQuery} - deleteQuery={deleteQuery} - refetch={refetchRelatedHosts} - > - <EuiInMemoryTable - columns={relatedHostsColumns} - items={relatedHosts} + <EuiPanel hasBorder={true}> + <EuiFlexGroup direction="row" gutterSize="xs" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiTitle size="xxs"> + <h5>{i18n.RELATED_HOSTS_TITLE}</h5> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiToolTip content={i18n.RELATED_HOSTS_TOOL_TIP}> + <EuiIcon color="subdued" type="iInCircle" className="eui-alignTop" /> + </EuiToolTip> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="s" /> + <RelatedHostsManage + id={relatedHostsQueryId} + inspect={inspectRelatedHosts} loading={isRelatedHostLoading} - data-test-subj={USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID} - pagination={pagination} - /> - <InspectButton - queryId={relatedHostsQueryId} - title={i18n.RELATED_HOSTS_TITLE} - inspectIndex={0} - /> - </RelatedHostsManage> + setQuery={setQuery} + deleteQuery={deleteQuery} + refetch={refetchRelatedHosts} + > + <EuiInMemoryTable + columns={relatedHostsColumns} + items={relatedHosts} + loading={isRelatedHostLoading} + data-test-subj={USER_DETAILS_RELATED_HOSTS_TABLE_TEST_ID} + pagination={pagination} + /> + <InspectButton + queryId={relatedHostsQueryId} + title={i18n.RELATED_HOSTS_TITLE} + inspectIndex={0} + /> + </RelatedHostsManage> + </EuiPanel> </ExpandablePanel> </> ); diff --git a/x-pack/plugins/security_solution/public/flyout/left/context.tsx b/x-pack/plugins/security_solution/public/flyout/left/context.tsx index b552a830fc265..b5c4f340d5485 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/context.tsx @@ -15,12 +15,16 @@ import type { LeftPanelProps } from '.'; import type { GetFieldsData } from '../../common/hooks/use_get_fields_data'; import { useGetFieldsData } from '../../common/hooks/use_get_fields_data'; import { useTimelineEventsDetails } from '../../timelines/containers/details'; -import { getAlertIndexAlias } from '../../timelines/components/side_panel/event_details/helpers'; +import { + getAlertIndexAlias, + useBasicDataFromDetailsData, +} from '../../timelines/components/side_panel/event_details/helpers'; import { useSpaceId } from '../../common/hooks/use_space_id'; import { useRouteSpy } from '../../common/utils/route/use_route_spy'; import { SecurityPageName } from '../../../common/constants'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; import { useSourcererDataView } from '../../common/containers/sourcerer'; +import { useRuleWithFallback } from '../../detection_engine/rule_management/logic/use_rule_with_fallback'; export interface LeftPanelContext { /** @@ -51,6 +55,10 @@ export interface LeftPanelContext { * The actual raw document object */ searchHit: SearchHit | undefined; + /** + * User defined fields to highlight (defined on the rule) + */ + investigationFields: string[]; /** * Retrieves searchHit values for the provided field */ @@ -83,6 +91,8 @@ export const LeftPanelProvider = ({ id, indexName, scopeId, children }: LeftPane skip: !id, }); const getFieldsData = useGetFieldsData(searchHit?.fields); + const { ruleId } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); + const { rule: maybeRule } = useRuleWithFallback(ruleId); const contextValue = useMemo( () => @@ -95,6 +105,7 @@ export const LeftPanelProvider = ({ id, indexName, scopeId, children }: LeftPane dataAsNestedObject, dataFormattedForFieldBrowser, searchHit, + investigationFields: maybeRule?.investigation_fields ?? [], getFieldsData, } : undefined, @@ -103,10 +114,11 @@ export const LeftPanelProvider = ({ id, indexName, scopeId, children }: LeftPane indexName, scopeId, sourcererDataView.browserFields, - dataFormattedForFieldBrowser, - getFieldsData, dataAsNestedObject, + dataFormattedForFieldBrowser, searchHit, + maybeRule?.investigation_fields, + getFieldsData, ] ); diff --git a/x-pack/plugins/security_solution/public/flyout/left/hooks/use_threat_intelligence_details.test.ts b/x-pack/plugins/security_solution/public/flyout/left/hooks/use_threat_intelligence_details.test.ts index 1a6387d4eb3f2..1e92ff0a6bd45 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/hooks/use_threat_intelligence_details.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/left/hooks/use_threat_intelligence_details.test.ts @@ -75,6 +75,7 @@ describe('useThreatIntelligenceDetails', () => { _index: 'testIndex', }, dataAsNestedObject: null, + investigationFields: [], }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/left/mocks/mock_context.ts b/x-pack/plugins/security_solution/public/flyout/left/mocks/mock_context.ts index 99bfc24bab50b..3569570568986 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/mocks/mock_context.ts +++ b/x-pack/plugins/security_solution/public/flyout/left/mocks/mock_context.ts @@ -47,4 +47,5 @@ export const mockContextValue: LeftPanelContext = { dataAsNestedObject: { _id: 'testId', }, + investigationFields: [], }; diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/alert_reason_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/preview/components/alert_reason_preview.test.tsx new file mode 100644 index 0000000000000..30076fd3ca1d2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/alert_reason_preview.test.tsx @@ -0,0 +1,47 @@ +/* + * 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 React from 'react'; +import { render } from '@testing-library/react'; +import { PreviewPanelContext } from '../context'; +import { mockContextValue } from '../mocks/mock_preview_panel_context'; +import { ALERT_REASON_PREVIEW_BODY_TEST_ID } from './test_ids'; +import { AlertReasonPreview } from './alert_reason_preview'; +import { ThemeProvider } from 'styled-components'; +import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock'; + +const mockTheme = getMockTheme({ eui: { euiFontSizeXS: '' } }); + +const panelContextValue = { + ...mockContextValue, +}; + +describe('<AlertReasonPreview />', () => { + it('should render alert reason preview', () => { + const { getByTestId } = render( + <PreviewPanelContext.Provider value={panelContextValue}> + <ThemeProvider theme={mockTheme}> + <AlertReasonPreview /> + </ThemeProvider> + </PreviewPanelContext.Provider> + ); + expect(getByTestId(ALERT_REASON_PREVIEW_BODY_TEST_ID)).toBeInTheDocument(); + }); + + it('should render null is dataAsNestedObject is null', () => { + const contextValue = { + ...mockContextValue, + dataAsNestedObject: null, + }; + const { queryByTestId } = render( + <PreviewPanelContext.Provider value={contextValue}> + <AlertReasonPreview /> + </PreviewPanelContext.Provider> + ); + expect(queryByTestId(ALERT_REASON_PREVIEW_BODY_TEST_ID)).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/alert_reason_preview.tsx b/x-pack/plugins/security_solution/public/flyout/preview/components/alert_reason_preview.tsx new file mode 100644 index 0000000000000..4fd912cbfbeec --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/alert_reason_preview.tsx @@ -0,0 +1,50 @@ +/* + * 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 React, { useMemo } from 'react'; +import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { ALERT_REASON_TITLE } from './translations'; +import { ALERT_REASON_PREVIEW_BODY_TEST_ID } from './test_ids'; +import { usePreviewPanelContext } from '../context'; +import { getRowRenderer } from '../../../timelines/components/timeline/body/renderers/get_row_renderer'; +import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; + +/** + * Alert reason renderer on a preview panel on top of the right section of expandable flyout + */ +export const AlertReasonPreview: React.FC = () => { + const { dataAsNestedObject } = usePreviewPanelContext(); + + const renderer = useMemo( + () => + dataAsNestedObject != null + ? getRowRenderer({ data: dataAsNestedObject, rowRenderers: defaultRowRenderers }) + : null, + [dataAsNestedObject] + ); + + if (!dataAsNestedObject || !renderer) { + return null; + } + + return ( + <EuiPanel hasShadow={false} data-test-subj={ALERT_REASON_PREVIEW_BODY_TEST_ID}> + <EuiTitle> + <h6>{ALERT_REASON_TITLE}</h6> + </EuiTitle> + <EuiSpacer size="m" /> + {renderer.renderRow({ + contextId: 'event-details', + data: dataAsNestedObject, + isDraggable: false, + scopeId: 'global', + })} + </EuiPanel> + ); +}; + +AlertReasonPreview.displayName = 'AlertReasonPreview'; diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts index 1c27fd7472fca..764ec90fd9bdf 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/test_ids.ts @@ -38,3 +38,5 @@ export const RULE_PREVIEW_LOADING_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRulePreviewLoadingSpinner'; export const RULE_PREVIEW_FOOTER_TEST_ID = 'securitySolutionDocumentDetailsFlyoutRulePreviewFooter'; export const RULE_PREVIEW_NAVIGATE_TO_RULE_TEST_ID = 'goToRuleDetails'; +export const ALERT_REASON_PREVIEW_BODY_TEST_ID = + 'securitySolutionDocumentDetailsFlyoutAlertReasonPreviewBody'; diff --git a/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts index e3f3b1fd095fb..36bfdd33ea2ca 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/preview/components/translations.ts @@ -31,3 +31,8 @@ export const RULE_PREVIEW_ACTIONS_TEXT = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.rulePreviewActionsSectionText', { defaultMessage: 'Actions' } ); + +export const ALERT_REASON_TITLE = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.alertReasonTitle', + { defaultMessage: 'Alert reason' } +); diff --git a/x-pack/plugins/security_solution/public/flyout/preview/context.tsx b/x-pack/plugins/security_solution/public/flyout/preview/context.tsx index 521303635c25d..005ef1dcdb258 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/preview/context.tsx @@ -7,11 +7,15 @@ import React, { createContext, useContext, useMemo } from 'react'; import type { DataViewBase } from '@kbn/es-query'; +import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; +import { SecurityPageName } from '@kbn/security-solution-navigation'; import type { PreviewPanelProps } from '.'; -import { useRouteSpy } from '../../common/utils/route/use_route_spy'; -import { SecurityPageName } from '../../../common/constants'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; import { useSourcererDataView } from '../../common/containers/sourcerer'; +import { useTimelineEventsDetails } from '../../timelines/containers/details'; +import { getAlertIndexAlias } from '../../timelines/components/side_panel/event_details/helpers'; +import { useSpaceId } from '../../common/hooks/use_space_id'; +import { useRouteSpy } from '../../common/utils/route/use_route_spy'; export interface PreviewPanelContext { /** @@ -34,6 +38,10 @@ export interface PreviewPanelContext { * Index pattern for rule details */ indexPattern: DataViewBase; + /** + * An object with top level fields from the ECS object + */ + dataAsNestedObject: Ecs | null; } export const PreviewPanelContext = createContext<PreviewPanelContext | undefined>(undefined); @@ -52,12 +60,21 @@ export const PreviewPanelProvider = ({ ruleId, children, }: PreviewPanelProviderProps) => { + const currentSpaceId = useSpaceId(); + const eventIndex = indexName ? getAlertIndexAlias(indexName, currentSpaceId) ?? indexName : ''; const [{ pageName }] = useRouteSpy(); const sourcererScope = pageName === SecurityPageName.detections ? SourcererScopeName.detections : SourcererScopeName.default; const sourcererDataView = useSourcererDataView(sourcererScope); + const [_, __, ___, dataAsNestedObject] = useTimelineEventsDetails({ + indexName: eventIndex, + eventId: id ?? '', + runtimeMappings: sourcererDataView.runtimeMappings, + skip: !id, + }); + const contextValue = useMemo( () => id && indexName && scopeId @@ -67,9 +84,10 @@ export const PreviewPanelProvider = ({ scopeId, ruleId: ruleId ?? '', indexPattern: sourcererDataView.indexPattern, + dataAsNestedObject, } : undefined, - [id, indexName, scopeId, ruleId, sourcererDataView.indexPattern] + [id, indexName, scopeId, ruleId, sourcererDataView.indexPattern, dataAsNestedObject] ); return ( diff --git a/x-pack/plugins/security_solution/public/flyout/preview/index.tsx b/x-pack/plugins/security_solution/public/flyout/preview/index.tsx index 9bfefb8f257fd..db9f7bb5ba58a 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/preview/index.tsx @@ -10,8 +10,9 @@ import type { FlyoutPanelProps, PanelPath } from '@kbn/expandable-flyout'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { panels } from './panels'; -export type PreviewPanelPaths = 'rule-preview'; +export type PreviewPanelPaths = 'rule-preview' | 'alert-reason-preview'; export const RulePreviewPanel: PreviewPanelPaths = 'rule-preview'; +export const AlertReasonPreviewPanel: PreviewPanelPaths = 'alert-reason-preview'; export const PreviewPanelKey: PreviewPanelProps['key'] = 'document-details-preview'; export interface PreviewPanelProps extends FlyoutPanelProps { diff --git a/x-pack/plugins/security_solution/public/flyout/preview/mocks/mock_preview_panel_context.ts b/x-pack/plugins/security_solution/public/flyout/preview/mocks/mock_preview_panel_context.ts index 4e9f9cc43d8ba..cdfe8ab5307ba 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/mocks/mock_preview_panel_context.ts +++ b/x-pack/plugins/security_solution/public/flyout/preview/mocks/mock_preview_panel_context.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; +import { mockDataAsNestedObject } from '../../shared/mocks/mock_context'; import type { PreviewPanelContext } from '../context'; /** @@ -16,4 +18,5 @@ export const mockContextValue: PreviewPanelContext = { scopeId: 'scopeId', ruleId: '', indexPattern: { fields: [], title: 'test index' }, + dataAsNestedObject: mockDataAsNestedObject as unknown as Ecs, }; diff --git a/x-pack/plugins/security_solution/public/flyout/preview/panels.tsx b/x-pack/plugins/security_solution/public/flyout/preview/panels.tsx index b9aee26bdd577..e585c58945d9c 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/panels.tsx +++ b/x-pack/plugins/security_solution/public/flyout/preview/panels.tsx @@ -6,8 +6,9 @@ */ import React from 'react'; +import { AlertReasonPreview } from './components/alert_reason_preview'; import type { PreviewPanelPaths } from '.'; -import { RULE_PREVIEW } from './translations'; +import { ALERT_REASON_PREVIEW, RULE_PREVIEW } from './translations'; import { RulePreview } from './components/rule_preview'; import { RulePreviewFooter } from './components/rule_preview_footer'; @@ -40,4 +41,9 @@ export const panels: PreviewPanelType = [ content: <RulePreview />, footer: <RulePreviewFooter />, }, + { + id: 'alert-reason-preview', + name: ALERT_REASON_PREVIEW, + content: <AlertReasonPreview />, + }, ]; diff --git a/x-pack/plugins/security_solution/public/flyout/preview/translations.ts b/x-pack/plugins/security_solution/public/flyout/preview/translations.ts index 1db37fbb49bb8..cf359e7900cea 100644 --- a/x-pack/plugins/security_solution/public/flyout/preview/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/preview/translations.ts @@ -11,3 +11,8 @@ export const RULE_PREVIEW = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.rulePreviewPanel', { defaultMessage: 'Rule preview' } ); + +export const ALERT_REASON_PREVIEW = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.alertReasonPreviewPanel', + { defaultMessage: 'Alert reason preview' } +); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/description.stories.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/description.stories.tsx deleted file mode 100644 index 13fa592c4f7d6..0000000000000 --- a/x-pack/plugins/security_solution/public/flyout/right/components/description.stories.tsx +++ /dev/null @@ -1,92 +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 React from 'react'; -import { css } from '@emotion/react'; -import type { Story } from '@storybook/react'; -import { Description } from './description'; -import { RightPanelContext } from '../context'; - -const ruleUuid = { - category: 'kibana', - field: 'kibana.alert.rule.uuid', - values: ['123'], - originalValue: ['123'], - isObjectArray: false, -}; -const ruleDescription = { - category: 'kibana', - field: 'kibana.alert.rule.description', - values: [ - `This is a very long description of the rule. In theory. this description is long enough that it should be cut off when displayed in collapsed mode. If it isn't then there is a problem`, - ], - originalValue: ['description'], - isObjectArray: false, -}; - -export default { - component: Description, - title: 'Flyout/Description', -}; - -const wrapper = (children: React.ReactNode, panelContextValue: RightPanelContext) => ( - <RightPanelContext.Provider value={panelContextValue}> - <div - css={css` - width: 500px; - `} - > - {children} - </div> - </RightPanelContext.Provider> -); -export const Rule: Story<void> = () => { - const panelContextValue = { - dataFormattedForFieldBrowser: [ruleUuid, ruleDescription], - } as unknown as RightPanelContext; - - return wrapper(<Description />, panelContextValue); -}; - -export const Document: Story<void> = () => { - const panelContextValue = { - dataFormattedForFieldBrowser: [ - { - category: 'kibana', - field: 'kibana.alert.rule.description', - values: ['This is a description for the document.'], - originalValue: ['description'], - isObjectArray: false, - }, - ], - } as unknown as RightPanelContext; - - return wrapper(<Description />, panelContextValue); -}; - -export const EmptyDescription: Story<void> = () => { - const panelContextValue = { - dataFormattedForFieldBrowser: [ - ruleUuid, - { - category: 'kibana', - field: 'kibana.alert.rule.description', - values: [''], - originalValue: ['description'], - isObjectArray: false, - }, - ], - } as unknown as RightPanelContext; - - return wrapper(<Description />, panelContextValue); -}; - -export const Empty: Story<void> = () => { - const panelContextValue = {} as unknown as RightPanelContext; - - return wrapper(<Description />, panelContextValue); -}; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/description.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/description.test.tsx index 1cf4823d79358..f80d7c1939661 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/description.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/description.test.tsx @@ -8,14 +8,17 @@ import React from 'react'; import { render } from '@testing-library/react'; import { DESCRIPTION_TITLE_TEST_ID, RULE_SUMMARY_BUTTON_TEST_ID } from './test_ids'; -import { DOCUMENT_DESCRIPTION_TITLE, RULE_DESCRIPTION_TITLE } from './translations'; +import { + DOCUMENT_DESCRIPTION_TITLE, + PREVIEW_RULE_DETAILS, + RULE_DESCRIPTION_TITLE, +} from './translations'; import { Description } from './description'; -import { TestProviders } from '../../../common/mock'; import { RightPanelContext } from '../context'; -import { ThemeProvider } from 'styled-components'; -import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock'; - -const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } }); +import { mockGetFieldsData } from '../mocks/mock_context'; +import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; +import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; +import { PreviewPanelKey } from '../../preview'; const ruleUuid = { category: 'kibana', @@ -41,23 +44,32 @@ const ruleName = { isObjectArray: false, }; -jest.mock('../../../common/lib/kibana'); -jest.mock('../../../common/components/link_to'); +const flyoutContextValue = { + openPreviewPanel: jest.fn(), +} as unknown as ExpandableFlyoutContext; + +const panelContextValue = (dataFormattedForFieldBrowser: TimelineEventsDetailsItem[] | null) => + ({ + eventId: 'event id', + indexName: 'indexName', + scopeId: 'scopeId', + dataFormattedForFieldBrowser, + getFieldsData: mockGetFieldsData, + } as unknown as RightPanelContext); + +const renderDescription = (panelContext: RightPanelContext) => + render( + <ExpandableFlyoutContext.Provider value={flyoutContextValue}> + <RightPanelContext.Provider value={panelContext}> + <Description /> + </RightPanelContext.Provider> + </ExpandableFlyoutContext.Provider> + ); describe('<Description />', () => { it('should render the component', () => { - const panelContextValue = { - dataFormattedForFieldBrowser: [ruleUuid, ruleDescription, ruleName], - } as unknown as RightPanelContext; - - const { getByTestId } = render( - <TestProviders> - <RightPanelContext.Provider value={panelContextValue}> - <ThemeProvider theme={mockTheme}> - <Description /> - </ThemeProvider> - </RightPanelContext.Provider> - </TestProviders> + const { getByTestId } = renderDescription( + panelContextValue([ruleUuid, ruleDescription, ruleName]) ); expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument(); @@ -66,18 +78,8 @@ describe('<Description />', () => { }); it('should not render rule preview button if rule name is not available', () => { - const panelContextValue = { - dataFormattedForFieldBrowser: [ruleUuid, ruleDescription], - } as unknown as RightPanelContext; - - const { getByTestId, queryByTestId } = render( - <TestProviders> - <RightPanelContext.Provider value={panelContextValue}> - <ThemeProvider theme={mockTheme}> - <Description /> - </ThemeProvider> - </RightPanelContext.Provider> - </TestProviders> + const { getByTestId, queryByTestId } = renderDescription( + panelContextValue([ruleUuid, ruleDescription]) ); expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument(); @@ -86,21 +88,44 @@ describe('<Description />', () => { }); it('should render document title if document is not an alert', () => { - const panelContextValue = { - dataFormattedForFieldBrowser: [ruleDescription], - } as unknown as RightPanelContext; - - const { getByTestId } = render( - <TestProviders> - <RightPanelContext.Provider value={panelContextValue}> - <ThemeProvider theme={mockTheme}> - <Description /> - </ThemeProvider> - </RightPanelContext.Provider> - </TestProviders> - ); + const { getByTestId } = renderDescription(panelContextValue([ruleDescription])); expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toBeInTheDocument(); expect(getByTestId(DESCRIPTION_TITLE_TEST_ID)).toHaveTextContent(DOCUMENT_DESCRIPTION_TITLE); }); + + it('should render null if dataFormattedForFieldBrowser is null', () => { + const panelContext = { + ...panelContextValue([ruleUuid, ruleDescription, ruleName]), + dataFormattedForFieldBrowser: null, + } as unknown as RightPanelContext; + + const { container } = renderDescription(panelContext); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should open preview panel when clicking on button', () => { + const panelContext = panelContextValue([ruleUuid, ruleDescription, ruleName]); + + const { getByTestId } = renderDescription(panelContext); + + getByTestId(RULE_SUMMARY_BUTTON_TEST_ID).click(); + + expect(flyoutContextValue.openPreviewPanel).toHaveBeenCalledWith({ + id: PreviewPanelKey, + path: { tab: 'rule-preview' }, + params: { + id: panelContext.eventId, + indexName: panelContext.indexName, + scopeId: panelContext.scopeId, + banner: { + title: PREVIEW_RULE_DETAILS, + backgroundColor: 'warning', + textColor: 'warning', + }, + ruleId: ruleUuid.values[0], + }, + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/header_title.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/header_title.test.tsx index 010535ffc55c7..f4c24e64954ba 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/header_title.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/header_title.test.tsx @@ -39,7 +39,7 @@ const renderHeader = (contextValue: RightPanelContext) => <TestProvidersComponent> <ExpandableFlyoutContext.Provider value={flyoutContextValue}> <RightPanelContext.Provider value={contextValue}> - <HeaderTitle /> + <HeaderTitle flyoutIsExpandable={true} /> </RightPanelContext.Provider> </ExpandableFlyoutContext.Provider> </TestProvidersComponent> @@ -52,7 +52,7 @@ describe('<HeaderTitle />', () => { jest.mocked(useAssistant).mockReturnValue({ showAssistant: true, promptContextId: '' }); }); - it('should render mitre attack information', () => { + it('should render component', () => { const contextValue = { dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser, getFieldsData: jest.fn().mockImplementation(mockGetFieldsData), diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/header_title.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/header_title.tsx index 23bf91053220a..eca086385eb17 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/header_title.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/header_title.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { FC } from 'react'; +import type { VFC } from 'react'; import React, { memo } from 'react'; import { NewChatById } from '@kbn/elastic-assistant'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; @@ -26,10 +26,17 @@ import { PreferenceFormattedDate } from '../../../common/components/formatted_da import { FLYOUT_HEADER_TITLE_TEST_ID } from './test_ids'; import { ShareButton } from './share_button'; +export interface HeaderTitleProps { + /** + * If false, update the margin-top to compensate the fact that the expand detail button is not displayed + */ + flyoutIsExpandable: boolean; +} + /** * Document details flyout right section header */ -export const HeaderTitle: FC = memo(() => { +export const HeaderTitle: VFC<HeaderTitleProps> = memo(({ flyoutIsExpandable }) => { const { dataFormattedForFieldBrowser } = useRightPanelContext(); const { isAlert, ruleName, timestamp, alertUrl } = useBasicDataFromDetailsData( dataFormattedForFieldBrowser @@ -48,7 +55,7 @@ export const HeaderTitle: FC = memo(() => { justifyContent="flexEnd" gutterSize="none" css={css` - margin-top: -44px; + margin-top: ${flyoutIsExpandable ? '-44px' : '-28px'}; padding: 0 25px; `} > diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.test.tsx index b156006a906ec..ed9d2cd623709 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.test.tsx @@ -13,10 +13,16 @@ import { HighlightedFields } from './highlighted_fields'; import { mockDataFormattedForFieldBrowser } from '../mocks/mock_context'; import { useHighlightedFields } from '../hooks/use_highlighted_fields'; import { TestProviders } from '../../../common/mock'; +import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback'; jest.mock('../hooks/use_highlighted_fields'); +jest.mock('../../../detection_engine/rule_management/logic/use_rule_with_fallback'); describe('<HighlightedFields />', () => { + beforeEach(() => { + (useRuleWithFallback as jest.Mock).mockReturnValue({ investigation_fields: [] }); + }); + it('should render the component', () => { const panelContextValue = { dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser, diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx index a0ef290b11ea8..f02682721ee5c 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx @@ -9,6 +9,8 @@ import type { FC } from 'react'; import React from 'react'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiPanel, EuiTitle } from '@elastic/eui'; +import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback'; +import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; import { HighlightedFieldsCell } from './highlighted_fields_cell'; import { CellActionsMode, @@ -57,8 +59,13 @@ const columns: Array<EuiBasicTableColumn<UseHighlightedFieldsResult>> = [ */ export const HighlightedFields: FC = () => { const { dataFormattedForFieldBrowser } = useRightPanelContext(); + const { ruleId } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); + const { rule: maybeRule } = useRuleWithFallback(ruleId); - const highlightedFields = useHighlightedFields({ dataFormattedForFieldBrowser }); + const highlightedFields = useHighlightedFields({ + dataFormattedForFieldBrowser, + investigationFields: maybeRule?.investigation_fields ?? [], + }); if (!dataFormattedForFieldBrowser || highlightedFields.length === 0) { return null; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx index a98246c3eaa0c..b017b4b9225a4 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview.tsx @@ -23,8 +23,14 @@ import { PREVALENCE_TAB_ID } from '../../left/components/prevalence_details'; * and the SummaryPanel component for data rendering. */ export const PrevalenceOverview: FC = () => { - const { eventId, indexName, browserFields, dataFormattedForFieldBrowser, scopeId } = - useRightPanelContext(); + const { + eventId, + indexName, + browserFields, + dataFormattedForFieldBrowser, + scopeId, + investigationFields, + } = useRightPanelContext(); const { openLeftPanel } = useExpandableFlyoutContext(); const goToCorrelationsTab = useCallback(() => { @@ -46,6 +52,7 @@ export const PrevalenceOverview: FC = () => { eventId, browserFields, dataFormattedForFieldBrowser, + investigationFields, scopeId, }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/reason.stories.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/reason.stories.tsx deleted file mode 100644 index 68cb0b3a35e31..0000000000000 --- a/x-pack/plugins/security_solution/public/flyout/right/components/reason.stories.tsx +++ /dev/null @@ -1,45 +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 React from 'react'; -import type { Story } from '@storybook/react'; -import { StorybookProviders } from '../../../common/mock/storybook_providers'; -import { Reason } from './reason'; -import { RightPanelContext } from '../context'; -import { mockDataAsNestedObject, mockDataFormattedForFieldBrowser } from '../mocks/mock_context'; - -export default { - component: Reason, - title: 'Flyout/Reason', -}; - -export const Default: Story<void> = () => { - const panelContextValue = { - dataAsNestedObject: mockDataAsNestedObject, - dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser, - } as unknown as RightPanelContext; - - return ( - <StorybookProviders> - <RightPanelContext.Provider value={panelContextValue}> - <Reason /> - </RightPanelContext.Provider> - </StorybookProviders> - ); -}; - -export const Empty: Story<void> = () => { - const panelContextValue = { - dataFormattedForFieldBrowser: {}, - } as unknown as RightPanelContext; - - return ( - <RightPanelContext.Provider value={panelContextValue}> - <Reason /> - </RightPanelContext.Provider> - ); -}; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/reason.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/reason.test.tsx index b7050d1df0fa0..3ec854cfbd815 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/reason.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/reason.test.tsx @@ -7,70 +7,85 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { REASON_TITLE_TEST_ID } from './test_ids'; +import { + REASON_DETAILS_PREVIEW_BUTTON_TEST_ID, + REASON_DETAILS_TEST_ID, + REASON_TITLE_TEST_ID, +} from './test_ids'; import { Reason } from './reason'; import { RightPanelContext } from '../context'; -import { mockDataAsNestedObject, mockDataFormattedForFieldBrowser } from '../mocks/mock_context'; -import { euiDarkVars } from '@kbn/ui-theme'; -import { ThemeProvider } from 'styled-components'; +import { mockDataFormattedForFieldBrowser, mockGetFieldsData } from '../mocks/mock_context'; +import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; +import { PreviewPanelKey } from '../../preview'; +import { PREVIEW_ALERT_REASON_DETAILS } from './translations'; -describe('<Reason />', () => { - it('should render the component', () => { - const panelContextValue = { - dataAsNestedObject: mockDataAsNestedObject, - dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser, - } as unknown as RightPanelContext; +const flyoutContextValue = { + openPreviewPanel: jest.fn(), +} as unknown as ExpandableFlyoutContext; - const { getByTestId } = render( - <ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}> - <RightPanelContext.Provider value={panelContextValue}> - <Reason /> - </RightPanelContext.Provider> - </ThemeProvider> - ); +const panelContextValue = { + eventId: 'event id', + indexName: 'indexName', + scopeId: 'scopeId', + dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser, + getFieldsData: mockGetFieldsData, +} as unknown as RightPanelContext; +const renderReason = (panelContext: RightPanelContext = panelContextValue) => + render( + <ExpandableFlyoutContext.Provider value={flyoutContextValue}> + <RightPanelContext.Provider value={panelContext}> + <Reason /> + </RightPanelContext.Provider> + </ExpandableFlyoutContext.Provider> + ); + +describe('<Reason />', () => { + it('should render the component', () => { + const { getByTestId } = renderReason(); expect(getByTestId(REASON_TITLE_TEST_ID)).toBeInTheDocument(); }); it('should render null if dataFormattedForFieldBrowser is null', () => { - const panelContextValue = { - dataAsNestedObject: {}, + const panelContext = { + ...panelContextValue, + dataFormattedForFieldBrowser: null, } as unknown as RightPanelContext; - const { container } = render( - <RightPanelContext.Provider value={panelContextValue}> - <Reason /> - </RightPanelContext.Provider> - ); + const { container } = renderReason(panelContext); expect(container).toBeEmptyDOMElement(); }); - it('should render null if dataAsNestedObject is null', () => { - const panelContextValue = { - dataFormattedForFieldBrowser: [], + it('should render no reason if the field is null', () => { + const panelContext = { + ...panelContextValue, + getFieldsData: () => {}, } as unknown as RightPanelContext; - const { container } = render( - <RightPanelContext.Provider value={panelContextValue}> - <Reason /> - </RightPanelContext.Provider> - ); + const { getByTestId } = renderReason(panelContext); - expect(container).toBeEmptyDOMElement(); + expect(getByTestId(REASON_DETAILS_TEST_ID)).toBeEmptyDOMElement(); }); - it('should render null if renderer is null', () => { - const panelContextValue = { - dataAsNestedObject: {}, - dataFormattedForFieldBrowser: [], - } as unknown as RightPanelContext; - const { container } = render( - <RightPanelContext.Provider value={panelContextValue}> - <Reason /> - </RightPanelContext.Provider> - ); + it('should open preview panel when clicking on button', () => { + const { getByTestId } = renderReason(); - expect(container).toBeEmptyDOMElement(); + getByTestId(REASON_DETAILS_PREVIEW_BUTTON_TEST_ID).click(); + + expect(flyoutContextValue.openPreviewPanel).toHaveBeenCalledWith({ + id: PreviewPanelKey, + path: { tab: 'alert-reason-preview' }, + params: { + id: panelContextValue.eventId, + indexName: panelContextValue.indexName, + scopeId: panelContextValue.scopeId, + banner: { + title: PREVIEW_ALERT_REASON_DETAILS, + backgroundColor: 'warning', + textColor: 'warning', + }, + }, + }); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/reason.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/reason.tsx index b6633ac42c46b..b356809917973 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/reason.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/reason.tsx @@ -6,31 +6,71 @@ */ import type { FC } from 'react'; -import React, { useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; -import { REASON_DETAILS_TEST_ID, REASON_TITLE_TEST_ID } from './test_ids'; -import { ALERT_REASON_TITLE, DOCUMENT_REASON_TITLE } from './translations'; +import React, { useCallback, useMemo } from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { ALERT_REASON } from '@kbn/rule-data-utils'; +import { getField } from '../../shared/utils'; +import { AlertReasonPreviewPanel, PreviewPanelKey } from '../../preview'; +import { + REASON_DETAILS_PREVIEW_BUTTON_TEST_ID, + REASON_DETAILS_TEST_ID, + REASON_TITLE_TEST_ID, +} from './test_ids'; +import { + ALERT_REASON_DETAILS_TEXT, + ALERT_REASON_TITLE, + DOCUMENT_REASON_TITLE, + PREVIEW_ALERT_REASON_DETAILS, +} from './translations'; import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; -import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; -import { getRowRenderer } from '../../../timelines/components/timeline/body/renderers/get_row_renderer'; import { useRightPanelContext } from '../context'; /** * Displays the information provided by the rowRenderer. Supports multiple types of documents. */ export const Reason: FC = () => { - const { dataAsNestedObject, dataFormattedForFieldBrowser } = useRightPanelContext(); + const { eventId, indexName, scopeId, dataFormattedForFieldBrowser, getFieldsData } = + useRightPanelContext(); const { isAlert } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); + const alertReason = getField(getFieldsData(ALERT_REASON)); - const renderer = useMemo( - () => - dataAsNestedObject != null - ? getRowRenderer({ data: dataAsNestedObject, rowRenderers: defaultRowRenderers }) - : null, - [dataAsNestedObject] + const { openPreviewPanel } = useExpandableFlyoutContext(); + const openRulePreview = useCallback(() => { + openPreviewPanel({ + id: PreviewPanelKey, + path: { tab: AlertReasonPreviewPanel }, + params: { + id: eventId, + indexName, + scopeId, + banner: { + title: PREVIEW_ALERT_REASON_DETAILS, + backgroundColor: 'warning', + textColor: 'warning', + }, + }, + }); + }, [eventId, openPreviewPanel, indexName, scopeId]); + + const viewPreview = useMemo( + () => ( + <EuiFlexItem grow={false}> + <EuiButtonEmpty + size="s" + iconType="expand" + onClick={openRulePreview} + iconSide="right" + data-test-subj={REASON_DETAILS_PREVIEW_BUTTON_TEST_ID} + > + {ALERT_REASON_DETAILS_TEXT} + </EuiButtonEmpty> + </EuiFlexItem> + ), + [openRulePreview] ); - if (!dataFormattedForFieldBrowser || !dataAsNestedObject || !renderer) { + if (!dataFormattedForFieldBrowser) { return null; } @@ -38,17 +78,21 @@ export const Reason: FC = () => { <EuiFlexGroup direction="column" gutterSize="s"> <EuiFlexItem data-test-subj={REASON_TITLE_TEST_ID}> <EuiTitle size="xxs"> - <h5>{isAlert ? ALERT_REASON_TITLE : DOCUMENT_REASON_TITLE}</h5> + <h5> + {isAlert ? ( + <EuiFlexGroup justifyContent="spaceBetween" alignItems="center"> + <EuiFlexItem> + <h5>{ALERT_REASON_TITLE}</h5> + </EuiFlexItem> + {viewPreview} + </EuiFlexGroup> + ) : ( + DOCUMENT_REASON_TITLE + )} + </h5> </EuiTitle> </EuiFlexItem> - <EuiFlexItem data-test-subj={REASON_DETAILS_TEST_ID}> - {renderer.renderRow({ - contextId: 'event-details', - data: dataAsNestedObject, - isDraggable: false, - scopeId: 'global', - })} - </EuiFlexItem> + <EuiFlexItem data-test-subj={REASON_DETAILS_TEST_ID}>{alertReason}</EuiFlexItem> </EuiFlexGroup> ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts index 9e8b112851be6..7d41fe13f9fca 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts @@ -46,6 +46,8 @@ export const DESCRIPTION_DETAILS_TEST_ID = 'securitySolutionDocumentDetailsFlyoutDescriptionDetails'; export const REASON_TITLE_TEST_ID = 'securitySolutionDocumentDetailsFlyoutReasonTitle'; export const REASON_DETAILS_TEST_ID = 'securitySolutionDocumentDetailsFlyoutReasonDetails'; +export const REASON_DETAILS_PREVIEW_BUTTON_TEST_ID = + 'securitySolutionDocumentDetailsFlyoutReasonDetailsPreviewButton'; export const MITRE_ATTACK_TITLE_TEST_ID = 'securitySolutionAlertDetailsFlyoutMitreAttackTitle'; export const MITRE_ATTACK_DETAILS_TEST_ID = 'securitySolutionAlertDetailsFlyoutMitreAttackDetails'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts index f32e9abb1d48f..a411b0f44054e 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts @@ -45,6 +45,13 @@ export const RULE_SUMMARY_TEXT = i18n.translate( } ); +export const ALERT_REASON_DETAILS_TEXT = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.alertReasonDetailsText', + { + defaultMessage: 'Show full reason', + } +); + /* About section */ export const ABOUT_TITLE = i18n.translate( @@ -66,6 +73,11 @@ export const PREVIEW_RULE_DETAILS = i18n.translate( { defaultMessage: 'Preview rule details' } ); +export const PREVIEW_ALERT_REASON_DETAILS = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.previewAlertReasonDetailsText', + { defaultMessage: 'Preview alert reason' } +); + export const DOCUMENT_DESCRIPTION_TITLE = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.documentDescriptionTitle', { diff --git a/x-pack/plugins/security_solution/public/flyout/right/content.tsx b/x-pack/plugins/security_solution/public/flyout/right/content.tsx index 9dd8391d24d11..d0d0b0a3b80b9 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/content.tsx @@ -10,23 +10,28 @@ import type { VFC } from 'react'; import React, { useMemo } from 'react'; import { FLYOUT_BODY_TEST_ID } from './test_ids'; import type { RightPanelPaths } from '.'; -import { tabs } from './tabs'; +import type { RightPanelTabsType } from './tabs'; +import {} from './tabs'; export interface PanelContentProps { /** * Id of the tab selected in the parent component to display its content */ selectedTabId: RightPanelPaths; + /** + * Tabs display right below the flyout's header + */ + tabs: RightPanelTabsType; } /** * Document details expandable flyout right section, that will display the content * of the overview, table and json tabs. */ -export const PanelContent: VFC<PanelContentProps> = ({ selectedTabId }) => { +export const PanelContent: VFC<PanelContentProps> = ({ selectedTabId, tabs }) => { const selectedTabContent = useMemo(() => { return tabs.find((tab) => tab.id === selectedTabId)?.content; - }, [selectedTabId]); + }, [selectedTabId, tabs]); return <EuiFlyoutBody data-test-subj={FLYOUT_BODY_TEST_ID}>{selectedTabContent}</EuiFlyoutBody>; }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/context.tsx b/x-pack/plugins/security_solution/public/flyout/right/context.tsx index 7b12fc3a3a6cb..31eec77707d2f 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/context.tsx @@ -10,9 +10,13 @@ import { css } from '@emotion/react'; import React, { createContext, useContext, useMemo } from 'react'; import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs'; + import type { SearchHit } from '../../../common/search_strategy'; import { useTimelineEventsDetails } from '../../timelines/containers/details'; -import { getAlertIndexAlias } from '../../timelines/components/side_panel/event_details/helpers'; +import { + getAlertIndexAlias, + useBasicDataFromDetailsData, +} from '../../timelines/components/side_panel/event_details/helpers'; import { useSpaceId } from '../../common/hooks/use_space_id'; import { useRouteSpy } from '../../common/utils/route/use_route_spy'; import { SecurityPageName } from '../../../common/constants'; @@ -21,6 +25,7 @@ import { useSourcererDataView } from '../../common/containers/sourcerer'; import type { RightPanelProps } from '.'; import type { GetFieldsData } from '../../common/hooks/use_get_fields_data'; import { useGetFieldsData } from '../../common/hooks/use_get_fields_data'; +import { useRuleWithFallback } from '../../detection_engine/rule_management/logic/use_rule_with_fallback'; export interface RightPanelContext { /** @@ -51,6 +56,10 @@ export interface RightPanelContext { * The actual raw document object */ searchHit: SearchHit | undefined; + /** + * User defined fields to highlight (defined on the rule) + */ + investigationFields: string[]; /** * Promise to trigger a data refresh */ @@ -94,6 +103,8 @@ export const RightPanelProvider = ({ skip: !id, }); const getFieldsData = useGetFieldsData(searchHit?.fields); + const { ruleId } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); + const { rule: maybeRule } = useRuleWithFallback(ruleId); const contextValue = useMemo( () => @@ -106,12 +117,14 @@ export const RightPanelProvider = ({ dataAsNestedObject, dataFormattedForFieldBrowser, searchHit, + investigationFields: maybeRule?.investigation_fields ?? [], refetchFlyoutData, getFieldsData, } : undefined, [ id, + maybeRule, indexName, scopeId, sourcererDataView.browserFields, diff --git a/x-pack/plugins/security_solution/public/flyout/right/header.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/header.test.tsx new file mode 100644 index 0000000000000..6432cbc2d41b6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/header.test.tsx @@ -0,0 +1,60 @@ +/* + * 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 React from 'react'; +import { render } from '@testing-library/react'; +import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; +import { TestProviders } from '../../common/mock'; +import { RightPanelContext } from './context'; +import { mockContextValue } from './mocks/mock_right_panel_context'; +import { PanelHeader } from './header'; +import { + COLLAPSE_DETAILS_BUTTON_TEST_ID, + EXPAND_DETAILS_BUTTON_TEST_ID, +} from './components/test_ids'; +import { mockFlyoutContextValue } from '../shared/mocks/mock_flyout_context'; + +describe('<PanelHeader />', () => { + it('should render expand details button if flyout is expandable', () => { + const { getByTestId } = render( + <TestProviders> + <ExpandableFlyoutContext.Provider value={mockFlyoutContextValue}> + <RightPanelContext.Provider value={mockContextValue}> + <PanelHeader + flyoutIsExpandable={true} + selectedTabId={'overview'} + setSelectedTabId={() => window.alert('test')} + tabs={[]} + /> + </RightPanelContext.Provider> + </ExpandableFlyoutContext.Provider> + </TestProviders> + ); + + expect(getByTestId(EXPAND_DETAILS_BUTTON_TEST_ID)).toBeInTheDocument(); + }); + + it('should not render expand details button if flyout is not expandable', () => { + const { queryByTestId } = render( + <TestProviders> + <ExpandableFlyoutContext.Provider value={mockFlyoutContextValue}> + <RightPanelContext.Provider value={mockContextValue}> + <PanelHeader + flyoutIsExpandable={false} + selectedTabId={'overview'} + setSelectedTabId={() => window.alert('test')} + tabs={[]} + /> + </RightPanelContext.Provider> + </ExpandableFlyoutContext.Provider> + </TestProviders> + ); + + expect(queryByTestId(EXPAND_DETAILS_BUTTON_TEST_ID)).not.toBeInTheDocument(); + expect(queryByTestId(COLLAPSE_DETAILS_BUTTON_TEST_ID)).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/header.tsx b/x-pack/plugins/security_solution/public/flyout/right/header.tsx index 4f316b9a8be50..67425b6eb3565 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/header.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/header.tsx @@ -10,18 +10,32 @@ import type { VFC } from 'react'; import React, { memo } from 'react'; import { css } from '@emotion/react'; import type { RightPanelPaths } from '.'; -import { tabs } from './tabs'; +import type { RightPanelTabsType } from './tabs'; import { HeaderTitle } from './components/header_title'; import { ExpandDetailButton } from './components/expand_detail_button'; export interface PanelHeaderProps { + /** + * Id of the tab selected in the parent component to display its content + */ selectedTabId: RightPanelPaths; + /** + * Callback to set the selected tab id in the parent component + * @param selected + */ setSelectedTabId: (selected: RightPanelPaths) => void; - handleOnEventClosed?: () => void; + /** + * Tabs to display in the header + */ + tabs: RightPanelTabsType; + /** + * If true, the expand detail button will be displayed + */ + flyoutIsExpandable: boolean; } export const PanelHeader: VFC<PanelHeaderProps> = memo( - ({ selectedTabId, setSelectedTabId, handleOnEventClosed }) => { + ({ flyoutIsExpandable, selectedTabId, setSelectedTabId, tabs }) => { const onSelectedTabChanged = (id: RightPanelPaths) => setSelectedTabId(id); const renderTabs = tabs.map((tab, index) => ( <EuiTab @@ -38,20 +52,22 @@ export const PanelHeader: VFC<PanelHeaderProps> = memo( <EuiFlyoutHeader hasBorder css={css` - margin-bottom: -24px; + margin-bottom: ${flyoutIsExpandable ? '-24px' : '0px'}; `} > - <div - // moving the buttons up in the header - css={css` - margin-top: -24px; - margin-left: -8px; - `} - > - <ExpandDetailButton /> - </div> + {flyoutIsExpandable && ( + <div + // moving the buttons up in the header + css={css` + margin-top: -24px; + margin-left: -8px; + `} + > + <ExpandDetailButton /> + </div> + )} <EuiSpacer size="xs" /> - <HeaderTitle /> + <HeaderTitle flyoutIsExpandable={flyoutIsExpandable} /> <EuiSpacer size="m" /> <EuiTabs size="l" diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_highlighted_fields.ts b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_highlighted_fields.ts index 20e036145a6ca..b9377a6ca85ac 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_highlighted_fields.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_highlighted_fields.ts @@ -19,6 +19,10 @@ export interface UseHighlightedFieldsParams { * An array of field objects with category and value */ dataFormattedForFieldBrowser: TimelineEventsDetailsItem[] | null; + /** + * An array of fields user has selected to highlight, defined on rule + */ + investigationFields?: string[]; } export interface UseHighlightedFieldsResult { @@ -43,6 +47,7 @@ export interface UseHighlightedFieldsResult { */ export const useHighlightedFields = ({ dataFormattedForFieldBrowser, + investigationFields, }: UseHighlightedFieldsParams): UseHighlightedFieldsResult[] => { if (!dataFormattedForFieldBrowser) return []; @@ -61,6 +66,7 @@ export const useHighlightedFields = ({ { category: 'kibana', field: ALERT_RULE_TYPE }, dataFormattedForFieldBrowser ); + const eventRuleType = Array.isArray(eventRuleTypeField?.originalValue) ? eventRuleTypeField?.originalValue?.[0] : eventRuleTypeField?.originalValue; @@ -69,6 +75,7 @@ export const useHighlightedFields = ({ eventCategories, eventCode, eventRuleType, + highlightedFieldsOverride: investigationFields ?? [], }); return tableFields.reduce<UseHighlightedFieldsResult[]>((acc, field) => { diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx index 162bc8bc851aa..5121e166e9a73 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx @@ -29,6 +29,10 @@ export interface UsePrevalenceParams { * Maintain backwards compatibility // TODO remove when possible */ scopeId: string; + /** + * User defined fields to highlight (defined on rule) + */ + investigationFields?: string[]; } /** @@ -41,6 +45,7 @@ export const usePrevalence = ({ eventId, browserFields, dataFormattedForFieldBrowser, + investigationFields, scopeId, }: UsePrevalenceParams): ReactElement[] => { // retrieves the highlighted fields @@ -49,11 +54,12 @@ export const usePrevalence = ({ getSummaryRows({ browserFields: browserFields || {}, data: dataFormattedForFieldBrowser || [], + investigationFields: investigationFields || [], eventId, scopeId, isReadOnly: false, }), - [browserFields, dataFormattedForFieldBrowser, eventId, scopeId] + [browserFields, investigationFields, dataFormattedForFieldBrowser, eventId, scopeId] ); return useMemo( diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_process_data.ts b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_process_data.ts index 72ca71badaf07..ac98ddd1df2b2 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_process_data.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_process_data.ts @@ -6,6 +6,7 @@ */ import { useMemo } from 'react'; +import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; import type { GetFieldsData } from '../../../common/hooks/use_get_fields_data'; import { getField } from '../../shared/utils'; import { useRightPanelContext } from '../context'; @@ -14,8 +15,6 @@ const FIELD_USER_NAME = 'process.entry_leader.user.name' as const; const FIELD_USER_ID = 'process.entry_leader.user.id' as const; const FIELD_PROCESS_NAME = 'process.entry_leader.name' as const; const FIELD_START_AT = 'process.entry_leader.start' as const; -const FIELD_RULE_NAME = 'kibana.alert.rule.name' as const; -const FIELD_RULE_ID = 'kibana.alert.rule.uuid' as const; const FIELD_WORKING_DIRECTORY = 'process.group_leader.working_directory' as const; const FIELD_COMMAND = 'process.command_line' as const; @@ -48,8 +47,8 @@ export const useProcessData = () => { userName: getUserDisplayName(getFieldsData), processName: getField(getFieldsData(FIELD_PROCESS_NAME)), startAt: getField(getFieldsData(FIELD_START_AT)), - ruleName: getField(getFieldsData(FIELD_RULE_NAME)), - ruleId: getField(getFieldsData(FIELD_RULE_ID)), + ruleName: getField(getFieldsData(ALERT_RULE_NAME)), + ruleId: getField(getFieldsData(ALERT_RULE_UUID)), workdir: getField(getFieldsData(FIELD_WORKING_DIRECTORY)), command: getField(getFieldsData(FIELD_COMMAND)), }), diff --git a/x-pack/plugins/security_solution/public/flyout/right/index.tsx b/x-pack/plugins/security_solution/public/flyout/right/index.tsx index 80600b1357b60..1af4450b921ab 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/index.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/index.tsx @@ -9,6 +9,8 @@ import type { FC } from 'react'; import React, { memo, useMemo } from 'react'; import type { FlyoutPanelProps, PanelPath } from '@kbn/expandable-flyout'; import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { EventKind } from '../shared/hooks/use_fetch_field_value_pair_by_event_type'; +import { getField } from '../shared/utils'; import { useRightPanelContext } from './context'; import { PanelHeader } from './header'; import { PanelContent } from './content'; @@ -34,13 +36,17 @@ export interface RightPanelProps extends FlyoutPanelProps { */ export const RightPanel: FC<Partial<RightPanelProps>> = memo(({ path }) => { const { openRightPanel } = useExpandableFlyoutContext(); - const { eventId, indexName, scopeId } = useRightPanelContext(); + const { eventId, getFieldsData, indexName, scopeId } = useRightPanelContext(); + + // for 8.10, we only render the flyout in its expandable mode if the document viewed is of type signal + const documentIsSignal = getField(getFieldsData('event.kind')) === EventKind.signal; + const tabsDisplayed = documentIsSignal ? tabs : tabs.filter((tab) => tab.id !== 'overview'); const selectedTabId = useMemo(() => { - const defaultTab = tabs[0].id; + const defaultTab = tabsDisplayed[0].id; if (!path) return defaultTab; - return tabs.map((tab) => tab.id).find((tabId) => tabId === path.tab) ?? defaultTab; - }, [path]); + return tabsDisplayed.map((tab) => tab.id).find((tabId) => tabId === path.tab) ?? defaultTab; + }, [path, tabsDisplayed]); const setSelectedTabId = (tabId: RightPanelTabsType[number]['id']) => { openRightPanel({ @@ -58,8 +64,13 @@ export const RightPanel: FC<Partial<RightPanelProps>> = memo(({ path }) => { return ( <> - <PanelHeader selectedTabId={selectedTabId} setSelectedTabId={setSelectedTabId} /> - <PanelContent selectedTabId={selectedTabId} /> + <PanelHeader + flyoutIsExpandable={documentIsSignal} + tabs={tabsDisplayed} + selectedTabId={selectedTabId} + setSelectedTabId={setSelectedTabId} + /> + <PanelContent tabs={tabsDisplayed} selectedTabId={selectedTabId} /> <PanelFooter /> </> ); diff --git a/x-pack/plugins/security_solution/public/flyout/right/mocks/mock_context.ts b/x-pack/plugins/security_solution/public/flyout/right/mocks/mock_context.ts index 6ae872acd45ac..cdc058569d9d3 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/mocks/mock_context.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/mocks/mock_context.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ALERT_RISK_SCORE, ALERT_SEVERITY } from '@kbn/rule-data-utils'; +import { ALERT_REASON, ALERT_RISK_SCORE, ALERT_SEVERITY } from '@kbn/rule-data-utils'; /** * Returns mocked data for field (mock this method: x-pack/plugins/security_solution/public/common/hooks/use_get_fields_data.ts) @@ -22,6 +22,8 @@ export const mockGetFieldsData = (field: string): string[] => { return ['host1']; case 'user.name': return ['user1']; + case ALERT_REASON: + return ['reason']; default: return []; } diff --git a/x-pack/plugins/security_solution/public/flyout/right/mocks/mock_right_panel_context.ts b/x-pack/plugins/security_solution/public/flyout/right/mocks/mock_right_panel_context.ts index 95c986df43787..e7593b1eea9e9 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/mocks/mock_right_panel_context.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/mocks/mock_right_panel_context.ts @@ -20,5 +20,6 @@ export const mockContextValue: RightPanelContext = { browserFields: null, dataAsNestedObject: null, searchHit: undefined, + investigationFields: [], refetchFlyoutData: jest.fn(), }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts index 7c6d7926f2fa9..003e55fa6f1b8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -100,4 +100,5 @@ export const getOutputRuleAlertForRest = (): RuleResponse => ({ namespace: undefined, data_view_id: undefined, alert_suppression: undefined, + investigation_fields: [], }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/schedule_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/schedule_notification_actions.test.ts index b4c1a794929c5..02e162c0d7ff1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/schedule_notification_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/schedule_notification_actions.test.ts @@ -44,6 +44,7 @@ describe('schedule_notification_actions', () => { responseActions: [], riskScore: 80, riskScoreMapping: [], + investigationFields: [], ruleNameOverride: undefined, dataViewId: undefined, outputIndex: 'output-1', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/schedule_throttle_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/schedule_throttle_notification_actions.test.ts index 701da673efcd6..a5381504e98fd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/schedule_throttle_notification_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/schedule_throttle_notification_actions.test.ts @@ -64,6 +64,7 @@ describe('schedule_throttle_notification_actions', () => { requiredFields: [], setup: '', alertSuppression: undefined, + investigationFields: undefined, }; }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts index 08a53c007dc06..7223b920c7bdc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts @@ -62,6 +62,7 @@ describe('duplicateRule', () => { timestampOverrideFallbackDisabled: undefined, dataViewId: undefined, alertSuppression: undefined, + investigationFields: undefined, }, schedule: { interval: '5m', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts index 66a4531d1c2e3..5248e7f06938f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts @@ -45,6 +45,7 @@ export const updateRules = async ({ ruleId: existingRule.params.ruleId, falsePositives: ruleUpdate.false_positives ?? [], from: ruleUpdate.from ?? 'now-6m', + investigationFields: ruleUpdate.investigation_fields ?? [], // Unlike the create route, immutable comes from the existing rule here immutable: existingRule.params.immutable, license: ruleUpdate.license, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts index 60e4d56278337..662085c95d62b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts @@ -136,6 +136,7 @@ describe('getExportAll', () => { note: '# Investigative notes', version: 1, exceptions_list: getListArrayMock(), + investigation_fields: [], }); expect(detailsJson).toEqual({ exported_exception_list_count: 1, @@ -319,6 +320,7 @@ describe('getExportAll', () => { version: 1, revision: 0, exceptions_list: getListArrayMock(), + investigation_fields: [], }); expect(detailsJson).toEqual({ exported_exception_list_count: 1, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts index cf1c9db355bdd..b8f27c0d16a5c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts @@ -132,6 +132,7 @@ describe('get_export_by_object_ids', () => { note: '# Investigative notes', version: 1, exceptions_list: getListArrayMock(), + investigation_fields: [], }, exportDetails: { exported_exception_list_count: 0, @@ -327,6 +328,7 @@ describe('get_export_by_object_ids', () => { version: 1, revision: 0, exceptions_list: getListArrayMock(), + investigation_fields: [], }); expect(detailsJson).toEqual({ exported_exception_list_count: 0, @@ -523,6 +525,7 @@ describe('get_export_by_object_ids', () => { namespace: undefined, data_view_id: undefined, alert_suppression: undefined, + investigation_fields: [], }, ], }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts index 300d58ecc2e11..ecb379a1dcc2d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts @@ -409,6 +409,7 @@ export const convertPatchAPIToInternalSchema = ( description: nextParams.description ?? existingParams.description, ruleId: existingParams.ruleId, falsePositives: nextParams.false_positives ?? existingParams.falsePositives, + investigationFields: nextParams.investigation_fields ?? existingParams.investigationFields, from: nextParams.from ?? existingParams.from, immutable: existingParams.immutable, license: nextParams.license ?? existingParams.license, @@ -470,6 +471,7 @@ export const convertCreateAPIToInternalSchema = ( description: input.description, ruleId: newRuleId, falsePositives: input.false_positives ?? [], + investigationFields: input.investigation_fields ?? [], from: input.from ?? 'now-6m', immutable, license: input.license, @@ -619,6 +621,7 @@ export const commonParamsCamelToSnake = (params: BaseRuleParams) => { rule_name_override: params.ruleNameOverride, timestamp_override: params.timestampOverride, timestamp_override_fallback_disabled: params.timestampOverrideFallbackDisabled, + investigation_fields: params.investigationFields, author: params.author, false_positives: params.falsePositives, from: params.from, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts index ce60bf34d2ed8..d24bcaa5a7b9b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts @@ -78,6 +78,7 @@ export const ruleOutput = (): RuleResponse => ({ data_view_id: undefined, saved_id: undefined, alert_suppression: undefined, + investigation_fields: [], }); describe('validate', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts index 44fa44ee01892..a6e6ee5282e89 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts @@ -48,6 +48,7 @@ const getBaseRuleParams = (): BaseRuleParams => { timelineTitle: 'some-timeline-title', timestampOverride: undefined, timestampOverrideFallbackDisabled: undefined, + investigationFields: [], meta: { someMeta: 'someField', }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts index c851ed9288ea6..d3e66cf49148d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts @@ -57,6 +57,7 @@ import { RuleAuthorArray, RuleDescription, RuleFalsePositiveArray, + RuleCustomHighlightedFieldArray, RuleFilterArray, RuleLicense, RuleMetadata, @@ -97,6 +98,7 @@ export const baseRuleParams = t.exact( falsePositives: RuleFalsePositiveArray, from: RuleIntervalFrom, ruleId: RuleSignatureId, + investigationFields: t.union([RuleCustomHighlightedFieldArray, t.undefined]), immutable: IsRuleImmutable, license: t.union([RuleLicense, t.undefined]), outputIndex: AlertsIndex, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/es_results.ts index 93c93eb631fa2..6a522193558aa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/es_results.ts @@ -523,6 +523,7 @@ export const sampleSignalHit = (): SignalHit => ({ filters: undefined, saved_id: undefined, alert_suppression: undefined, + investigation_fields: undefined, }, depth: 1, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts index 06b0dc5b90514..8e06156bdc913 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts @@ -165,6 +165,7 @@ describe('buildAlert', () => { index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], query: 'user.name: root or user.name: admin', filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], + investigation_fields: [], }, [ALERT_RULE_INDICES]: completeRule.ruleParams.index, ...flattenWithPrefix(ALERT_RULE_NAMESPACE, { @@ -358,6 +359,7 @@ describe('buildAlert', () => { index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], query: 'user.name: root or user.name: admin', filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], + investigation_fields: [], }, ...flattenWithPrefix(ALERT_RULE_NAMESPACE, { actions: [], diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index ca55a8288dd12..61ddfa8e00505 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -6613,6 +6613,31 @@ } } } + }, + "alerts_stats": { + "type": "array", + "items": { + "properties": { + "posture_type": { + "type": "keyword" + }, + "rules_count": { + "type": "long" + }, + "alerts_count": { + "type": "long" + }, + "alerts_open_count": { + "type": "long" + }, + "alerts_closed_count": { + "type": "long" + }, + "alerts_acknowledged_count": { + "type": "long" + } + } + } } } }, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 57e178fe4ac4f..e76f335fcc301 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -11588,7 +11588,6 @@ "xpack.csp.vulnerabilities.table.filterIn": "Inclure", "xpack.csp.vulnerabilities.table.filterOut": "Exclure", "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.packageTitle": "Pack", - "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.resourceTitle": "Ressource", "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.versionTitle": "Version", "xpack.csp.vulnerabilities.vulnerabilityFindingFlyout.jsonTabLabel": "JSON", "xpack.csp.vulnerabilities.vulnerabilityFindingFlyout.loadingAriaLabel": "Chargement", @@ -11615,7 +11614,6 @@ "xpack.csp.vulnerabilityDashboard.viewAllButton.buttonTitle": "Tout afficher", "xpack.csp.vulnerabilityTable.column.fixVersion": "Version du correctif", "xpack.csp.vulnerabilityTable.column.package": "Pack", - "xpack.csp.vulnerabilityTable.column.resource": "Ressource", "xpack.csp.vulnerabilityTable.column.severity": "Sévérité", "xpack.csp.vulnerabilityTable.column.sortAscending": "Basse -> Critique", "xpack.csp.vulnerabilityTable.column.sortDescending": "Critique -> Basse", @@ -14857,8 +14855,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.mysqlName": "MySQL", "xpack.enterpriseSearch.workplaceSearch.integrations.netowkrDriveDescription": "Effectuez des recherches sur le contenu de votre lecteur réseau avec Enterprise Search.", "xpack.enterpriseSearch.workplaceSearch.integrations.networkDriveName": "Lecteur réseau", - "xpack.enterpriseSearch.workplaceSearch.integrations.onedriveDescription": "Effectuez des recherches dans vos fichiers stockés sur OneDrive avec Workplace Search.", - "xpack.enterpriseSearch.workplaceSearch.integrations.onedriveName": "OneDrive", "xpack.enterpriseSearch.workplaceSearch.integrations.oracleDescription": "Effectuez des recherches sur votre contenu sur Oracle avec Enterprise Search.", "xpack.enterpriseSearch.workplaceSearch.integrations.oracleName": "Oracle", "xpack.enterpriseSearch.workplaceSearch.integrations.postgreSQLDescription": "Effectuez des recherches sur votre contenu sur PostgreSQL avec Enterprise Search.", @@ -14866,7 +14862,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.s3": "Amazon S3", "xpack.enterpriseSearch.workplaceSearch.integrations.s3Description": "Effectuez des recherches sur votre contenu sur Amazon S3 avec Enterprise Search.", "xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxDescription": "Effectuez des recherches dans votre contenu sur Salesforce Sandbox avec Workplace Search.", - "xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxName": "Sandbox Salesforce", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineDescription": "Effectuez des recherches dans vos fichiers stockés sur SharePoint Online avec Workplace Search.", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineName": "SharePoint Online", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerDescription": "Effectuez des recherches dans vos fichiers stockés sur le serveur Microsoft SharePoint avec Workplace Search.", @@ -31108,7 +31103,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText": "Sélectionner un champ pour vérifier la cardinalité", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdLabel": "Seuil", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.licenseWarning": "La suppression d'alertes est activée avec la licence Platinum ou supérieure", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.placeholderText": "Sélectionner un champ", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByDurationValueLabel": "Supprimer les alertes pour", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabel": "Supprimer les alertes par", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabelAppend": "Facultatif (version d'évaluation technique)", @@ -33365,7 +33359,6 @@ "xpack.securitySolution.flyout.entities.failRelatedHostsDescription": "Impossible de lancer la recherche sur les hôtes associés", "xpack.securitySolution.flyout.entities.failRelatedUsersDescription": "Impossible de lancer la recherche sur les utilisateurs associés", "xpack.securitySolution.flyout.entities.hostsInfoTitle": "Informations sur l’hôte", - "xpack.securitySolution.flyout.entities.hostsTitle": "Hôtes", "xpack.securitySolution.flyout.entities.relatedEntitiesIpColumn": "Adresses IP", "xpack.securitySolution.flyout.entities.relatedEntitiesNameColumn": "Nom", "xpack.securitySolution.flyout.entities.relatedHostsTitle": "Hôtes associés", @@ -33373,7 +33366,6 @@ "xpack.securitySolution.flyout.entities.relatedUsersTitle": "Utilisateurs associés", "xpack.securitySolution.flyout.entities.relatedUsersToolTip": "Ces utilisateurs ont été authentifiés avec succès sur l’hôte concerné après l’alerte.", "xpack.securitySolution.flyout.entities.usersInfoTitle": "Informations sur l’utilisateur", - "xpack.securitySolution.flyout.entities.usersTitle": "Utilisateurs", "xpack.securitySolution.flyout.prevalenceErrorMessage": "prévalence", "xpack.securitySolution.flyout.prevalenceTableAlertCountColumnTitle": "Nombre d'alertes", "xpack.securitySolution.flyout.prevalenceTableDocCountColumnTitle": "Compte du document", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 95949c67fcc5e..74c6db4b1e59c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11603,7 +11603,6 @@ "xpack.csp.vulnerabilities.table.filterIn": "フィルタリング", "xpack.csp.vulnerabilities.table.filterOut": "除外", "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.packageTitle": "パッケージ", - "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.resourceTitle": "リソース", "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.versionTitle": "バージョン", "xpack.csp.vulnerabilities.vulnerabilityFindingFlyout.jsonTabLabel": "JSON", "xpack.csp.vulnerabilities.vulnerabilityFindingFlyout.loadingAriaLabel": "読み込み中", @@ -11630,7 +11629,6 @@ "xpack.csp.vulnerabilityDashboard.viewAllButton.buttonTitle": "すべて表示", "xpack.csp.vulnerabilityTable.column.fixVersion": "修正バージョン", "xpack.csp.vulnerabilityTable.column.package": "パッケージ", - "xpack.csp.vulnerabilityTable.column.resource": "リソース", "xpack.csp.vulnerabilityTable.column.severity": "深刻度", "xpack.csp.vulnerabilityTable.column.sortAscending": "低 -> 重大", "xpack.csp.vulnerabilityTable.column.sortDescending": "重大 -> 低", @@ -14871,8 +14869,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.mysqlName": "MySQL", "xpack.enterpriseSearch.workplaceSearch.integrations.netowkrDriveDescription": "エンタープライズ サーチでネットワークドライブコンテンツを検索します。", "xpack.enterpriseSearch.workplaceSearch.integrations.networkDriveName": "ネットワークドライブ", - "xpack.enterpriseSearch.workplaceSearch.integrations.onedriveDescription": "Workplace Searchを使用して、OneDriveに保存されたファイルを検索します。", - "xpack.enterpriseSearch.workplaceSearch.integrations.onedriveName": "OneDrive", "xpack.enterpriseSearch.workplaceSearch.integrations.oracleDescription": "エンタープライズ サーチでOracleのコンテンツを検索します。", "xpack.enterpriseSearch.workplaceSearch.integrations.oracleName": "Oracle", "xpack.enterpriseSearch.workplaceSearch.integrations.postgreSQLDescription": "エンタープライズ サーチでPostgreSQLのコンテンツを検索します。", @@ -14880,7 +14876,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.s3": "Amazon S3", "xpack.enterpriseSearch.workplaceSearch.integrations.s3Description": "エンタープライズサーチでAmazon S3のコンテンツを検索します。", "xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxDescription": "Workplace Searchを使用して、Salesforce Sandboxのコンテンツを検索します。", - "xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxName": "Salesforce Sandbox", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineDescription": "Workplace Searchを使用して、SharePointに保存されたファイルを検索します。", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineName": "SharePoint Online", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerDescription": "Workplace Searchを使用して、Microsoft SharePoint Serverに保存されたファイルを検索します。", @@ -31107,7 +31102,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText": "カーディナリティを確認するフィールドを選択します", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdLabel": "しきい値", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.licenseWarning": "アラートの非表示は、プラチナライセンス以上で有効です", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.placeholderText": "フィールドを選択", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByDurationValueLabel": "アラートを非表示", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabel": "アラートを非表示", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabelAppend": "任意(テクニカルプレビュー)", @@ -33364,7 +33358,6 @@ "xpack.securitySolution.flyout.entities.failRelatedHostsDescription": "関連するホストで検索を実行できませんでした", "xpack.securitySolution.flyout.entities.failRelatedUsersDescription": "関連するユーザーで検索を実行できませんでした", "xpack.securitySolution.flyout.entities.hostsInfoTitle": "ホスト情報", - "xpack.securitySolution.flyout.entities.hostsTitle": "ホスト", "xpack.securitySolution.flyout.entities.relatedEntitiesIpColumn": "IPアドレス", "xpack.securitySolution.flyout.entities.relatedEntitiesNameColumn": "名前", "xpack.securitySolution.flyout.entities.relatedHostsTitle": "関連するホスト", @@ -33372,7 +33365,6 @@ "xpack.securitySolution.flyout.entities.relatedUsersTitle": "関連するユーザー", "xpack.securitySolution.flyout.entities.relatedUsersToolTip": "アラート後、ユーザーは影響を受けるホストへの認証に成功しました。", "xpack.securitySolution.flyout.entities.usersInfoTitle": "ユーザー情報", - "xpack.securitySolution.flyout.entities.usersTitle": "ユーザー", "xpack.securitySolution.flyout.prevalenceErrorMessage": "発生率", "xpack.securitySolution.flyout.prevalenceTableAlertCountColumnTitle": "アラート件数", "xpack.securitySolution.flyout.prevalenceTableDocCountColumnTitle": "ドキュメントカウント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 367fe86bcbd5e..b64a7850e0191 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11603,7 +11603,6 @@ "xpack.csp.vulnerabilities.table.filterIn": "筛选范围", "xpack.csp.vulnerabilities.table.filterOut": "筛除", "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.packageTitle": "软件包", - "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.resourceTitle": "资源", "xpack.csp.vulnerabilities.vulnerabilitiesFindingFlyout.flyoutDescriptionList.versionTitle": "版本", "xpack.csp.vulnerabilities.vulnerabilityFindingFlyout.jsonTabLabel": "JSON", "xpack.csp.vulnerabilities.vulnerabilityFindingFlyout.loadingAriaLabel": "正在加载", @@ -11630,7 +11629,6 @@ "xpack.csp.vulnerabilityDashboard.viewAllButton.buttonTitle": "查看全部", "xpack.csp.vulnerabilityTable.column.fixVersion": "修复版本", "xpack.csp.vulnerabilityTable.column.package": "软件包", - "xpack.csp.vulnerabilityTable.column.resource": "资源", "xpack.csp.vulnerabilityTable.column.severity": "严重性", "xpack.csp.vulnerabilityTable.column.sortAscending": "低 -> 严重", "xpack.csp.vulnerabilityTable.column.sortDescending": "严重 -> 低", @@ -14871,8 +14869,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.mysqlName": "MySQL", "xpack.enterpriseSearch.workplaceSearch.integrations.netowkrDriveDescription": "使用 Enterprise Search 搜索您的网络驱动器内容。", "xpack.enterpriseSearch.workplaceSearch.integrations.networkDriveName": "网络驱动器", - "xpack.enterpriseSearch.workplaceSearch.integrations.onedriveDescription": "通过 Workplace Search 搜索存储在 OneDrive 上的文件。", - "xpack.enterpriseSearch.workplaceSearch.integrations.onedriveName": "OneDrive", "xpack.enterpriseSearch.workplaceSearch.integrations.oracleDescription": "使用 Enterprise Search 在 Oracle 上搜索您的内容。", "xpack.enterpriseSearch.workplaceSearch.integrations.oracleName": "Oracle", "xpack.enterpriseSearch.workplaceSearch.integrations.postgreSQLDescription": "使用 Enterprise Search 在 PostgreSQL 上搜索您的内容。", @@ -14880,7 +14876,6 @@ "xpack.enterpriseSearch.workplaceSearch.integrations.s3": "Amazon S3", "xpack.enterpriseSearch.workplaceSearch.integrations.s3Description": "使用 Enterprise Search 在 Amazon S3 上搜索您的内容。", "xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxDescription": "通过 Workplace Search 搜索 Salesforce Sandbox 上的内容。", - "xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxName": "Salesforce Sandbox", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineDescription": "通过 Workplace Search 搜索存储在 SharePoint Online 上的文件。", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineName": "Sharepoint", "xpack.enterpriseSearch.workplaceSearch.integrations.sharepointServerDescription": "通过 Workplace Search 搜索存储在 Microsoft SharePoint Server 上的文件。", @@ -31103,7 +31098,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText": "选择字段以检查基数", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdLabel": "阈值", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.licenseWarning": "告警阻止通过白金级或更高级许可证启用", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupBy.placeholderText": "选择字段", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByDurationValueLabel": "阻止以下项的告警", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabel": "阻止告警的依据", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByFieldsLabelAppend": "可选(技术预览)", @@ -33360,7 +33354,6 @@ "xpack.securitySolution.flyout.entities.failRelatedHostsDescription": "无法对相关主机执行搜索", "xpack.securitySolution.flyout.entities.failRelatedUsersDescription": "无法对相关用户执行搜索", "xpack.securitySolution.flyout.entities.hostsInfoTitle": "主机信息", - "xpack.securitySolution.flyout.entities.hostsTitle": "主机", "xpack.securitySolution.flyout.entities.relatedEntitiesIpColumn": "IP 地址", "xpack.securitySolution.flyout.entities.relatedEntitiesNameColumn": "名称", "xpack.securitySolution.flyout.entities.relatedHostsTitle": "相关主机", @@ -33368,7 +33361,6 @@ "xpack.securitySolution.flyout.entities.relatedUsersTitle": "相关用户", "xpack.securitySolution.flyout.entities.relatedUsersToolTip": "告警后,这些用户已成功通过受影响主机的身份验证。", "xpack.securitySolution.flyout.entities.usersInfoTitle": "用户信息", - "xpack.securitySolution.flyout.entities.usersTitle": "用户", "xpack.securitySolution.flyout.prevalenceErrorMessage": "普及率", "xpack.securitySolution.flyout.prevalenceTableAlertCountColumnTitle": "告警计数", "xpack.securitySolution.flyout.prevalenceTableDocCountColumnTitle": "文档计数", diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts index 66e075792b6fa..57c9651bec55a 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts @@ -76,6 +76,7 @@ export default ({ getService }: FtrProviderContext) => { author: [], created_by: 'elastic', description: 'Simple Rule Query', + investigation_fields: [], enabled: true, false_positives: [], from: 'now-6m', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts index fe17a9fb62008..b624cd95787aa 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts @@ -207,6 +207,7 @@ export default ({ getService }: FtrProviderContext) => { risk_score_mapping: [], name: 'Simple Rule Query', query: 'user.name: root or user.name: admin', + investigation_fields: [], references: [], related_integrations: [], required_fields: [], diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts index 49cf3cc5107a7..cb47021ba3d5e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts @@ -742,5 +742,6 @@ function expectToMatchRuleSchema(obj: RuleResponse): void { index: expect.arrayContaining([]), query: expect.any(String), actions: expect.arrayContaining([]), + investigation_fields: expect.arrayContaining([]), }); } diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts index b671c2ce39d4d..9a55755f2e93a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts @@ -426,6 +426,28 @@ export default ({ getService }: FtrProviderContext) => { message: 'rule_id: "fake_id" not found', }); }); + + describe('investigation_fields', () => { + it('should overwrite investigation_fields value on update - non additive', async () => { + await createRule(supertest, log, { + ...getSimpleRule('rule-1'), + investigation_fields: ['blob', 'boop'], + }); + + const rulePatch = { + rule_id: 'rule-1', + investigation_fields: ['foo', 'bar'], + }; + + const { body } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rulePatch) + .expect(200); + + expect(body.investigation_fields).to.eql(['foo', 'bar']); + }); + }); }); describe('patch per-action frequencies', () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts index 7b2dfca1f46fd..0f26e2b396db1 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts @@ -841,6 +841,28 @@ export default ({ getService }: FtrProviderContext) => { }); }); }); + + describe('investigation_fields', () => { + it('should overwrite investigation_fields value on update - non additive', async () => { + await createRule(supertest, log, { + ...getSimpleRule('rule-1'), + investigation_fields: ['blob', 'boop'], + }); + + const ruleUpdate = { + ...getSimpleRuleUpdate('rule-1'), + investigation_fields: ['foo', 'bar'], + }; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleUpdate) + .expect(200); + + expect(body.investigation_fields).to.eql(['foo', 'bar']); + }); + }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts index 9e7b6265a2b9f..1435565286485 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group6/alerts/alerts_compatibility.ts @@ -337,6 +337,7 @@ export default ({ getService }: FtrProviderContext) => { max_signals: 100, risk_score_mapping: [], severity_mapping: [], + investigation_fields: [], threat: [], to: 'now', references: [], @@ -512,6 +513,7 @@ export default ({ getService }: FtrProviderContext) => { related_integrations: [], required_fields: [], setup: '', + investigation_fields: [], }, 'kibana.alert.rule.actions': [], 'kibana.alert.rule.created_by': 'elastic', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts index 792fcb30b6645..b9f6b8caed951 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/machine_learning.ts @@ -146,6 +146,7 @@ export default ({ getService }: FtrProviderContext) => { to: 'now', type: 'machine_learning', version: 1, + investigation_fields: [], }, [ALERT_DEPTH]: 1, [ALERT_REASON]: `event with process store, by root on mothra created critical alert Test ML rule.`, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/new_terms.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/new_terms.ts index 0ac86c991015d..72a71185065d9 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/new_terms.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/rule_execution_logic/new_terms.ts @@ -198,6 +198,7 @@ export default ({ getService }: FtrProviderContext) => { history_window_start: '2019-01-19T20:42:00.000Z', index: ['auditbeat-*'], language: 'kuery', + investigation_fields: [], }, 'kibana.alert.rule.actions': [], 'kibana.alert.rule.author': [], diff --git a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts index 0115b00c4b46b..9c35d6652935f 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts @@ -102,4 +102,5 @@ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial<RuleResponse> = related_integrations: [], required_fields: [], setup: '', + investigation_fields: [], }); diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts index 7c51faf2b8846..92f427876e351 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts @@ -60,6 +60,7 @@ export const getMockSharedResponseSchema = ( timestamp_override: undefined, timestamp_override_fallback_disabled: undefined, namespace: undefined, + investigation_fields: [], }); const getQueryRuleOutput = (ruleId = 'rule-1', enabled = false): RuleResponse => ({ diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index 79fa4d0eea06e..7fedb18db416c 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -199,8 +199,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/157711 - describe.skip('alerts flyouts', () => { + describe('alerts flyouts', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); await pageObjects.common.navigateToApp('infraOps'); @@ -217,7 +216,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.infraHome.closeAlertFlyout(); }); - it('should open and close inventory alert flyout', async () => { + it('should open and close metrics threshold alert flyout', async () => { await pageObjects.infraHome.openMetricsThresholdAlertFlyout(); await pageObjects.infraHome.closeAlertFlyout(); }); diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index 527124c34ef74..afe19a1ae1938 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -17,8 +17,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); - // FLAKY: https://github.com/elastic/kibana/issues/89072 - describe.skip('overview page', function () { + describe('overview page', function () { const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; @@ -199,7 +198,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('can change query syntax to kql', async () => { await testSubjects.click('switchQueryLanguageButton'); - await testSubjects.click('languageToggle'); + await testSubjects.click('kqlLanguageMenuItem'); }); it('runs filter query without issues', async () => { diff --git a/x-pack/test/functional/page_objects/infra_home_page.ts b/x-pack/test/functional/page_objects/infra_home_page.ts index f9f088b106375..aed3558cfdcb4 100644 --- a/x-pack/test/functional/page_objects/infra_home_page.ts +++ b/x-pack/test/functional/page_objects/infra_home_page.ts @@ -335,20 +335,40 @@ export function InfraHomePageProvider({ getService, getPageObjects }: FtrProvide await testSubjects.missingOrFail('metrics-alert-menu'); }, + async dismissDatePickerTooltip() { + const isTooltipOpen = await testSubjects.exists(`waffleDatePickerIntervalTooltip`, { + timeout: 1000, + }); + + if (isTooltipOpen) { + await testSubjects.click(`waffleDatePickerIntervalTooltip`); + } + }, + async openInventoryAlertFlyout() { + await this.dismissDatePickerTooltip(); await testSubjects.click('infrastructure-alerts-and-rules'); await testSubjects.click('inventory-alerts-menu-option'); - await testSubjects.click('inventory-alerts-create-rule'); + + // forces date picker tooltip to close in case it pops up after Alerts and rules opens + await testSubjects.moveMouseTo('contextMenuPanelTitleButton'); + + await retry.tryForTime(1000, () => testSubjects.click('inventory-alerts-create-rule')); await testSubjects.missingOrFail('inventory-alerts-create-rule', { timeout: 30000 }); - await testSubjects.find('euiFlyoutCloseButton'); }, async openMetricsThresholdAlertFlyout() { + await this.dismissDatePickerTooltip(); await testSubjects.click('infrastructure-alerts-and-rules'); await testSubjects.click('metrics-threshold-alerts-menu-option'); - await testSubjects.click('metrics-threshold-alerts-create-rule'); + + // forces date picker tooltip to close in case it pops up after Alerts and rules opens + await testSubjects.moveMouseTo('contextMenuPanelTitleButton'); + + await retry.tryForTime(1000, () => + testSubjects.click('metrics-threshold-alerts-create-rule') + ); await testSubjects.missingOrFail('metrics-threshold-alerts-create-rule', { timeout: 30000 }); - await testSubjects.find('euiFlyoutCloseButton'); }, async closeAlertFlyout() { diff --git a/x-pack/test/functional/services/uptime/common.ts b/x-pack/test/functional/services/uptime/common.ts index 4488c340a2b6b..d78aea22f8167 100644 --- a/x-pack/test/functional/services/uptime/common.ts +++ b/x-pack/test/functional/services/uptime/common.ts @@ -45,7 +45,7 @@ export function UptimeCommonProvider({ getService, getPageObjects }: FtrProvider await this.setKueryBarText('queryInput', filterQuery); }, async goToNextPage() { - await testSubjects.click('xpack.synthetics.monitorList.nextButton', 5000); + await testSubjects.click('xpack.uptime.monitorList.nextButton', 5000); }, async goToPreviousPage() { await testSubjects.click('xpack.synthetics.monitorList.prevButton', 5000); @@ -97,11 +97,11 @@ export function UptimeCommonProvider({ getService, getPageObjects }: FtrProvider }; }, async openPageSizeSelectPopover(): Promise<void> { - return testSubjects.click('xpack.synthetics.monitorList.pageSizeSelect.popoverOpen', 5000); + return testSubjects.click('xpack.uptime.monitorList.pageSizeSelect.popoverOpen', 5000); }, async clickPageSizeSelectPopoverItem(size: number = 10): Promise<void> { return testSubjects.click( - `xpack.synthetics.monitorList.pageSizeSelect.sizeSelectItem${size.toString()}`, + `xpack.uptime.monitorList.pageSizeSelect.sizeSelectItem${size.toString()}`, 5000 ); }, diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group1/create_case_form.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group1/create_case_form.ts index 7eec8e96ef380..d3230de9d0b10 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group1/create_case_form.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group1/create_case_form.ts @@ -99,6 +99,39 @@ export default ({ getService, getPageObject }: FtrProviderContext) => { ); }); + it('trims fields correctly while creating a case', async () => { + const titleWithSpace = 'This is a title with spaces '; + const descriptionWithSpace = + 'This is a case description with empty spaces at the end!! '; + const categoryWithSpace = 'security '; + const tagWithSpace = 'coke '; + + await cases.create.openCreateCasePage(); + await cases.create.createCase({ + title: titleWithSpace, + description: descriptionWithSpace, + tag: tagWithSpace, + severity: CaseSeverity.HIGH, + category: categoryWithSpace, + }); + + // validate title is trimmed + const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]'); + expect(await title.getVisibleText()).equal(titleWithSpace.trim()); + + // validate description is trimmed + const description = await testSubjects.find('scrollable-markdown'); + expect(await description.getVisibleText()).equal(descriptionWithSpace.trim()); + + // validate tag exists and is trimmed + const tag = await testSubjects.find(`tag-${tagWithSpace.trim()}`); + expect(await tag.getVisibleText()).equal(tagWithSpace.trim()); + + // validate category exists and is trimmed + const category = await testSubjects.find(`category-viewer-${categoryWithSpace.trim()}`); + expect(await category.getVisibleText()).equal(categoryWithSpace.trim()); + }); + describe('Assignees', function () { before(async () => { await createUsersAndRoles(getService, users, roles); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/alerts_detection_callouts_index_outdated.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/alerts_detection_callouts_index_outdated.cy.ts index 59aa3a5abe793..1a1b47e925c91 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/alerts_detection_callouts_index_outdated.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/alerts_detection_callouts_index_outdated.cy.ts @@ -15,7 +15,11 @@ import { PAGE_TITLE } from '../../screens/common/page'; import { login, visitWithoutDateRange, waitForPageWithoutDateRange } from '../../tasks/login'; import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; import { createRule, deleteCustomRule } from '../../tasks/api_calls/rules'; -import { getCallOut, waitForCallOutToBeShown } from '../../tasks/common/callouts'; +import { + getCallOut, + NEED_ADMIN_FOR_UPDATE_CALLOUT, + waitForCallOutToBeShown, +} from '../../tasks/common/callouts'; const loadPageAsPlatformEngineerUser = (url: string) => { login(ROLES.soc_manager); @@ -31,8 +35,6 @@ describe( 'Detections > Need Admin Callouts indicating an admin is needed to migrate the alert data set', { tags: tag.ESS }, () => { - const NEED_ADMIN_FOR_UPDATE_CALLOUT = 'need-admin-for-update-rules'; - before(() => { // First, we have to open the app on behalf of a privileged user in order to initialize it. // Otherwise the app will be disabled and show a "welcome"-like page. diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/missing_privileges_callout.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/missing_privileges_callout.cy.ts index 4ca60eebad297..767b2ecbdd5c2 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/missing_privileges_callout.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_alerts/missing_privileges_callout.cy.ts @@ -15,7 +15,12 @@ import { PAGE_TITLE } from '../../screens/common/page'; import { login, visitWithoutDateRange, waitForPageWithoutDateRange } from '../../tasks/login'; import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; import { createRule, deleteCustomRule } from '../../tasks/api_calls/rules'; -import { getCallOut, waitForCallOutToBeShown, dismissCallOut } from '../../tasks/common/callouts'; +import { + getCallOut, + waitForCallOutToBeShown, + dismissCallOut, + MISSING_PRIVILEGES_CALLOUT, +} from '../../tasks/common/callouts'; const loadPageAsReadOnlyUser = (url: string) => { login(ROLES.reader); @@ -39,8 +44,6 @@ const waitForPageTitleToBeShown = () => { }; describe('Detections > Callouts', { tags: tag.ESS }, () => { - const MISSING_PRIVILEGES_CALLOUT = 'missing-user-privileges'; - before(() => { // First, we have to open the app on behalf of a privileged user in order to initialize it. // Otherwise the app will be disabled and show a "welcome"-like page. diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/authorization/all_rules_read_only.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/authorization/all_rules_read_only.cy.ts index 62f484b69427a..bd8c5743d37b2 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/authorization/all_rules_read_only.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/authorization/all_rules_read_only.cy.ts @@ -22,12 +22,11 @@ import { dismissCallOut, getCallOut, waitForCallOutToBeShown, + MISSING_PRIVILEGES_CALLOUT, } from '../../../../tasks/common/callouts'; import { login, visitWithoutDateRange } from '../../../../tasks/login'; import { SECURITY_DETECTIONS_RULES_URL } from '../../../../urls/navigation'; -const MISSING_PRIVILEGES_CALLOUT = 'missing-user-privileges'; - describe('All rules - read only', { tags: tag.ESS }, () => { before(() => { cleanKibana(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_actions.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_actions.cy.ts index d6000c671fb86..3b3755dc66d3f 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_actions.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_actions.cy.ts @@ -7,6 +7,11 @@ import type { RuleActionArray } from '@kbn/securitysolution-io-ts-alerting-types'; import { ROLES } from '@kbn/security-solution-plugin/common/test'; +import { + MISSING_PRIVILEGES_CALLOUT, + waitForCallOutToBeShown, +} from '../../../../../tasks/common/callouts'; +import { createRuleAssetSavedObject } from '../../../../../helpers/rules'; import { tag } from '../../../../../tags'; import { @@ -34,6 +39,7 @@ import { waitForRulesTableToBeLoaded, selectNumberOfRules, goToEditRuleActionsSettingsOf, + disableAutoRefresh, } from '../../../../../tasks/alerts_detection_rules'; import { waitForBulkEditActionToFinish, @@ -57,28 +63,28 @@ import { getMachineLearningRule, getNewTermsRule, } from '../../../../../objects/rule'; -import { excessivelyInstallAllPrebuiltRules } from '../../../../../tasks/api_calls/prebuilt_rules'; +import { + createAndInstallMockedPrebuiltRules, + excessivelyInstallAllPrebuiltRules, + preventPrebuiltRulesPackageInstallation, +} from '../../../../../tasks/api_calls/prebuilt_rules'; const ruleNameToAssert = 'Custom rule name with actions'; const expectedNumberOfCustomRulesToBeEdited = 7; -// 7 custom rules of different types + 3 prebuilt. +// 7 custom rules of different types + 2 prebuilt. // number of selected rules doesn't matter, we only want to make sure they will be edited an no modal window displayed as for other actions -const expectedNumberOfRulesToBeEdited = expectedNumberOfCustomRulesToBeEdited + 3; +const expectedNumberOfRulesToBeEdited = expectedNumberOfCustomRulesToBeEdited + 2; const expectedExistingSlackMessage = 'Existing slack action'; const expectedSlackMessage = 'Slack action test message'; -// TODO: Fix flakiness and unskip https://github.com/elastic/kibana/issues/154721 -describe.skip( +describe( 'Detection rules, bulk edit of rule actions', { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, () => { - before(() => { + beforeEach(() => { cleanKibana(); login(); - }); - - beforeEach(() => { deleteAlertsAndRules(); deleteConnectors(); cy.task('esArchiverResetKibana'); @@ -111,12 +117,27 @@ describe.skip( createRule(getNewRule({ saved_id: 'mocked', rule_id: '7' })); createSlackConnector(); + + // Prevent prebuilt rules package installation and mock two prebuilt rules + preventPrebuiltRulesPackageInstallation(); + + const RULE_1 = createRuleAssetSavedObject({ + name: 'Test rule 1', + rule_id: 'rule_1', + }); + const RULE_2 = createRuleAssetSavedObject({ + name: 'Test rule 2', + rule_id: 'rule_2', + }); + + createAndInstallMockedPrebuiltRules({ rules: [RULE_1, RULE_2] }); }); context('Restricted action privileges', () => { it("User with no privileges can't add rule actions", () => { login(ROLES.hunter_no_actions); visitWithoutDateRange(SECURITY_DETECTIONS_RULES_URL, ROLES.hunter_no_actions); + waitForCallOutToBeShown(MISSING_PRIVILEGES_CALLOUT, 'primary'); waitForRulesTableToBeLoaded(); selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); @@ -129,8 +150,10 @@ describe.skip( context('All actions privileges', () => { beforeEach(() => { + login(); visitWithoutDateRange(SECURITY_DETECTIONS_RULES_URL); waitForRulesTableToBeLoaded(); + disableAutoRefresh(); }); it('Add a rule action to rules (existing connector)', () => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_data_view.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_data_view.cy.ts index 824699e2cd230..9ac8cfbe5ddd0 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_data_view.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/rule_management/rule_actions/bulk_actions/bulk_edit_rules_data_view.cy.ts @@ -58,7 +58,7 @@ const expectedNumberOfCustomRulesToBeEdited = 6; describe( 'Bulk editing index patterns of rules with a data view only', - { tags: [tag.ESS, tag.SERVERLESS] }, + { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, () => { before(() => { cleanKibana(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts index 3e29f3a08cb70..bef92ab69444b 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/endpoint_exceptions.cy.ts @@ -42,7 +42,8 @@ import { } from '../../../screens/exceptions'; import { goToEndpointExceptionsTab } from '../../../tasks/rule_details'; -describe( +// See https://github.com/elastic/kibana/issues/163967 +describe.skip( 'Endpoint Exceptions workflows from Alert', { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, () => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions/auto_populate_with_alert_data.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions/auto_populate_with_alert_data.cy.ts index 6a7df890aec06..5a2451d42d86e 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions/auto_populate_with_alert_data.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions/auto_populate_with_alert_data.cy.ts @@ -39,7 +39,8 @@ import { } from '../../../../screens/exceptions'; import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; -describe( +// See https://github.com/elastic/kibana/issues/163967 +describe.skip( 'Auto populate exception with Alert data', { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, () => { diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions/closing_all_matching_alerts.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions/closing_all_matching_alerts.cy.ts index 96bf48fa27935..ea905a7774126 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions/closing_all_matching_alerts.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/alerts_table_flow/rule_exceptions/closing_all_matching_alerts.cy.ts @@ -4,13 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; import { addExceptionFromFirstAlert, goToClosedAlertsOnRuleDetailsPage, waitForAlerts, } from '../../../../tasks/alerts'; import { deleteAlertsAndRules, postDataView } from '../../../../tasks/common'; -import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; import { login, visitWithoutDateRange } from '../../../../tasks/login'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../../urls/navigation'; import { goToRuleDetails } from '../../../../tasks/alerts_detection_rules'; @@ -27,6 +27,7 @@ import { submitNewExceptionItem, } from '../../../../tasks/exceptions'; +// See https://github.com/elastic/kibana/issues/163967 describe('Close matching Alerts ', () => { const newRule = getNewRule(); const ITEM_NAME = 'Sample Exception Item'; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/rule_details_flow/add_edit_exception.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/rule_details_flow/add_edit_exception.cy.ts index fc91468b88c56..c86f79ee0e264 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/rule_details_flow/add_edit_exception.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/rule_details_flow/add_edit_exception.cy.ts @@ -60,277 +60,284 @@ import { } from '../../../tasks/api_calls/exceptions'; import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; -describe('Add/edit exception from rule details', { tags: [tag.ESS, tag.SERVERLESS] }, () => { - const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; - const FIELD_DIFFERENT_FROM_EXISTING_ITEM_FIELD = 'agent.name'; - const ITEM_FIELD = 'unique_value.test'; - - before(() => { - cy.task('esArchiverResetKibana'); - cy.task('esArchiverLoad', 'exceptions'); - login(); - }); - - after(() => { - cy.task('esArchiverUnload', 'exceptions'); - }); - - describe('existing list and items', () => { - const exceptionList = getExceptionList(); - beforeEach(() => { - deleteAlertsAndRules(); - deleteExceptionList(exceptionList.list_id, exceptionList.namespace_type); - // create rule with exceptions - createExceptionList(exceptionList, exceptionList.list_id).then((response) => { - createRule( - getNewRule({ - query: 'agent.name:*', - index: ['exceptions*'], - exceptions_list: [ +describe( + 'Add/edit exception from rule details', + { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, + () => { + const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; + const FIELD_DIFFERENT_FROM_EXISTING_ITEM_FIELD = 'agent.name'; + const ITEM_FIELD = 'unique_value.test'; + + before(() => { + cy.task('esArchiverResetKibana'); + cy.task('esArchiverLoad', 'exceptions'); + login(); + }); + + after(() => { + cy.task('esArchiverUnload', 'exceptions'); + }); + + describe('existing list and items', () => { + const exceptionList = getExceptionList(); + beforeEach(() => { + deleteAlertsAndRules(); + deleteExceptionList(exceptionList.list_id, exceptionList.namespace_type); + // create rule with exceptions + createExceptionList(exceptionList, exceptionList.list_id).then((response) => { + createRule( + getNewRule({ + query: 'agent.name:*', + index: ['exceptions*'], + exceptions_list: [ + { + id: response.body.id, + list_id: exceptionList.list_id, + type: exceptionList.type, + namespace_type: exceptionList.namespace_type, + }, + ], + rule_id: '2', + }) + ); + createExceptionListItem(exceptionList.list_id, { + list_id: exceptionList.list_id, + item_id: 'simple_list_item', + tags: [], + type: 'simple', + description: 'Test exception item 2', + name: 'Sample Exception List Item 2', + namespace_type: 'single', + entries: [ { - id: response.body.id, - list_id: exceptionList.list_id, - type: exceptionList.type, - namespace_type: exceptionList.namespace_type, + field: ITEM_FIELD, + operator: 'included', + type: 'match_any', + value: ['foo'], }, ], - rule_id: '2', - }) - ); - createExceptionListItem(exceptionList.list_id, { - list_id: exceptionList.list_id, - item_id: 'simple_list_item', - tags: [], - type: 'simple', - description: 'Test exception item 2', - name: 'Sample Exception List Item 2', - namespace_type: 'single', - entries: [ - { - field: ITEM_FIELD, - operator: 'included', - type: 'match_any', - value: ['foo'], - }, - ], + }); }); - }); - login(); - visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - goToRuleDetails(); - goToExceptionsTab(); - }); + login(); + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + goToExceptionsTab(); + }); - it('Edits an exception item', () => { - const NEW_ITEM_NAME = 'Exception item-EDITED'; - const ITEM_NAME = 'Sample Exception List Item 2'; + it('Edits an exception item', () => { + const NEW_ITEM_NAME = 'Exception item-EDITED'; + const ITEM_NAME = 'Sample Exception List Item 2'; - // displays existing exception items - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); - cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); - cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', ITEM_NAME); - cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).should('have.text', ' unique_value.testis one of foo'); + // displays existing exception items + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); + cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', ITEM_NAME); + cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).should( + 'have.text', + ' unique_value.testis one of foo' + ); - // open edit exception modal - openEditException(); + // open edit exception modal + openEditException(); - // edit exception item name - editExceptionFlyoutItemName(NEW_ITEM_NAME); + // edit exception item name + editExceptionFlyoutItemName(NEW_ITEM_NAME); - // check that the existing item's field is being populated - cy.get(EXCEPTION_ITEM_CONTAINER) - .eq(0) - .find(FIELD_INPUT_PARENT) - .eq(0) - .should('have.text', ITEM_FIELD); - cy.get(VALUES_MATCH_ANY_INPUT).should('have.text', 'foo'); + // check that the existing item's field is being populated + cy.get(EXCEPTION_ITEM_CONTAINER) + .eq(0) + .find(FIELD_INPUT_PARENT) + .eq(0) + .should('have.text', ITEM_FIELD); + cy.get(VALUES_MATCH_ANY_INPUT).should('have.text', 'foo'); - // edit conditions - editException(FIELD_DIFFERENT_FROM_EXISTING_ITEM_FIELD, 0, 0); + // edit conditions + editException(FIELD_DIFFERENT_FROM_EXISTING_ITEM_FIELD, 0, 0); - // submit - submitEditedExceptionItem(); + // submit + submitEditedExceptionItem(); - // new exception item displays - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); - // check that updates stuck - cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', NEW_ITEM_NAME); - cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).should('have.text', ' agent.nameIS foo'); - }); + // check that updates stuck + cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', NEW_ITEM_NAME); + cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).should('have.text', ' agent.nameIS foo'); + }); - describe('rule with existing shared exceptions', () => { - it('Creates an exception item to add to shared list', () => { - // displays existing exception items - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); - cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); + describe('rule with existing shared exceptions', () => { + it('Creates an exception item to add to shared list', () => { + // displays existing exception items + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); - // open add exception modal - addExceptionFlyoutFromViewerHeader(); + // open add exception modal + addExceptionFlyoutFromViewerHeader(); - // add exception item conditions - addExceptionConditions(getException()); + // add exception item conditions + addExceptionConditions(getException()); - // Name is required so want to check that submit is still disabled - cy.get(CONFIRM_BTN).should('have.attr', 'disabled'); + // Name is required so want to check that submit is still disabled + cy.get(CONFIRM_BTN).should('have.attr', 'disabled'); - // add exception item name - addExceptionFlyoutItemName('My item name'); + // add exception item name + addExceptionFlyoutItemName('My item name'); - // select to add exception item to a shared list - selectSharedListToAddExceptionTo(1); + // select to add exception item to a shared list + selectSharedListToAddExceptionTo(1); - // not testing close alert functionality here, just ensuring that the options appear as expected - cy.get(CLOSE_ALERTS_CHECKBOX).should('exist'); - cy.get(CLOSE_ALERTS_CHECKBOX).should('not.have.attr', 'disabled'); + // not testing close alert functionality here, just ensuring that the options appear as expected + cy.get(CLOSE_ALERTS_CHECKBOX).should('exist'); + cy.get(CLOSE_ALERTS_CHECKBOX).should('not.have.attr', 'disabled'); - // submit - submitNewExceptionItem(); + // submit + submitNewExceptionItem(); - // new exception item displays - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 2); - }); + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 2); + }); - it('Creates an exception item to add to rule only', () => { - // displays existing exception items - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); - cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); + it('Creates an exception item to add to rule only', () => { + // displays existing exception items + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); - // open add exception modal - addExceptionFlyoutFromViewerHeader(); + // open add exception modal + addExceptionFlyoutFromViewerHeader(); - // add exception item conditions - addExceptionConditions(getException()); + // add exception item conditions + addExceptionConditions(getException()); - // Name is required so want to check that submit is still disabled - cy.get(CONFIRM_BTN).should('have.attr', 'disabled'); + // Name is required so want to check that submit is still disabled + cy.get(CONFIRM_BTN).should('have.attr', 'disabled'); - // add exception item name - addExceptionFlyoutItemName('My item name'); + // add exception item name + addExceptionFlyoutItemName('My item name'); - // select to add exception item to rule only - selectAddToRuleRadio(); + // select to add exception item to rule only + selectAddToRuleRadio(); - // not testing close alert functionality here, just ensuring that the options appear as expected - cy.get(CLOSE_ALERTS_CHECKBOX).should('exist'); - cy.get(CLOSE_ALERTS_CHECKBOX).should('not.have.attr', 'disabled'); + // not testing close alert functionality here, just ensuring that the options appear as expected + cy.get(CLOSE_ALERTS_CHECKBOX).should('exist'); + cy.get(CLOSE_ALERTS_CHECKBOX).should('not.have.attr', 'disabled'); - // submit - submitNewExceptionItem(); + // submit + submitNewExceptionItem(); - // new exception item displays - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 2); - }); + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 2); + }); - // Trying to figure out with EUI why the search won't trigger - it('Can search for items', () => { - // displays existing exception items - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); - cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); + // Trying to figure out with EUI why the search won't trigger + it('Can search for items', () => { + // displays existing exception items + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); - // can search for an exception value - searchForExceptionItem('foo'); + // can search for an exception value + searchForExceptionItem('foo'); - // new exception item displays - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); - // displays empty search result view if no matches found - searchForExceptionItem('abc'); + // displays empty search result view if no matches found + searchForExceptionItem('abc'); - // new exception item displays - cy.get(NO_EXCEPTIONS_SEARCH_RESULTS_PROMPT).should('exist'); + // new exception item displays + cy.get(NO_EXCEPTIONS_SEARCH_RESULTS_PROMPT).should('exist'); + }); }); }); - }); - - describe('rule without existing exceptions', () => { - beforeEach(() => { - deleteAlertsAndRules(); - createRule( - getNewRule({ - query: 'agent.name:*', - index: ['exceptions*'], - interval: '10s', - rule_id: 'rule_testing', - }) - ); - login(); - visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - goToRuleDetails(); - goToExceptionsTab(); - }); - afterEach(() => { - cy.task('esArchiverUnload', 'exceptions_2'); - }); + describe('rule without existing exceptions', () => { + beforeEach(() => { + deleteAlertsAndRules(); + createRule( + getNewRule({ + query: 'agent.name:*', + index: ['exceptions*'], + interval: '10s', + rule_id: 'rule_testing', + }) + ); + login(); + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + goToExceptionsTab(); + }); - it('Cannot create an item to add to rule but not shared list as rule has no lists attached', () => { - // when no exceptions exist, empty component shows with action to add exception - cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); + afterEach(() => { + cy.task('esArchiverUnload', 'exceptions_2'); + }); - // open add exception modal - openExceptionFlyoutFromEmptyViewerPrompt(); + it('Cannot create an item to add to rule but not shared list as rule has no lists attached', () => { + // when no exceptions exist, empty component shows with action to add exception + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); - // add exception item conditions - addExceptionConditions({ - field: 'agent.name', - operator: 'is', - values: ['foo'], - }); + // open add exception modal + openExceptionFlyoutFromEmptyViewerPrompt(); + + // add exception item conditions + addExceptionConditions({ + field: 'agent.name', + operator: 'is', + values: ['foo'], + }); - // Name is required so want to check that submit is still disabled - cy.get(CONFIRM_BTN).should('have.attr', 'disabled'); + // Name is required so want to check that submit is still disabled + cy.get(CONFIRM_BTN).should('have.attr', 'disabled'); - // add exception item name - addExceptionFlyoutItemName('My item name'); + // add exception item name + addExceptionFlyoutItemName('My item name'); - // select to add exception item to rule only - selectAddToRuleRadio(); + // select to add exception item to rule only + selectAddToRuleRadio(); - // Check that add to shared list is disabled, should be unless - // rule has shared lists attached to it already - cy.get(ADD_TO_SHARED_LIST_RADIO_INPUT).should('have.attr', 'disabled'); + // Check that add to shared list is disabled, should be unless + // rule has shared lists attached to it already + cy.get(ADD_TO_SHARED_LIST_RADIO_INPUT).should('have.attr', 'disabled'); - // Close matching alerts - selectBulkCloseAlerts(); + // Close matching alerts + selectBulkCloseAlerts(); - // submit - submitNewExceptionItem(); + // submit + submitNewExceptionItem(); - // new exception item displays - cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); - // Alerts table should now be empty from having added exception and closed - // matching alert - goToAlertsTab(); - cy.get(EMPTY_ALERT_TABLE).should('exist'); + // Alerts table should now be empty from having added exception and closed + // matching alert + goToAlertsTab(); + cy.get(EMPTY_ALERT_TABLE).should('exist'); - // Closed alert should appear in table - goToClosedAlertsOnRuleDetailsPage(); - cy.get(ALERTS_COUNT).should('exist'); - cy.get(ALERTS_COUNT).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); + // Closed alert should appear in table + goToClosedAlertsOnRuleDetailsPage(); + cy.get(ALERTS_COUNT).should('exist'); + cy.get(ALERTS_COUNT).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - // Remove the exception and load an event that would have matched that exception - // to show that said exception now starts to show up again - goToExceptionsTab(); + // Remove the exception and load an event that would have matched that exception + // to show that said exception now starts to show up again + goToExceptionsTab(); - // when removing exception and again, no more exist, empty screen shows again - removeException(); - cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); + // when removing exception and again, no more exist, empty screen shows again + removeException(); + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); - // load more docs - cy.task('esArchiverLoad', 'exceptions_2'); + // load more docs + cy.task('esArchiverLoad', 'exceptions_2'); - // now that there are no more exceptions, the docs should match and populate alerts - goToAlertsTab(); - waitForAlertsToPopulate(); - goToOpenedAlertsOnRuleDetailsPage(); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); + // now that there are no more exceptions, the docs should match and populate alerts + goToAlertsTab(); + waitForAlertsToPopulate(); + goToOpenedAlertsOnRuleDetailsPage(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); - cy.get(ALERTS_COUNT).should('exist'); - cy.get(ALERTS_COUNT).should('have.text', '2 alerts'); + cy.get(ALERTS_COUNT).should('exist'); + cy.get(ALERTS_COUNT).should('have.text', '2 alerts'); + }); }); - }); -}); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/rule_details_flow/add_edit_exception_data_view.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/rule_details_flow/add_edit_exception_data_view.cy.ts index 1d020462683da..ab0595b23f889 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/rule_details_flow/add_edit_exception_data_view.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/rule_details_flow/add_edit_exception_data_view.cy.ts @@ -44,7 +44,7 @@ import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; describe( 'Add exception using data views from rule details', - { tags: [tag.ESS, tag.SERVERLESS] }, + { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, () => { const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; const ITEM_NAME = 'Sample Exception List Item'; diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/read_only.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/read_only.cy.ts index 25b9d2e34fe2e..b11d3de105b83 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/read_only.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/exceptions/shared_exception_lists_management/shared_exception_list_page/read_only.cy.ts @@ -17,12 +17,11 @@ import { dismissCallOut, getCallOut, waitForCallOutToBeShown, + MISSING_PRIVILEGES_CALLOUT, } from '../../../../tasks/common/callouts'; import { login, visitWithoutDateRange } from '../../../../tasks/login'; import { EXCEPTIONS_URL } from '../../../../urls/navigation'; -const MISSING_PRIVILEGES_CALLOUT = 'missing-user-privileges'; - describe('Shared exception lists - read only', { tags: tag.ESS }, () => { before(() => { cy.task('esArchiverResetKibana'); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/detection_page_filters.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/detection_page_filters.cy.ts index e9ea4d15b6152..f3806d8f070fb 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/detection_page_filters.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/detection_page_filters.cy.ts @@ -108,7 +108,7 @@ const assertFilterControlsWithFilterObject = ( }); }; -describe(`Detections : Page Filters`, { tags: [tag.ESS, tag.SERVERLESS] }, () => { +describe(`Detections : Page Filters`, { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, () => { before(() => { cleanKibana(); createRule(getNewRule({ rule_id: 'custom_rule_filters' })); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_alert_reason_preview.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_alert_reason_preview.cy.ts new file mode 100644 index 0000000000000..83d2dbee62212 --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_preview_panel_alert_reason_preview.cy.ts @@ -0,0 +1,42 @@ +/* + * 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 { DOCUMENT_DETAILS_FLYOUT_ALERT_REASON_PREVIEW_CONTAINER } from '../../../../screens/expandable_flyout/alert_details_preview_panel_alert_reason_preview'; +import { expandFirstAlertExpandableFlyout } from '../../../../tasks/expandable_flyout/common'; +import { clickAlertReasonButton } from '../../../../tasks/expandable_flyout/alert_details_right_panel_overview_tab'; +import { cleanKibana } from '../../../../tasks/common'; +import { login, visit } from '../../../../tasks/login'; +import { createRule } from '../../../../tasks/api_calls/rules'; +import { getNewRule } from '../../../../objects/rule'; +import { ALERTS_URL } from '../../../../urls/navigation'; +import { waitForAlertsToPopulate } from '../../../../tasks/create_new_rule'; +import { tag } from '../../../../tags'; + +describe( + 'Alert details expandable flyout rule preview panel', + { tags: [tag.ESS, tag.BROKEN_IN_SERVERLESS] }, + () => { + const rule = getNewRule(); + + beforeEach(() => { + cleanKibana(); + login(); + createRule(rule); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + clickAlertReasonButton(); + }); + + describe('alert reason preview', () => { + it('should display alert reason preview', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_ALERT_REASON_PREVIEW_CONTAINER).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_ALERT_REASON_PREVIEW_CONTAINER).should('be.visible'); + }); + }); + } +); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts index 2cdf95746dcfa..050463b70ae50 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts @@ -111,7 +111,8 @@ describe( cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE) .should('be.visible') - .and('have.text', 'Alert reason'); + .and('contain.text', 'Alert reason') + .and('contain.text', 'Show full reason'); cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_DETAILS) .should('be.visible') .and('contain.text', rule.name); diff --git a/x-pack/test/security_solution_cypress/cypress/objects/rule.ts b/x-pack/test/security_solution_cypress/cypress/objects/rule.ts index e2920f4975478..586a4188b84b1 100644 --- a/x-pack/test/security_solution_cypress/cypress/objects/rule.ts +++ b/x-pack/test/security_solution_cypress/cypress/objects/rule.ts @@ -532,6 +532,7 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response<RuleResponse immutable, related_integrations: relatedIntegrations, setup, + investigation_fields: investigationFields, } = ruleResponse.body; let query: string | undefined; @@ -558,6 +559,7 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response<RuleResponse severity, note, output_index: '', + investigation_fields: investigationFields, author, false_positives: falsePositives, from, diff --git a/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_preview_panel_alert_reason_preview.ts b/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_preview_panel_alert_reason_preview.ts new file mode 100644 index 0000000000000..37db919da75ab --- /dev/null +++ b/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_preview_panel_alert_reason_preview.ts @@ -0,0 +1,13 @@ +/* + * 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 { ALERT_REASON_PREVIEW_BODY_TEST_ID } from '@kbn/security-solution-plugin/public/flyout/preview/components/test_ids'; +import { getDataTestSubjectSelector } from '../../helpers/common'; + +export const DOCUMENT_DETAILS_FLYOUT_ALERT_REASON_PREVIEW_CONTAINER = getDataTestSubjectSelector( + ALERT_REASON_PREVIEW_BODY_TEST_ID +); diff --git a/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts b/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts index dc7e3fdd1020e..ebfb40a3091f5 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/expandable_flyout/alert_details_right_panel_overview_tab.ts @@ -38,6 +38,7 @@ import { ANALYZER_PREVIEW_CONTENT_TEST_ID, SESSION_PREVIEW_CONTENT_TEST_ID, INSIGHTS_PREVALENCE_VALUE_TEST_ID, + REASON_DETAILS_PREVIEW_BUTTON_TEST_ID, } from '@kbn/security-solution-plugin/public/flyout/right/components/test_ids'; import { getDataTestSubjectSelector } from '../../helpers/common'; @@ -59,6 +60,8 @@ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE = getDataTestSubjectSelector(REASON_TITLE_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_DETAILS = getDataTestSubjectSelector(REASON_DETAILS_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_ALERT_REASON_PREVIEW_BUTTON = + getDataTestSubjectSelector(REASON_DETAILS_PREVIEW_BUTTON_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_TITLE = getDataTestSubjectSelector( MITRE_ATTACK_TITLE_TEST_ID ); diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/common/callouts.ts b/x-pack/test/security_solution_cypress/cypress/tasks/common/callouts.ts index c65a29b8aa750..802faf821f8da 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/common/callouts.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/common/callouts.ts @@ -7,6 +7,9 @@ import { callOutWithId, CALLOUT_DISMISS_BTN } from '../../screens/common/callouts'; +export const NEED_ADMIN_FOR_UPDATE_CALLOUT = 'need-admin-for-update-rules'; +export const MISSING_PRIVILEGES_CALLOUT = 'missing-user-privileges'; + export const getCallOut = (id: string, options?: Cypress.Timeoutable) => { return cy.get(callOutWithId(id), options); }; diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts b/x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts index c87a3c2afb9fe..b94a8be090fa4 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout/alert_details_right_panel_overview_tab.ts @@ -20,6 +20,8 @@ import { DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_DESCRIPTION_TITLE, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_RULE_PREVIEW_BUTTON, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_RESPONSE_SECTION_HEADER, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_ALERT_REASON_PREVIEW_BUTTON, } from '../../screens/expandable_flyout/alert_details_right_panel_overview_tab'; /* About section */ @@ -129,3 +131,17 @@ export const clickRuleSummaryButton = () => { .click(); }); }; + +/** + * Click `Show full reason` button to open alert reason preview panel + */ +export const clickAlertReasonButton = () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE).scrollIntoView(); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_REASON_TITLE) + .should('be.visible') + .within(() => { + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_OPEN_ALERT_REASON_PREVIEW_BUTTON) + .should('be.visible') + .click(); + }); +}; diff --git a/x-pack/test_serverless/api_integration/test_suites/common/alerting/rules.ts b/x-pack/test_serverless/api_integration/test_suites/common/alerting/rules.ts index 86fdf7afb842a..c263a6f540c3e 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/alerting/rules.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/alerting/rules.ts @@ -34,7 +34,7 @@ export default function ({ getService }: FtrProviderContext) { const esClient = getService('es'); const esDeleteAllIndices = getService('esDeleteAllIndices'); - describe('Alerting rules', () => { + describe.skip('Alerting rules', () => { const RULE_TYPE_ID = '.es-query'; const ALERT_ACTION_INDEX = 'alert-action-es-query'; let actionId: string; diff --git a/yarn.lock b/yarn.lock index 42e3b4e2a799f..0569bafd6f6ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1549,12 +1549,12 @@ "@elastic/transport" "^8.3.1" tslib "^2.4.0" -"@elastic/elasticsearch@npm:@elastic/elasticsearch@8.9.0": - version "8.9.0" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-8.9.0.tgz#d132021c6c12e4171fe14371609a5c69b535edd4" - integrity sha512-UyolnzjOYTRL2966TYS3IoJP4tQbvak/pmYmbP3JdphD53RjkyVDdxMpTBv+2LcNBRrvYPTzxQbpRW/nGSXA9g== +"@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@8.9.1-canary.1": + version "8.9.1-canary.1" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.9.1-canary.1.tgz#7c1cdc6cc4129910544b2a3abd6a73b9fcc82ff3" + integrity sha512-pxFP57AEmbsgC6LsGv7xyAR4qCXiX6JXAGVdzBXDl2qEdz1p5y3htgyT6tGvyTV11Ma0AflsWx0jJ1vrp6bGew== dependencies: - "@elastic/transport" "^8.3.2" + "@elastic/transport" "^8.3.3" tslib "^2.4.0" "@elastic/ems-client@8.4.0": @@ -1734,22 +1734,10 @@ undici "^5.21.2" yaml "^2.2.2" -"@elastic/transport@^8.3.1": - version "8.3.1" - resolved "https://registry.yarnpkg.com/@elastic/transport/-/transport-8.3.1.tgz#e7569d7df35b03108ea7aa886113800245faa17f" - integrity sha512-jv/Yp2VLvv5tSMEOF8iGrtL2YsYHbpf4s+nDsItxUTLFTzuJGpnsB/xBlfsoT2kAYEnWHiSJuqrbRcpXEI/SEQ== - dependencies: - debug "^4.3.4" - hpagent "^1.0.0" - ms "^2.1.3" - secure-json-parse "^2.4.0" - tslib "^2.4.0" - undici "^5.5.1" - -"@elastic/transport@^8.3.2": - version "8.3.2" - resolved "https://registry.yarnpkg.com/@elastic/transport/-/transport-8.3.2.tgz#295e91f43e3a60a839f998ac3090a83ddb441592" - integrity sha512-ZiBYRVPj6pwYW99fueyNU4notDf7ZPs7Ix+4T1btIJsKJmeaORIItIfs+0O7KV4vV+DcvyMhkY1FXQx7kQOODw== +"@elastic/transport@^8.3.1", "@elastic/transport@^8.3.3": + version "8.3.3" + resolved "https://registry.yarnpkg.com/@elastic/transport/-/transport-8.3.3.tgz#06c5b1b9566796775ac96d17959dafc269da5ec1" + integrity sha512-g5nc//dq/RQUTMkJUB8Ui8KJa/WflWmUa7yLl4SRZd67PPxIp3cn+OvGMNIhpiLRcfz1upanzgZHb/7Po2eEdQ== dependencies: debug "^4.3.4" hpagent "^1.0.0" @@ -4748,6 +4736,10 @@ version "0.0.0" uid "" +"@kbn/lens-embeddable-utils@link:packages/kbn-lens-embeddable-utils": + version "0.0.0" + uid "" + "@kbn/lens-plugin@link:x-pack/plugins/lens": version "0.0.0" uid "" @@ -6020,10 +6012,6 @@ version "0.0.0" uid "" -"@kbn/lens-embeddable-utils@link:packages/kbn-lens-embeddable-utils": - version "0.0.0" - uid "" - "@kbn/visualizations-plugin@link:src/plugins/visualizations": version "0.0.0" uid "" @@ -9739,7 +9727,7 @@ "@types/cookiejar" "*" "@types/node" "*" -"@types/supertest@^2.0.5": +"@types/supertest@^2.0.12": version "2.0.12" resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.12.tgz#ddb4a0568597c9aadff8dbec5b2e8fddbe8692fc" integrity sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ== @@ -13025,11 +13013,16 @@ compare-versions@^6.0.0: resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.0.tgz#3f2131e3ae93577df111dba133e6db876ffe127a" integrity sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg== -component-emitter@^1.2.0, component-emitter@^1.2.1: +component-emitter@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= +component-emitter@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + compress-commons@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.1.tgz#df2a09a7ed17447642bad10a85cc9a19e5c42a7d" @@ -13206,7 +13199,7 @@ cookie@^0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -cookiejar@^2.1.0: +cookiejar@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== @@ -14609,6 +14602,14 @@ dezalgo@^1.0.0: asap "^2.0.0" wrappy "1" +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + dfa@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/dfa/-/dfa-1.2.0.tgz#96ac3204e2d29c49ea5b57af8d92c2ae12790657" @@ -16341,6 +16342,11 @@ fast-safe-stringify@^2.0.7: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz#dc2af48c46cf712b683e849b2bbd446b32de936f" integrity sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag== +fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + fast-shallow-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b" @@ -16800,15 +16806,6 @@ fork-ts-checker-webpack-plugin@^6.0.4: semver "^7.3.2" tapable "^1.0.0" -form-data@^2.3.1: - version "2.5.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.0.tgz#094ec359dc4b55e7d62e0db4acd76e89fe874d37" - integrity sha512-WXieX3G/8side6VIqx44ablyULoGruSde5PNTxoUyo5CeyAMX6nVWUd0rgist/EuX655cjhUhTo1Fo3tRYqbcA== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - form-data@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" @@ -16848,10 +16845,15 @@ formdata-polyfill@^4.0.10: dependencies: fetch-blob "^3.1.2" -formidable@^1.2.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" - integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== +formidable@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.2.tgz#fa973a2bec150e4ce7cac15589d7a25fc30ebd89" + integrity sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g== + dependencies: + dezalgo "^1.0.4" + hexoid "^1.0.0" + once "^1.4.0" + qs "^6.11.0" formik@^2.2.9: version "2.2.9" @@ -17956,6 +17958,11 @@ heap@^0.2.6: resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.6.tgz#087e1f10b046932fc8594dd9e6d378afc9d1e5ac" integrity sha1-CH4fELBGky/IWU3Z5tN4r8nR5aw= +hexoid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + highlight.js@^10.1.1, highlight.js@~10.4.0: version "10.4.1" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0" @@ -21627,7 +21634,7 @@ merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -methods@^1.1.1, methods@^1.1.2, methods@~1.1.2: +methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= @@ -21742,11 +21749,16 @@ mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, dependencies: mime-db "1.51.0" -mime@1.6.0, mime@^1.4.1: +mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + mime@^2.4.4: version "2.5.2" resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" @@ -24634,13 +24646,20 @@ qs@6.9.7: resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== -qs@^6.10.0, qs@^6.5.1, qs@^6.7.0: +qs@^6.10.0, qs@^6.7.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== dependencies: side-channel "^1.0.4" +qs@^6.11.0: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + qs@~6.5.2: version "6.5.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" @@ -27920,21 +27939,21 @@ success-symbol@^0.1.0: resolved "https://registry.yarnpkg.com/success-symbol/-/success-symbol-0.1.0.tgz#24022e486f3bf1cdca094283b769c472d3b72897" integrity sha1-JAIuSG878c3KCUKDt2nEctO3KJc= -superagent@^3.8.2, superagent@^3.8.3: - version "3.8.3" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.3.tgz#460ea0dbdb7d5b11bc4f78deba565f86a178e128" - integrity sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA== +superagent@^8.0.5, superagent@^8.1.2: + version "8.1.2" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.1.2.tgz#03cb7da3ec8b32472c9d20f6c2a57c7f3765f30b" + integrity sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA== dependencies: - component-emitter "^1.2.0" - cookiejar "^2.1.0" - debug "^3.1.0" - extend "^3.0.0" - form-data "^2.3.1" - formidable "^1.2.0" - methods "^1.1.1" - mime "^1.4.1" - qs "^6.5.1" - readable-stream "^2.3.5" + component-emitter "^1.3.0" + cookiejar "^2.1.4" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.1.2" + methods "^1.1.2" + mime "2.6.0" + qs "^6.11.0" + semver "^7.3.8" supercluster@^8.0.1: version "8.0.1" @@ -27950,13 +27969,13 @@ superjson@^1.10.0: dependencies: copy-anything "^3.0.2" -supertest@^3.1.0: - version "3.4.2" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.4.2.tgz#bad7de2e43d60d27c8caeb8ab34a67c8a5f71aad" - integrity sha512-WZWbwceHUo2P36RoEIdXvmqfs47idNNZjCuJOqDz6rvtkk8ym56aU5oglORCpPeXGxT7l9rkJ41+O1lffQXYSA== +supertest@^6.3.3: + version "6.3.3" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.3.tgz#42f4da199fee656106fd422c094cf6c9578141db" + integrity sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA== dependencies: methods "^1.1.2" - superagent "^3.8.3" + superagent "^8.0.5" supports-color@8.1.1, supports-color@^8.0.0, supports-color@^8.1.1: version "8.1.1" @@ -28933,13 +28952,6 @@ undici@^5.21.2, undici@^5.22.1: dependencies: busboy "^1.6.0" -undici@^5.5.1: - version "5.20.0" - resolved "https://registry.yarnpkg.com/undici/-/undici-5.20.0.tgz#6327462f5ce1d3646bcdac99da7317f455bcc263" - integrity sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g== - dependencies: - busboy "^1.6.0" - unfetch@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be"