[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**

![settings before](https://github.com/user-attachments/assets/4bab48bd-e0b5-42eb-93fe-3faefdfc58bf)

**After**

![settings after](https://github.com/user-attachments/assets/b499dab0-0ee1-464a-8bda-cdbf5236b0d3)

#### 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:

![filter value is invalid](https://github.com/user-attachments/assets/39493dba-bf1d-4ce7-8480-15ee2ed599ea)

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:

![local_reset](https://github.com/user-attachments/assets/0a2d040f-c31a-40b0-8c16-04b7d333f73e)

, 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**
![01-up_to_100_alerts_before](https://github.com/user-attachments/assets/4143e847-5220-463b-8fb0-da5215d16b24)

**After**
![02-up_to_100_alerts_after](https://github.com/user-attachments/assets/835bd3fb-1e63-4192-b694-4595e8fa9309)

#### 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:
Andrew Macri 2025-02-18 15:21:59 -05:00 committed by GitHub
parent dee6931a3e
commit 05ae2b1cf8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 130 additions and 53 deletions

View file

@ -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]);

View file

@ -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}`
);

View file

@ -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}

View file

@ -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(),

View file

@ -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>

View file

@ -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

View file

@ -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}

View file

@ -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

View file

@ -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}

View file

@ -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(),

View file

@ -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}

View file

@ -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',
});
});
});

View file

@ -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;
}

View file

@ -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',
{

View file

@ -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" />,

View file

@ -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}

View file

@ -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