mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Security Solution] [Attack discovery] Fixes alerts filtering issues (#211371)
### [Security Solution] [Attack discovery] Fixes alerts filtering issues This PR resolves the following Attack discovery alerts filtering issues: - [[Security Solution] [Bug] A few filters show error 'Unexpected error from Elasticsearch' for the alerts flyout #208481](https://github.com/elastic/kibana/issues/208481) - [[Security Solution] [Bug] Lucene not updated as the space holder when we apply Lucene as the filtering language #208170](https://github.com/elastic/kibana/issues/208170) - Connector selection resets in non-default spaces - Saving a filter edited via `Edit Query DSL` with an unknown `user.name` value results in a `filter value is invalid or incomplete` filter - Local field Reset clears the preview dropdowns when they are in an error state - Updates the formatting of `Up to _n_ alerts` for Borealis #### Feature flag required for testing The following feature flag is required to test this PR: ```yaml xpack.securitySolution.enableExperimental: - 'attackDiscoveryAlertFiltering' ``` The following sections provide details and desk testing steps for the alerts filtering issues fixed by this PR. ### [Security Solution] [Bug] A few filters show error 'Unexpected error from Elasticsearch' for the alerts flyout #208481 To resolve [[Security Solution] [Bug] A few filters show error 'Unexpected error from Elasticsearch' for the alerts flyout #208481](https://github.com/elastic/kibana/issues/208481): - The `_ignored` metadata field was added to the [METADATA](https://www.elastic.co/guide/en/elasticsearch/reference/current/esql-metadata-fields.html) keyword in the `ES|QL` queries that power the `Alert summary` and `Alerts preview` tabs - The `Alert summary` and `Alerts preview` tabs dropdowns are now filtered to only display fields in the alerts index - Example: Previously, if the `dll.Ext.load_index` field was in a `logs-*` index, but not the alerts index, it would still appear in the `Alerts summary` dropdown, and result in an error if selected. After the fix in this PR, this (example) field would not appear in the dropdown. #### Desk testing To desk test this fix: - See issue <https://github.com/elastic/kibana/issues/208481> - In addition to the steps in [issue #208481](https://github.com/elastic/kibana/issues/208481), verify the `Alert summary` and `Alerts preview` dropdowns - Do NOT display an error when the `_ignored` field is selected - Identify a field that is present in a `logs-*` index, but not the alerts index, for example `dll.Ext.load_index`. Verify this field is NOT displayed in the dropdown ### [Security Solution] [Bug] Lucene not updated as the space holder when we apply Lucene as the filtering language #208170 To resolve [[Security Solution] [Bug] Lucene not updated as the space holder when we apply Lucene as the filtering language #208170](https://github.com/elastic/kibana/issues/208170), the custom placeholder was removed, as illustrated by the _before_ and _after_ screenshots below: **Before**  **After**  #### Desk testing To desk test this fix, see <https://github.com/elastic/kibana/issues/208170> ### Connector selection resets in non-default spaces This PR fixes an issue where the last selected connector would reset in non-default spaces when all of the following were true: - The user is in a non-default space - Two or more generative AI connectors are configured This issue occurred in non-default spaces because: - `spaceId` loads asynchronously - Kibana's `package.json` references an older version of `react-use`, with a known bug in the `useLocalStorage` hook, which is fixed by <https://github.com/streamich/react-use/pull/1944> - I verified (locally) the fix from <https://github.com/streamich/react-use/pull/1944> would work if Kibana's version of `react-use` was updated in `package.json`, however that effort appears to be on hold: <https://github.com/elastic/kibana/pull/179268> . For now (to minimize changes), `spaceId` has been removed from all Attack discovery local storage keys. #### Desk testing 1. Create a new space (if you only have the default space) 2. Configure two or more Gen AI connectors 3. Select the newly created space 4. Navigate to Security > Attack discovery 5. Select a connector, for example `Claude 3.5 Sonnet` 6. Now select a _different_ connector, for example `Gemini 1.5 Pro 002` 7. Navigate to a different page in the Security solution, for example Security > Alerts 9. Once again, navigate to Security > Attack discovery **Expected result** - The previously selected connector, e.g. `Gemini 1.5 Pro 002` is still selected ### Saving a filter edited via `Edit Query DSL` with an unknown `user.name` value results in a `filter value is invalid or incomplete` filter This PR fixes an issue where editing a previously created non-Query DSL filter via `Edit Query DSL`, and then entering an unknown `user.name`, resulted in a filter with text that reads: `filter value is invalid or incomplete`, as illustrated by the screenshot below:  Generating attack discoveries with a filter like the one shown in the screenshot above would also result in errors. This issue was resolved by adding a `FilterManager` to manage the local state of the filters in the settings panel. #### Desk testing 1. Navigate to Security > Attack discovery 2. Click the settings gear 3. Click the `+` button to open the `Add filter` popover 4. In the popover, configure a `user.name` `is` `Administrator` filter Note: replace `Administrator` with a real `user.name` value if your alerts index doesn't have the value ``Administrator`` 5. Click `Add filter` to close the popover **Expected result** - The `user.name: Administrator` filter appears below the query bar 6. Click the `user.name: Administrator` filter, and choose `Edit filter` from the popover 7. Click the `Edit as Query DSL` button (in the upper right hand corner) 8. In the `Edit filter` Elasticsearch Query DSL editor, edit the Query DSL such that it has a value that does NOT exist in the index, like the following example: ```json { "match_phrase": { "user.name": "Admasdfinistrator" } } ``` 9. Click `Update filter` **Expected results** - The `user.name: Admasdfinistrator` filter, which references a value that does not exist in the alerts index, appears below the query bar - The updated filter does NOT have the text `filter value is invalid or incomplete`, as illustrated by the the screenshot in the description of this issue above. ### Local field Reset clears the preview dropdowns when they are in an error state This PR fixes an issue where the local (to the preview tab) reset button did not clear the preview dropdowns if they were in an error state. The issue is fixed by calling `clearSearchValue()` to reset the stack by field when it's in an error state (i.e. because an invalid field was entered) Note: The "local" (to the tab) `Reset` button shown in the screenshot below is fixed by this PR:  , however the `Reset` button at the bottom of the flyout will NOT clear the dropdown if it's in an error state. (For now, this is the expected behavior.) The workaround is to manually select a valid value in the dropdown, or click `Save` or `Cancel`. (The preview dropdown does not effect the Attack discovery query, is not saved, and automatically resets to the default every time the flyout is opened.) #### Desk testing 1. Navigate to Security > Attack discovery 2. Click the settings gear 3. In the `Alert summary` tab, focus the dropdown and delete the text until it reads: ``` kibana.alert.rule.na ``` 4. Blur the dropdown by clicking outside it **Expected results** - The dropdown is highlighted red - The `Reset` button appears below the text `Select a field` 5. Click the `Reset` button below the text `Select a field` **Expected results** - The dropdown is NOT highlighted red (the error state is cleared) - The dropdown text is reset to the (valid) default value: `kibana.alert.rule.name` ### Updates the formatting of `Up to _n_ alerts` for Borealis This PR updates the formatting of the `Up to n alerts` text in Borealis, as illustrated by the before and after screenshots below: **Before**  **After**  #### Desk testing To desk test this fix: 1. Configure Kibana to use the `dark` theme 2. Navigate to Security > Attack discovery 3. In the connector selector, choose `+ Add new Connector...` 4. Click in the `Select a connector` dialog, click `OpenAI` 4. Enter a throwaway configuration for the connector (note: you won't actually use it), and click `Save` **Expected results** - The animated `Up to 100 alerts will be analyzed` message will appear - The color of the animated numeric text, e.g. `100` matches the color of the `Up to` text that precedes it - The extra whitespace trailing the `100`, shown in the _Before_ image (above) does NOT appear. The trailing whitespace after the `100` looks like the _After_ image (also above).
This commit is contained in:
parent
dee6931a3e
commit
05ae2b1cf8
17 changed files with 130 additions and 53 deletions
|
@ -27,7 +27,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
|
||||
import { AlertsSettings } from './alerts_settings';
|
||||
import { useSpaceId } from '../../../../common/hooks/use_space_id';
|
||||
import { Footer } from '../../settings_flyout/footer';
|
||||
import { getIsTourEnabled } from './is_tour_enabled';
|
||||
import * as i18n from './translations';
|
||||
|
@ -45,7 +44,6 @@ const SettingsModalComponent: React.FC<Props> = ({
|
|||
localStorageAttackDiscoveryMaxAlerts,
|
||||
setLocalStorageAttackDiscoveryMaxAlerts,
|
||||
}) => {
|
||||
const spaceId = useSpaceId() ?? 'default';
|
||||
const modalTitleId = useGeneratedHtmlId();
|
||||
|
||||
const [maxAlerts, setMaxAlerts] = useState(
|
||||
|
@ -68,7 +66,7 @@ const SettingsModalComponent: React.FC<Props> = ({
|
|||
}, [closeModal, maxAlerts, setLocalStorageAttackDiscoveryMaxAlerts]);
|
||||
|
||||
const [showSettingsTour, setShowSettingsTour] = useLocalStorage<boolean>(
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${spaceId}.${SHOW_SETTINGS_TOUR_LOCAL_STORAGE_KEY}.v8.16`,
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${SHOW_SETTINGS_TOUR_LOCAL_STORAGE_KEY}.v8.16`,
|
||||
true
|
||||
);
|
||||
const onTourFinished = useCallback(() => setShowSettingsTour(() => false), [setShowSettingsTour]);
|
||||
|
|
|
@ -30,7 +30,6 @@ import useLocalStorage from 'react-use/lib/useLocalStorage';
|
|||
import { SecurityPageName } from '../../../common/constants';
|
||||
import { HeaderPage } from '../../common/components/header_page';
|
||||
import { useInvalidFilterQuery } from '../../common/hooks/use_invalid_filter_query';
|
||||
import { useSpaceId } from '../../common/hooks/use_space_id';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
import { convertToBuildEsQuery } from '../../common/lib/kuery';
|
||||
import { SpyRoute } from '../../common/utils/route/spy_routes';
|
||||
|
@ -53,8 +52,6 @@ const AttackDiscoveryPageComponent: React.FC = () => {
|
|||
services: { uiSettings },
|
||||
} = useKibana();
|
||||
|
||||
const spaceId = useSpaceId() ?? 'default';
|
||||
|
||||
const {
|
||||
assistantFeatures: { attackDiscoveryAlertFiltering },
|
||||
http,
|
||||
|
@ -72,17 +69,17 @@ const AttackDiscoveryPageComponent: React.FC = () => {
|
|||
|
||||
// time selection:
|
||||
const [start, setStart] = useLocalStorage<string>(
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${spaceId}.${START_LOCAL_STORAGE_KEY}`,
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${START_LOCAL_STORAGE_KEY}`,
|
||||
DEFAULT_START
|
||||
);
|
||||
const [end, setEnd] = useLocalStorage<string>(
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${spaceId}.${END_LOCAL_STORAGE_KEY}`,
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${END_LOCAL_STORAGE_KEY}`,
|
||||
DEFAULT_END
|
||||
);
|
||||
|
||||
// search bar query:
|
||||
const [query, setQuery] = useLocalStorage<Query>(
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${spaceId}.${QUERY_LOCAL_STORAGE_KEY}`,
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${QUERY_LOCAL_STORAGE_KEY}`,
|
||||
getDefaultQuery(),
|
||||
{
|
||||
raw: false,
|
||||
|
@ -93,7 +90,7 @@ const AttackDiscoveryPageComponent: React.FC = () => {
|
|||
|
||||
// search bar filters:
|
||||
const [filters, setFilters] = useLocalStorage<Filter[]>(
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${spaceId}.${FILTERS_LOCAL_STORAGE_KEY}`,
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${FILTERS_LOCAL_STORAGE_KEY}`,
|
||||
[],
|
||||
{
|
||||
raw: false,
|
||||
|
@ -107,12 +104,12 @@ const AttackDiscoveryPageComponent: React.FC = () => {
|
|||
// get the last selected connector ID from local storage:
|
||||
const [localStorageAttackDiscoveryConnectorId, setLocalStorageAttackDiscoveryConnectorId] =
|
||||
useLocalStorage<string>(
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${spaceId}.${CONNECTOR_ID_LOCAL_STORAGE_KEY}`
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${CONNECTOR_ID_LOCAL_STORAGE_KEY}`
|
||||
);
|
||||
|
||||
const [localStorageAttackDiscoveryMaxAlerts, setLocalStorageAttackDiscoveryMaxAlerts] =
|
||||
useLocalStorage<string>(
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${spaceId}.${MAX_ALERTS_LOCAL_STORAGE_KEY}`,
|
||||
`${DEFAULT_ASSISTANT_NAMESPACE}.${ATTACK_DISCOVERY_STORAGE_KEY}.${MAX_ALERTS_LOCAL_STORAGE_KEY}`,
|
||||
`${DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS}`
|
||||
);
|
||||
|
||||
|
|
|
@ -27,9 +27,8 @@ const AnimatedCounterComponent: React.FC<Props> = ({ animationDurationMs = 1000
|
|||
|
||||
const text = svg
|
||||
.append('text')
|
||||
.attr('x', 3)
|
||||
.attr('y', 26)
|
||||
.attr('fill', euiTheme.colors.text)
|
||||
.attr('y', 24)
|
||||
.attr('fill', euiTheme.colors.textHeading)
|
||||
.text(zero);
|
||||
|
||||
text
|
||||
|
@ -45,14 +44,14 @@ const AnimatedCounterComponent: React.FC<Props> = ({ animationDurationMs = 1000
|
|||
})
|
||||
.duration(animationDurationMs);
|
||||
}
|
||||
}, [animationDurationMs, count, euiTheme.colors.text]);
|
||||
}, [animationDurationMs, count, euiTheme.colors.textHeading]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
css={css`
|
||||
height: 32px;
|
||||
margin-right: ${euiTheme.size.xs};
|
||||
width: ${count < 100 ? 40 : 60}px;
|
||||
width: ${count < 100 ? 32 : 48}px;
|
||||
`}
|
||||
data-test-subj="animatedCounter"
|
||||
ref={d3Ref}
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { FilterManager } from '@kbn/data-plugin/public';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { AlertSelectionQuery } from '.';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
|
@ -24,6 +25,7 @@ const mockUseSourcererDataView = useSourcererDataView as jest.MockedFunction<
|
|||
describe('AlertSelectionQuery', () => {
|
||||
const defaultProps = {
|
||||
end: 'now',
|
||||
filterManager: jest.fn() as unknown as FilterManager,
|
||||
filters: [],
|
||||
query: { query: '', language: 'kuery' },
|
||||
setEnd: jest.fn(),
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import type { OnTimeChangeProps } from '@elastic/eui';
|
||||
import { EuiSuperDatePicker, EuiSpacer } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import type { FilterManager } from '@kbn/data-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { Filter, Query } from '@kbn/es-query';
|
||||
import { debounce } from 'lodash/fp';
|
||||
|
@ -17,7 +18,6 @@ import { useKibana } from '../../../../../common/lib/kibana';
|
|||
import { getCommonTimeRanges } from '../helpers/get_common_time_ranges';
|
||||
import { useSourcererDataView } from '../../../../../sourcerer/containers';
|
||||
import { SourcererScopeName } from '../../../../../sourcerer/store/model';
|
||||
import * as i18n from '../translations';
|
||||
import { useDataView } from '../use_data_view';
|
||||
|
||||
export const MAX_ALERTS = 500;
|
||||
|
@ -27,10 +27,10 @@ export const NO_INDEX_PATTERNS: DataView[] = [];
|
|||
|
||||
interface Props {
|
||||
end: string;
|
||||
filterManager: FilterManager;
|
||||
filters: Filter[];
|
||||
query: Query;
|
||||
setEnd: React.Dispatch<React.SetStateAction<string>>;
|
||||
setFilters: React.Dispatch<React.SetStateAction<Filter[]>>;
|
||||
setQuery: React.Dispatch<React.SetStateAction<Query>>;
|
||||
setStart: React.Dispatch<React.SetStateAction<string>>;
|
||||
start: string;
|
||||
|
@ -38,10 +38,10 @@ interface Props {
|
|||
|
||||
const AlertSelectionQueryComponent: React.FC<Props> = ({
|
||||
end,
|
||||
filterManager,
|
||||
filters,
|
||||
query,
|
||||
setEnd,
|
||||
setFilters,
|
||||
setQuery,
|
||||
setStart,
|
||||
start,
|
||||
|
@ -129,9 +129,9 @@ const AlertSelectionQueryComponent: React.FC<Props> = ({
|
|||
*/
|
||||
const onFiltersUpdated = useCallback(
|
||||
(newFilters: Filter[]) => {
|
||||
setFilters(newFilters);
|
||||
filterManager.setFilters(newFilters);
|
||||
},
|
||||
[setFilters]
|
||||
[filterManager]
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -171,7 +171,6 @@ const AlertSelectionQueryComponent: React.FC<Props> = ({
|
|||
debouncedOnQueryChange(debouncedQuery?.query);
|
||||
}}
|
||||
onQuerySubmit={onQuerySubmit}
|
||||
placeholder={i18n.FILTER_YOUR_DATA}
|
||||
query={query}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -16,7 +16,7 @@ describe('getAlertSummaryEsqlQuery', () => {
|
|||
});
|
||||
|
||||
expect(query).toBe(
|
||||
`FROM alerts-* METADATA _id, _index, _version
|
||||
`FROM alerts-* METADATA _id, _index, _version, _ignored
|
||||
| WHERE kibana.alert.workflow_status IN ("open", "acknowledged") AND kibana.alert.rule.building_block_type IS NULL
|
||||
| SORT kibana.alert.risk_score DESC, @timestamp DESC
|
||||
| LIMIT 100
|
||||
|
@ -35,7 +35,7 @@ describe('getAlertSummaryEsqlQuery', () => {
|
|||
});
|
||||
|
||||
expect(query).toBe(
|
||||
`FROM alerts-* METADATA _id, _index, _version
|
||||
`FROM alerts-* METADATA _id, _index, _version, _ignored
|
||||
| WHERE kibana.alert.workflow_status IN ("open", "acknowledged") AND kibana.alert.rule.building_block_type IS NULL
|
||||
| SORT kibana.alert.risk_score DESC, @timestamp DESC
|
||||
| LIMIT 100
|
||||
|
|
|
@ -15,7 +15,7 @@ export const getAlertSummaryEsqlQuery = ({
|
|||
alertsIndexPattern: string;
|
||||
maxAlerts: number;
|
||||
tableStackBy0: string;
|
||||
}): string => `FROM ${alertsIndexPattern} METADATA _id, _index, _version
|
||||
}): string => `FROM ${alertsIndexPattern} METADATA _id, _index, _version, _ignored
|
||||
| WHERE kibana.alert.workflow_status IN ("open", "acknowledged") AND kibana.alert.rule.building_block_type IS NULL
|
||||
| SORT kibana.alert.risk_score DESC, @timestamp DESC
|
||||
| LIMIT ${maxAlerts}
|
||||
|
|
|
@ -16,7 +16,7 @@ describe('getAlertsPreviewEsqlQuery', () => {
|
|||
});
|
||||
|
||||
expect(result).toBe(
|
||||
`FROM alerts-* METADATA _id, _index, _version
|
||||
`FROM alerts-* METADATA _id, _index, _version, _ignored
|
||||
| WHERE kibana.alert.workflow_status IN ("open", "acknowledged") AND kibana.alert.rule.building_block_type IS NULL
|
||||
| SORT kibana.alert.risk_score DESC, @timestamp DESC
|
||||
| LIMIT 10
|
||||
|
@ -33,7 +33,7 @@ describe('getAlertsPreviewEsqlQuery', () => {
|
|||
});
|
||||
|
||||
expect(result).toBe(
|
||||
`FROM alerts-* METADATA _id, _index, _version
|
||||
`FROM alerts-* METADATA _id, _index, _version, _ignored
|
||||
| WHERE kibana.alert.workflow_status IN ("open", "acknowledged") AND kibana.alert.rule.building_block_type IS NULL
|
||||
| SORT kibana.alert.risk_score DESC, @timestamp DESC
|
||||
| LIMIT 10
|
||||
|
@ -50,7 +50,7 @@ describe('getAlertsPreviewEsqlQuery', () => {
|
|||
});
|
||||
|
||||
expect(result).toBe(
|
||||
`FROM alerts-* METADATA _id, _index, _version
|
||||
`FROM alerts-* METADATA _id, _index, _version, _ignored
|
||||
| WHERE kibana.alert.workflow_status IN ("open", "acknowledged") AND kibana.alert.rule.building_block_type IS NULL
|
||||
| SORT kibana.alert.risk_score DESC, @timestamp DESC
|
||||
| LIMIT 10
|
||||
|
@ -67,7 +67,7 @@ describe('getAlertsPreviewEsqlQuery', () => {
|
|||
});
|
||||
|
||||
expect(result).toBe(
|
||||
`FROM alerts-* METADATA _id, _index, _version
|
||||
`FROM alerts-* METADATA _id, _index, _version, _ignored
|
||||
| WHERE kibana.alert.workflow_status IN ("open", "acknowledged") AND kibana.alert.rule.building_block_type IS NULL
|
||||
| SORT kibana.alert.risk_score DESC, @timestamp DESC
|
||||
| LIMIT 5
|
||||
|
@ -84,7 +84,7 @@ describe('getAlertsPreviewEsqlQuery', () => {
|
|||
});
|
||||
|
||||
expect(result).toBe(
|
||||
`FROM custom-alerts-* METADATA _id, _index, _version
|
||||
`FROM custom-alerts-* METADATA _id, _index, _version, _ignored
|
||||
| WHERE kibana.alert.workflow_status IN ("open", "acknowledged") AND kibana.alert.rule.building_block_type IS NULL
|
||||
| SORT kibana.alert.risk_score DESC, @timestamp DESC
|
||||
| LIMIT 10
|
||||
|
|
|
@ -15,7 +15,7 @@ export const getAlertsPreviewEsqlQuery = ({
|
|||
alertsIndexPattern: string;
|
||||
maxAlerts: number;
|
||||
tableStackBy0: string;
|
||||
}): string => `FROM ${alertsIndexPattern} METADATA _id, _index, _version
|
||||
}): string => `FROM ${alertsIndexPattern} METADATA _id, _index, _version, _ignored
|
||||
| WHERE kibana.alert.workflow_status IN ("open", "acknowledged") AND kibana.alert.rule.building_block_type IS NULL
|
||||
| SORT kibana.alert.risk_score DESC, @timestamp DESC
|
||||
| LIMIT ${maxAlerts}
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import type { FilterManager } from '@kbn/data-plugin/public';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { AlertSelection } from '.';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
@ -28,13 +29,13 @@ const defaultProps = {
|
|||
alertsPreviewStackBy0: 'defaultAlertPreview',
|
||||
alertSummaryStackBy0: 'defaultAlertSummary',
|
||||
end: '2024-10-01T00:00:00.000Z',
|
||||
filterManager: jest.fn() as unknown as FilterManager,
|
||||
filters: [],
|
||||
maxAlerts: 100,
|
||||
query: { query: '', language: 'kuery' },
|
||||
setAlertsPreviewStackBy0: jest.fn(),
|
||||
setAlertSummaryStackBy0: jest.fn(),
|
||||
setEnd: jest.fn(),
|
||||
setFilters: jest.fn(),
|
||||
setMaxAlerts: jest.fn(),
|
||||
setQuery: jest.fn(),
|
||||
setStart: jest.fn(),
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTab, EuiTabs, EuiText, EuiSpacer } from '@elastic/eui';
|
||||
import type { FilterManager } from '@kbn/data-plugin/public';
|
||||
import type { Filter, Query } from '@kbn/es-query';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
|
@ -18,13 +19,13 @@ interface Props {
|
|||
alertsPreviewStackBy0: string;
|
||||
alertSummaryStackBy0: string;
|
||||
end: string;
|
||||
filterManager: FilterManager;
|
||||
filters: Filter[];
|
||||
maxAlerts: number;
|
||||
query: Query;
|
||||
setAlertsPreviewStackBy0: React.Dispatch<React.SetStateAction<string>>;
|
||||
setAlertSummaryStackBy0: React.Dispatch<React.SetStateAction<string>>;
|
||||
setEnd: React.Dispatch<React.SetStateAction<string>>;
|
||||
setFilters: React.Dispatch<React.SetStateAction<Filter[]>>;
|
||||
setMaxAlerts: React.Dispatch<React.SetStateAction<string>>;
|
||||
setQuery: React.Dispatch<React.SetStateAction<Query>>;
|
||||
setStart: React.Dispatch<React.SetStateAction<string>>;
|
||||
|
@ -35,13 +36,13 @@ const AlertSelectionComponent: React.FC<Props> = ({
|
|||
alertsPreviewStackBy0,
|
||||
alertSummaryStackBy0,
|
||||
end,
|
||||
filterManager,
|
||||
filters,
|
||||
maxAlerts,
|
||||
query,
|
||||
setAlertsPreviewStackBy0,
|
||||
setAlertSummaryStackBy0,
|
||||
setEnd,
|
||||
setFilters,
|
||||
setMaxAlerts,
|
||||
setQuery,
|
||||
setStart,
|
||||
|
@ -95,10 +96,10 @@ const AlertSelectionComponent: React.FC<Props> = ({
|
|||
<EuiFlexItem grow={false}>
|
||||
<AlertSelectionQuery
|
||||
end={end}
|
||||
filterManager={filterManager}
|
||||
filters={filters}
|
||||
query={query}
|
||||
setEnd={setEnd}
|
||||
setFilters={setFilters}
|
||||
setQuery={setQuery}
|
||||
setStart={setStart}
|
||||
start={start}
|
||||
|
|
|
@ -14,6 +14,8 @@ import { TestProviders } from '../../../../../common/mock';
|
|||
import { useSignalIndex } from '../../../../../detections/containers/detection_engine/alerts/use_signal_index';
|
||||
import { useSourcererDataView } from '../../../../../sourcerer/containers';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
|
||||
jest.mock('../../../../../common/lib/kibana');
|
||||
jest.mock('../../../../../sourcerer/containers');
|
||||
jest.mock('../../../../../detections/containers/detection_engine/alerts/use_signal_index');
|
||||
|
@ -24,6 +26,10 @@ jest.mock('react-router-dom', () => ({
|
|||
}),
|
||||
withRouter: jest.fn(),
|
||||
}));
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
const mockUseKibana = useKibana as jest.MockedFunction<typeof useKibana>;
|
||||
const mockUseSourcererDataView = useSourcererDataView as jest.MockedFunction<
|
||||
|
@ -154,4 +160,22 @@ describe('PreviewTab', () => {
|
|||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('limits the fields in the StackByComboBox to the fields in the signal index', () => {
|
||||
render(
|
||||
<TestProviders>
|
||||
<PreviewTab {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
payload: {
|
||||
id: 'detections',
|
||||
selectedDataViewId: 'mock-signal-index',
|
||||
selectedPatterns: ['mock-signal-index'],
|
||||
shouldValidateSelectedPatterns: false,
|
||||
},
|
||||
type: 'x-pack/security_solution/local/sourcerer/SET_SELECTED_DATA_VIEW',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,13 +17,16 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useEuiComboBoxReset } from '../../../../../common/components/use_combo_box_reset';
|
||||
import { StackByComboBox } from '../../../../../detections/components/alerts_kpis/common/components';
|
||||
import { useSignalIndex } from '../../../../../detections/containers/detection_engine/alerts/use_signal_index';
|
||||
import type { LensAttributes } from '../../../../../common/components/visualization_actions/types';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import { sourcererActions } from '../../../../../sourcerer/store';
|
||||
import { SourcererScopeName } from '../../../../../sourcerer/store/model';
|
||||
import * as i18n from '../translations';
|
||||
import type { Sorting } from '../types';
|
||||
|
||||
|
@ -84,6 +87,7 @@ const PreviewTabComponent = ({
|
|||
const {
|
||||
euiTheme: { font },
|
||||
} = useEuiTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { signalIndexName } = useSignalIndex();
|
||||
|
||||
|
@ -116,7 +120,12 @@ const PreviewTabComponent = ({
|
|||
[esqlQuery, getLensAttributes, sorting, tableStackBy0]
|
||||
);
|
||||
|
||||
const onReset = useCallback(() => setTableStackBy0(RESET_FIELD), [setTableStackBy0]);
|
||||
const onReset = useCallback(() => {
|
||||
// clear the input when it's in an error state, i.e. because the user entered an invalid field:
|
||||
stackByField0ComboboxRef.current?.clearSearchValue();
|
||||
|
||||
setTableStackBy0(RESET_FIELD);
|
||||
}, [setTableStackBy0, stackByField0ComboboxRef]);
|
||||
|
||||
const actions = useMemo(
|
||||
() => [
|
||||
|
@ -144,6 +153,23 @@ const PreviewTabComponent = ({
|
|||
[actions, body, tableStackBy0]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (signalIndexName != null) {
|
||||
// Limit the fields in the StackByComboBox to the fields in the signal index.
|
||||
// NOTE: The page containing this component must also be a member of
|
||||
// `detectionsPaths` in `sourcerer/containers/sourcerer_paths.ts` for this
|
||||
// action to have any effect.
|
||||
dispatch(
|
||||
sourcererActions.setSelectedDataView({
|
||||
id: SourcererScopeName.detections,
|
||||
selectedDataViewId: signalIndexName,
|
||||
selectedPatterns: [signalIndexName],
|
||||
shouldValidateSelectedPatterns: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [dispatch, signalIndexName]);
|
||||
|
||||
if (signalIndexName == null) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -36,13 +36,6 @@ export const ALERT_SUMMARY = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const FILTER_YOUR_DATA = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.settingsFlyout.alertSelection.filterYourDataPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Filter your data using KQL syntax',
|
||||
}
|
||||
);
|
||||
|
||||
export const SELECT_FIELD = i18n.translate(
|
||||
'xpack.securitySolution.attackDiscovery.settingsFlyout.alertSelection.alertsTable.selectFieldLabel',
|
||||
{
|
||||
|
|
|
@ -52,6 +52,9 @@ describe('SettingsFlyout', () => {
|
|||
lens: {
|
||||
EmbeddableComponent: () => <div data-test-subj="mockEmbeddableComponent" />,
|
||||
},
|
||||
uiSettings: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
unifiedSearch: {
|
||||
ui: {
|
||||
SearchBar: () => <div data-test-subj="mockSearchBar" />,
|
||||
|
|
|
@ -14,16 +14,18 @@ import {
|
|||
EuiTitle,
|
||||
useGeneratedHtmlId,
|
||||
} from '@elastic/eui';
|
||||
import { FilterManager } from '@kbn/data-plugin/public';
|
||||
import { DEFAULT_ATTACK_DISCOVERY_MAX_ALERTS } from '@kbn/elastic-assistant';
|
||||
import { DEFAULT_END, DEFAULT_START } from '@kbn/elastic-assistant-common';
|
||||
import type { Filter, Query } from '@kbn/es-query';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { AlertSelection } from './alert_selection';
|
||||
import { Footer } from './footer';
|
||||
import * as i18n from './translations';
|
||||
import { getDefaultQuery } from '../helpers';
|
||||
import { getMaxAlerts } from './alert_selection/helpers/get_max_alerts';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { Footer } from './footer';
|
||||
import { getDefaultQuery } from '../helpers';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const DEFAULT_STACK_BY_FIELD = 'kibana.alert.rule.name';
|
||||
|
||||
|
@ -60,6 +62,9 @@ const SettingsFlyoutComponent: React.FC<Props> = ({
|
|||
prefix: 'attackDiscoverySettingsFlyoutTitle',
|
||||
});
|
||||
|
||||
const { uiSettings } = useKibana().services;
|
||||
const filterManager = useRef<FilterManager>(new FilterManager(uiSettings));
|
||||
|
||||
const [alertSummaryStackBy0, setAlertSummaryStackBy0] = useState<string>(DEFAULT_STACK_BY_FIELD);
|
||||
const [alertsPreviewStackBy0, setAlertsPreviewStackBy0] =
|
||||
useState<string>(DEFAULT_STACK_BY_FIELD);
|
||||
|
@ -110,6 +115,29 @@ const SettingsFlyoutComponent: React.FC<Props> = ({
|
|||
|
||||
const numericMaxAlerts = useMemo(() => getMaxAlerts(localMaxAlerts), [localMaxAlerts]);
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
|
||||
// init the Filter manager with the local filters:
|
||||
filterManager.current.setFilters(localFilters);
|
||||
|
||||
// subscribe to filter updates:
|
||||
const subscription = filterManager.current.getUpdates$().subscribe({
|
||||
next: () => {
|
||||
if (isSubscribed) {
|
||||
const newFilters = filterManager.current.getFilters();
|
||||
|
||||
setLocalFilters(newFilters);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [localFilters]);
|
||||
|
||||
return (
|
||||
<EuiFlyoutResizable
|
||||
aria-labelledby={flyoutTitleId}
|
||||
|
@ -133,13 +161,13 @@ const SettingsFlyoutComponent: React.FC<Props> = ({
|
|||
alertsPreviewStackBy0={alertsPreviewStackBy0}
|
||||
alertSummaryStackBy0={alertSummaryStackBy0}
|
||||
end={localEnd}
|
||||
filterManager={filterManager.current}
|
||||
filters={localFilters}
|
||||
maxAlerts={numericMaxAlerts}
|
||||
query={localQuery}
|
||||
setAlertsPreviewStackBy0={setAlertsPreviewStackBy0}
|
||||
setAlertSummaryStackBy0={setAlertSummaryStackBy0}
|
||||
setEnd={setLocalEnd}
|
||||
setFilters={setLocalFilters}
|
||||
setMaxAlerts={setLocalMaxAlerts}
|
||||
setQuery={setLocalQuery}
|
||||
setStart={setLocalStart}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { matchPath } from 'react-router-dom';
|
|||
import {
|
||||
CASES_PATH,
|
||||
ALERTS_PATH,
|
||||
ATTACK_DISCOVERY_PATH,
|
||||
HOSTS_PATH,
|
||||
USERS_PATH,
|
||||
NETWORK_PATH,
|
||||
|
@ -29,7 +30,12 @@ export const sourcererPaths = [
|
|||
OVERVIEW_PATH,
|
||||
];
|
||||
|
||||
const detectionsPaths = [ALERTS_PATH, `${RULES_PATH}/id/:id`, `${CASES_PATH}/:detailName`];
|
||||
const detectionsPaths = [
|
||||
ALERTS_PATH,
|
||||
`${RULES_PATH}/id/:id`,
|
||||
`${CASES_PATH}/:detailName`,
|
||||
ATTACK_DISCOVERY_PATH,
|
||||
];
|
||||
|
||||
export const getScopeFromPath = (
|
||||
pathname: string
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue