[Security Solution] Discover Security Profile Changes + Event timeline redirection (#204756)

## Summary

This PR customized Discover's Security Root Profile with 3 new changes.
\

With this change Discover's Security profile ceases to be
`experimental`. Below are the changes listed one in this profile.

>[!Note]
> This change also removes the Flyout changes that were added for
security solution to the Discover's security profile.


### Row Indicators.

Adds row indicators based on Alert or an event as shown in the
screenshot below. Note the highlights on the left of the row.

`Yellow` for Alerts.
`Gray` for Events.


![image](https://github.com/user-attachments/assets/697bafe9-3c45-4926-9b00-4bf6453e70eb)

### Default list of Columns.

Adds the list of default columns that should appear when security
profile is activated.


![image](https://github.com/user-attachments/assets/71647f3e-37da-4fab-b350-baa567b7e9bd)

### Explore in Security Flyout.

As shown the demo below, this action explores and event or an alert in
security solution as shown in the screenshot below.




https://github.com/user-attachments/assets/4c228bb1-60af-433b-8e8f-1a5d4d049985









### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### Identify risks

Does this PR introduce any risks? For example, consider risks like hard
to test bugs, performance regression, potential of data loss.

Describe the risk, its severity, and mitigation for each identified
risk. Invite stakeholders and evaluate how to proceed before merging.

- [ ] [See some risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
- [ ] ...

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Davis McPhee <davis.mcphee@elastic.co>
This commit is contained in:
Jatin Kathuria 2025-06-24 02:39:29 +02:00 committed by GitHub
parent 1c2995447f
commit fbec7db5b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1103 additions and 320 deletions

3
.github/CODEOWNERS vendored
View file

@ -2396,6 +2396,9 @@ x-pack/solutions/security/plugins/security_solution/public/asset_inventory @elas
x-pack/test/security_solution_api_integration/test_suites/siem_migrations @elastic/security-threat-hunting
/x-pack/test_serverless/functional/test_suites/security/ftr/discover @elastic/security-threat-hunting
x-pack/test_serverless/functional/test_suites/security/config.context_awareness.ts @elastic/security-threat-hunting
## Security Solution Threat Hunting areas - Threat Hunting Investigations
/x-pack/solutions/security/plugins/security_solution/common/api/tags @elastic/security-threat-hunting-investigations

View file

@ -29,6 +29,7 @@ import { createTracesDataSourceProfileProvider } from './observability/traces_da
import { createDeprecationLogsDataSourceProfileProvider } from './common/deprecation_logs';
import { createClassicNavRootProfileProvider } from './common/classic_nav_root_profile';
import { createObservabilityDocumentProfileProviders } from './observability/observability_profile_providers';
import { createSecurityDocumentProfileProvider } from './security/security_document_profile';
/**
* Register profile providers for root, data source, and document contexts to the profile profile services
@ -158,5 +159,6 @@ const createDataSourceProfileProviders = (providerServices: ProfileProviderServi
*/
const createDocumentProfileProviders = (providerServices: ProfileProviderServices) => [
createExampleDocumentProfileProvider(),
createSecurityDocumentProfileProvider(providerServices),
...createObservabilityDocumentProfileProviders(providerServices),
];

View file

@ -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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { createDefaultSecuritySolutionAppStateGetter } from './get_default_app_state';
describe('createDefaultSecuritySolutionAppStateGetter', () => {
it('should return default app state without security solution specific columns and breakdown field if there is no index match', () => {
const getDefaultAppState = createDefaultSecuritySolutionAppStateGetter();
const params = {
dataView: {
getIndexPattern: () => 'logs-*',
},
};
const prevAppState = { someKey: 'someValue' };
const prevAppStateGetter = () => prevAppState;
// @ts-expect-error - params should be compatible with the expected type
const appState = getDefaultAppState(prevAppStateGetter)(params);
expect(Object.keys(appState)).toMatchObject(['someKey']);
});
it('should return default app state with security solution specific columns and breakdown field if there is index match', () => {
const getDefaultAppState = createDefaultSecuritySolutionAppStateGetter();
const params = {
dataView: {
getIndexPattern: () => '.alerts-security.alerts-*',
},
};
const prevAppState = { someKey: 'someValue' };
const prevAppStateGetter = () => prevAppState;
// @ts-expect-error - params should be compatible with the expected type
const appState = getDefaultAppState(prevAppStateGetter)(params);
expect(appState).toEqual({
...prevAppState,
breakdownField: 'kibana.alert.workflow_status',
columns: [
{ name: '@timestamp', width: 218 },
{ name: 'kibana.alert.workflow_status' },
{ name: 'message', width: 360 },
{ name: 'event.category' },
{ name: 'event.action' },
{ name: 'host.name' },
{ name: 'source.ip' },
{ name: 'destination.ip' },
{ name: 'user.name' },
],
});
});
});

View file

@ -0,0 +1,55 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { RootProfileProvider } from '../../../profiles';
import { ALERTS_INDEX_PATTERN } from '../constants';
export const createDefaultSecuritySolutionAppStateGetter: () => RootProfileProvider['profile']['getDefaultAppState'] =
() => (prev) => (params) => {
const { dataView } = params;
const appState = { ...prev(params) };
if (!dataView.getIndexPattern().includes(ALERTS_INDEX_PATTERN)) {
return appState;
}
return {
...appState,
breakdownField: 'kibana.alert.workflow_status',
columns: [
{
name: '@timestamp',
width: 218,
},
{
name: 'kibana.alert.workflow_status',
},
{
name: 'message',
width: 360,
},
{
name: 'event.category',
},
{
name: 'event.action',
},
{
name: 'host.name',
},
{
name: 'source.ip',
},
{
name: 'destination.ip',
},
{
name: 'user.name',
},
],
};
};

View file

@ -0,0 +1,57 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { DataTableRecord } from '@kbn/discover-utils';
import { getAlertEventRowIndicator } from './get_row_indicator';
import type { EuiThemeComputed } from '@elastic/eui';
describe('getAlertEventRowIndicator', () => {
it('should return the correct color and label for an event row', () => {
const row = {
flattened: {
'event.kind': 'event',
},
} as unknown as DataTableRecord;
const euiTheme = {
colors: {
backgroundLightText: 'backgroundLightText',
},
} as const as EuiThemeComputed;
const result = getAlertEventRowIndicator(row, euiTheme);
expect(result).toEqual({
color: 'backgroundLightText',
label: 'event',
});
});
it('should return the correct color and label for an alert row', () => {
const row = {
flattened: {
'event.kind': 'signal',
},
} as unknown as DataTableRecord;
const euiTheme = {
colors: {
backgroundLightText: 'backgroundLightText',
warning: 'warning',
},
} as const as EuiThemeComputed;
const result = getAlertEventRowIndicator(row, euiTheme);
expect(result).toEqual({
color: 'warning',
label: 'alert',
});
});
});

View file

@ -0,0 +1,29 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { getFieldValue } from '@kbn/discover-utils';
import type { UnifiedDataTableProps } from '@kbn/unified-data-table';
export const getAlertEventRowIndicator: NonNullable<UnifiedDataTableProps['getRowIndicator']> = (
row,
euiTheme
) => {
let eventColor = euiTheme.colors.backgroundLightText;
let rowLabel = 'event';
if (getFieldValue(row, 'event.kind') === 'signal') {
eventColor = euiTheme.colors.warning;
rowLabel = 'alert';
}
return {
color: eventColor,
label: rowLabel,
};
};

View file

@ -0,0 +1,146 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { AlertEventOverview } from './alert_event_overview';
import type { DataTableRecord } from '@kbn/discover-utils';
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
import { EcsFlat } from '@elastic/ecs';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { encode } from '@kbn/rison';
import { URLSearchParams } from 'url';
jest.mock('../../../../hooks/use_discover_services');
const TEST_TIMELINE_URL = 'test-timeline-url';
const mockGetUrlForApp = jest.fn().mockReturnValue(TEST_TIMELINE_URL);
const mockDiscoverServices = {
application: {
getUrlForApp: mockGetUrlForApp,
},
};
const mockRow = {
'kibana.alert.reason': 'test-reason',
'kibana.alert.rule.description': 'test-description',
'event.kind': 'signal',
_id: 'test-id',
'@timestamp': '2021-08-02T14:00:00.000Z',
'kibana.alert.url': 'test-url',
};
const mockHit = {
flattened: mockRow,
} as unknown as DataTableRecord;
const mockDataView = dataViewMock;
describe('AlertEventOverview', () => {
beforeEach(() => {
(useDiscoverServices as jest.Mock).mockReturnValue(mockDiscoverServices);
});
describe('expandable sections', () => {
test('should return the expandable sections correctly', () => {
render(<AlertEventOverview hit={mockHit} dataView={mockDataView} />);
expect(screen.getByTestId('expandableHeader-About')).toBeVisible();
expect(screen.getByTestId('expandableContent-About')).toBeVisible();
fireEvent.click(screen.getByTestId('expandableHeader-About'));
expect(screen.getByTestId('expandableContent-About')).not.toBeVisible();
});
test('should show expected sections', () => {
render(<AlertEventOverview hit={mockHit} dataView={mockDataView} />);
expect(screen.getByTestId('expandableHeader-About')).toBeVisible();
expect(screen.getByTestId('expandableHeader-Description')).toBeVisible();
expect(screen.getByTestId('expandableContent-Description')).toHaveTextContent(
'test-description'
);
expect(screen.getByTestId('expandableHeader-Reason')).toBeVisible();
expect(screen.getByTestId('expandableContent-Reason')).toHaveTextContent('test-reason');
expect(screen.getByTestId('exploreSecurity')).toBeVisible();
expect(screen.getByTestId('exploreSecurity').getAttribute('href')).toBe('test-url');
});
});
describe('data', () => {
test('should return Ecs description for different event types correctly', () => {
const localMockHit = {
flattened: {
...mockRow,
'event.category': 'process',
},
} as unknown as DataTableRecord;
render(<AlertEventOverview hit={localMockHit} dataView={mockDataView} />);
expect(screen.getByTestId('expandableContent-About')).toHaveTextContent(
EcsFlat['event.category'].allowed_values.find((i) => i.name === 'process')
?.description as string
);
});
test('should display timeline redirect url correctly', () => {
const localMockHit = {
flattened: {
...mockRow,
'event.kind': 'event',
'event.category': 'process',
},
} as unknown as DataTableRecord;
render(<AlertEventOverview hit={localMockHit} dataView={mockDataView} />);
const expectedURLJSON = {
timeline: encode({
activeTab: 'query',
isOpen: true,
query: {
expression: '_id: test-id',
kind: 'kuery',
},
}),
timeRange: encode({
timeline: {
timerange: {
from: mockRow['@timestamp'],
to: mockRow['@timestamp'],
kind: 'absolute',
linkTo: false,
},
},
}),
timelineFlyout: encode({
right: {
id: 'document-details-right',
params: {
id: 'test-id',
scopeId: 'timeline-1',
},
},
}),
};
const searchParams = new URLSearchParams(
`timeline=${expectedURLJSON.timeline}&timerange=${expectedURLJSON.timeRange}&timelineFlyout=${expectedURLJSON.timelineFlyout}`
);
expect(screen.getByTestId('exploreSecurity').getAttribute('href')).toBe(
`test-timeline-url?${searchParams}`
);
});
});
});

View file

@ -0,0 +1,140 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useMemo, useState } from 'react';
import type { FC, PropsWithChildren } from 'react';
import { getFieldValue } from '@kbn/discover-utils';
import type { DocViewerComponent } from '@kbn/unified-doc-viewer/src/services/types';
import {
EuiTitle,
EuiSpacer,
EuiAccordion,
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiText,
} from '@elastic/eui';
import * as i18n from '../translations';
import { getSecurityTimelineRedirectUrl } from '../utils';
import { getEcsAllowedValueDescription } from '../utils/ecs_description';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
export const ExpandableSection: FC<PropsWithChildren<{ title: string }>> = ({
title,
children,
}) => {
const [trigger, setTrigger] = useState<'open' | 'closed'>('open');
const onToggle = (isOpen: boolean) => {
const newState = isOpen ? 'open' : 'closed';
setTrigger(newState);
};
return (
<EuiAccordion
id={`accordion-${title}`}
forceState={trigger}
onToggle={onToggle}
buttonContent={
<EuiTitle size="xs" data-test-subj={`expandableHeader-${title}`}>
<h4>{title}</h4>
</EuiTitle>
}
>
<EuiSpacer size="m" />
<EuiFlexGroup
gutterSize={'m'}
direction="column"
data-test-subj={`expandableContent-${title}`}
>
{children}
</EuiFlexGroup>
</EuiAccordion>
);
};
export const AlertEventOverview: DocViewerComponent = ({ hit }) => {
const {
application: { getUrlForApp },
} = useDiscoverServices();
const timelinesURL = getUrlForApp('securitySolutionUI', {
path: 'alerts',
});
const reason = useMemo(() => getFieldValue(hit, 'kibana.alert.reason') as string, [hit]);
const description = useMemo(
() => getFieldValue(hit, 'kibana.alert.rule.description') as string,
[hit]
);
const alertURL = useMemo(() => getFieldValue(hit, 'kibana.alert.url') as string, [hit]);
const eventKind = useMemo(() => getFieldValue(hit, 'event.kind') as string, [hit]);
const isAlert = useMemo(() => eventKind === 'signal', [eventKind]);
const eventId = useMemo(() => getFieldValue(hit, '_id') as string, [hit]);
const eventURL = useMemo(
() =>
getSecurityTimelineRedirectUrl({
from: getFieldValue(hit, '@timestamp') as string,
to: getFieldValue(hit, '@timestamp') as string,
eventId: eventId as string,
index: getFieldValue(hit, '_index') as string,
baseURL: timelinesURL,
}),
[hit, eventId, timelinesURL]
);
const eventCategory = useMemo(() => getFieldValue(hit, 'event.category') as string, [hit]);
return (
<EuiFlexGroup
data-test-subj={isAlert ? 'alertOverview' : 'eventOverview'}
gutterSize="m"
direction="column"
style={{ paddingBlock: '20px' }}
>
<EuiFlexItem>
<ExpandableSection title={i18n.aboutSectionTitle}>
<EuiText size="s" data-test-subj="about">
{getEcsAllowedValueDescription(eventCategory)}
</EuiText>
</ExpandableSection>
</EuiFlexItem>
{description ? (
<EuiFlexItem>
<ExpandableSection title={i18n.descriptionSectionTitle}>
<EuiText size="s" data-test-subj="description">
{description}
</EuiText>
</ExpandableSection>
</EuiFlexItem>
) : null}
{isAlert ? (
<EuiFlexItem>
<ExpandableSection title={i18n.reasonSectionTitle}>
<EuiText size="s" data-test-subj="reason">
{reason}
</EuiText>
</ExpandableSection>
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="exploreSecurity"
href={isAlert && alertURL ? alertURL : eventURL}
target="_blank"
iconType="link"
fill
aria-label={i18n.overviewExploreButtonLabel(isAlert)}
>
{i18n.overviewExploreButtonLabel(isAlert)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,19 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { withSuspense } from '@kbn/shared-ux-utility';
import { lazy } from 'react';
export const AlertEventOverviewLazy = withSuspense(
lazy(() =>
import('./alert_event_overview').then((module) => ({
default: module.AlertEventOverview,
}))
)
);

View file

@ -7,11 +7,9 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { SecuritySolutionAppWrapperFeature } from '@kbn/discover-shared-plugin/public';
export const createAppWrapperAccessor = async (
appWrapperFeature?: SecuritySolutionAppWrapperFeature
) => {
if (!appWrapperFeature) return undefined;
return appWrapperFeature.getWrapper();
export const SECURITY_PROFILE_ID = {
root: 'security-root-profile',
document: 'security-document-profile',
};
export const ALERTS_INDEX_PATTERN = '.alerts-security.alerts-';

View file

@ -0,0 +1,10 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { createSecurityDocumentProfileProvider } from './profile';

View file

@ -0,0 +1,58 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { getFieldValue } from '@kbn/discover-utils';
import type { DocumentProfileProvider } from '../../../profiles';
import { DocumentType, SolutionType } from '../../../profiles';
import type { ProfileProviderServices } from '../../profile_provider_services';
import { SECURITY_PROFILE_ID } from '../constants';
import * as i18n from '../translations';
import type { SecurityProfileProviderFactory } from '../types';
import { AlertEventOverviewLazy } from '../components';
export const createSecurityDocumentProfileProvider: SecurityProfileProviderFactory<
DocumentProfileProvider
> = (_services: ProfileProviderServices) => {
return {
profileId: SECURITY_PROFILE_ID.document,
experimental: true,
profile: {
getDocViewer: (prev) => (params) => {
const prevDocViewer = prev(params);
const isAlert = getFieldValue(params.record, 'event.kind') === 'signal';
return {
...prevDocViewer,
docViewsRegistry: (registry) => {
registry.add({
id: 'doc_view_alerts_overview',
title: i18n.overviewTabTitle(isAlert),
order: 0,
component: AlertEventOverviewLazy,
});
return prevDocViewer.docViewsRegistry(registry);
},
};
},
},
resolve: ({ rootContext }) => {
if (rootContext.solutionType !== SolutionType.Security) {
return { isMatch: false };
}
return {
isMatch: true,
context: {
type: DocumentType.Default,
},
};
},
};
};

View file

@ -8,53 +8,48 @@
*/
import type { FunctionComponent, PropsWithChildren } from 'react';
import React from 'react';
import type { DataGridCellValueElementProps } from '@kbn/unified-data-table';
import type { RootProfileProvider } from '../../../profiles';
import { SolutionType } from '../../../profiles';
import type { ProfileProviderServices } from '../../profile_provider_services';
import type { SecurityProfileProviderFactory } from '../types';
import { createCellRendererAccessor } from '../accessors/get_cell_renderer_accessor';
import { createAppWrapperAccessor } from '../accessors/create_app_wrapper_accessor';
import { createDefaultSecuritySolutionAppStateGetter as createDefaultSecuritySolutionAppStateGetter } from '../accessors/get_default_app_state';
import { getAlertEventRowIndicator } from '../accessors/get_row_indicator';
import { ALERTS_INDEX_PATTERN, SECURITY_PROFILE_ID } from '../constants';
interface SecurityRootProfileContext {
appWrapper?: FunctionComponent<PropsWithChildren<{}>>;
getCellRenderer?: (
getSecuritySolutionCellRenderer?: (
fieldName: string
) => FunctionComponent<DataGridCellValueElementProps> | undefined;
}
const EmptyAppWrapper: FunctionComponent<PropsWithChildren<{}>> = ({ children }) => <>{children}</>;
export const createSecurityRootProfileProvider: SecurityProfileProviderFactory<
RootProfileProvider<SecurityRootProfileContext>
> = (services: ProfileProviderServices) => {
const { discoverShared } = services;
const discoverFeaturesRegistry = discoverShared.features.registry;
const cellRendererFeature = discoverFeaturesRegistry.getById('security-solution-cell-renderer');
const appWrapperFeature = discoverFeaturesRegistry.getById('security-solution-app-wrapper');
return {
profileId: 'security-root-profile',
isExperimental: true,
profileId: SECURITY_PROFILE_ID.root,
profile: {
getRenderAppWrapper: (PrevWrapper, params) => {
const AppWrapper = params.context.appWrapper ?? EmptyAppWrapper;
return ({ children }) => (
<PrevWrapper>
<AppWrapper>{children}</AppWrapper>
</PrevWrapper>
);
},
getCellRenderers:
(prev, { context }) =>
(params) => {
const entries = prev(params);
['host.name', 'user.name', 'source.ip', 'destination.ip'].forEach((fieldName) => {
entries[fieldName] = context.getCellRenderer?.(fieldName) ?? entries[fieldName];
if (!params.dataView.getIndexPattern().includes(ALERTS_INDEX_PATTERN)) {
return entries;
}
['kibana.alert.workflow_status'].forEach((fieldName) => {
entries[fieldName] =
context.getSecuritySolutionCellRenderer?.(fieldName) ?? entries[fieldName];
});
return entries;
},
getRowIndicatorProvider: () => () => getAlertEventRowIndicator,
getDefaultAppState: createDefaultSecuritySolutionAppStateGetter(),
},
resolve: async (params) => {
if (params.solutionNavId !== SolutionType.Security) {
@ -63,15 +58,13 @@ export const createSecurityRootProfileProvider: SecurityProfileProviderFactory<
};
}
const getAppWrapper = await createAppWrapperAccessor(appWrapperFeature);
const getCellRenderer = await createCellRendererAccessor(cellRendererFeature);
return {
isMatch: true,
context: {
solutionType: SolutionType.Security,
appWrapper: getAppWrapper?.(),
getCellRenderer,
getSecuritySolutionCellRenderer: getCellRenderer,
},
};
},

View file

@ -0,0 +1,56 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { i18n } from '@kbn/i18n';
export const exploreRowActionLabel = (isAlert: boolean) =>
i18n.translate('discover.profile.security.rowAction.exploreButtonLabel', {
values: { isAlert },
defaultMessage: 'Explore {isAlert, select, true {Alert} other {Event}} in Security',
});
export const overviewTabTitle = (isAlert: boolean) =>
i18n.translate('discover.profile.security.flyout.overviewTabTitle', {
values: { isAlert },
defaultMessage: '{isAlert, select, true {Alert} other {Event}} Overview',
});
export const overviewExploreButtonLabel = (isAlert: boolean) =>
i18n.translate('discover.profile.security.flyout.overviewExploreButtonLabel', {
values: { isAlert },
defaultMessage: 'Explore in {isAlert, select, true {Alerts} other {Timeline}}',
});
export const noEcsDescriptionReason = i18n.translate(
'discover.profile.security.flyout.noEventKindDescriptionMessage',
{
defaultMessage: "This field doesn't have a description because it's not part of ECS.",
}
);
export const aboutSectionTitle = i18n.translate(
'discover.profile.security.flyout.aboutSectionTitle',
{
defaultMessage: 'About',
}
);
export const descriptionSectionTitle = i18n.translate(
'discover.profile.security.flyout.descriptionSectionTitle',
{
defaultMessage: 'Description',
}
);
export const reasonSectionTitle = i18n.translate(
'discover.profile.security.flyout.reasonSectionTitle',
{
defaultMessage: 'Reason',
}
);

View file

@ -0,0 +1,25 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { EcsFlat } from '@elastic/ecs';
import * as i18n from '../translations';
export type EcsAllowedValue = (typeof EcsFlat)['event.category']['allowed_values'][0];
/**
* Helper function to return the description of an allowed value of the specified field
* @param fieldName
* @param value
* @returns ecs description of the value
*/
export const getEcsAllowedValueDescription = (value: string): string => {
const allowedValues: EcsAllowedValue[] = EcsFlat['event.category']?.allowed_values ?? [];
const result =
allowedValues?.find((item) => item.name === value)?.description ?? i18n.noEcsDescriptionReason;
return result;
};

View file

@ -0,0 +1,79 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { encode } from '@kbn/rison';
export interface CustomQuery {
kind: 'kuery' | 'lucene';
expression: string;
}
export interface TimelineRedirectArgs {
from?: string;
to?: string;
eventId?: string;
index: string;
baseURL: string;
}
export const getSecurityTimelineRedirectUrl = ({
from,
to,
index,
eventId,
baseURL,
}: TimelineRedirectArgs) => {
let timelineTimerangeSearchParam = {};
if (from && to) {
timelineTimerangeSearchParam = {
timeline: {
timerange: {
from,
to,
kind: 'absolute',
linkTo: false,
},
},
};
}
const query: CustomQuery = {
kind: 'kuery',
expression: `_id: ${eventId}`,
};
const timelineSearchParam = {
activeTab: 'query',
query,
isOpen: true,
};
const timelineFlyoutSearchParam = {
right: {
id: 'document-details-right',
params: {
id: eventId,
indexName: index,
scopeId: 'timeline-1',
},
},
};
const encodedTimelineParam = encode(timelineSearchParam);
const encodedTimelineTimerangeParam = encode(timelineTimerangeSearchParam);
const encodedTimelineFlyoutParam = encode(timelineFlyoutSearchParam);
const urlParams = new URLSearchParams({
timeline: encodedTimelineParam,
timerange: encodedTimelineTimerangeParam,
timelineFlyout: encodedTimelineFlyoutParam,
});
return `${baseURL}?${urlParams.toString()}`;
};

View file

@ -103,11 +103,14 @@
"@kbn/embeddable-enhanced-plugin",
"@kbn/shared-ux-page-analytics-no-data-types",
"@kbn/core-application-browser-mocks",
"@kbn/rison",
"@kbn/unified-tabs",
"@kbn/unified-histogram",
"@kbn/alerts-ui-shared",
"@kbn/core-pricing-browser-mocks",
"@kbn/css-utils"
],
"exclude": ["target/**/*"]
"exclude": [
"target/**/*"
]
}

View file

@ -186,7 +186,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await addSearchToDashboard('logst*-ss-_bytes-runtimefield');
await addSearchToDashboard('logst*-ss-_bytes-runtimefield-updated');
const [firstSearchCell, secondSearchCell] = await dataGrid.getAllCellElements(0, 3);
const [firstSearchCell, secondSearchCell] = await dataGrid.getAllCellElementsByColumnName(
0,
'_bytes-runtimefield'
);
const first = await firstSearchCell.getVisibleText();
const second = await secondSearchCell.getVisibleText();

View file

@ -174,9 +174,13 @@ export class DataGridService extends FtrService {
);
}
private getCellElementByColumnNameSelector(rowIndex: number, columnName: string) {
return `[data-test-subj="euiDataGridBody"] [data-test-subj="dataGridRowCell"][data-gridcell-column-id="${columnName}"][data-gridcell-visible-row-index="${rowIndex}"]`;
}
public async getCellElementByColumnName(rowIndex: number, columnName: string) {
return await this.find.byCssSelector(
`[data-test-subj="euiDataGridBody"] [data-test-subj="dataGridRowCell"][data-gridcell-column-id="${columnName}"][data-gridcell-visible-row-index="${rowIndex}"]`
this.getCellElementByColumnNameSelector(rowIndex, columnName)
);
}
@ -317,6 +321,12 @@ export class DataGridService extends FtrService {
return await this.find.allByCssSelector(this.getCellElementSelector(rowIndex, columnIndex));
}
public async getAllCellElementsByColumnName(rowIndex: number, columnName: string) {
return await this.find.allByCssSelector(
this.getCellElementByColumnNameSelector(rowIndex, columnName)
);
}
public async getDocCount(): Promise<number> {
const grid = await this.find.byCssSelector('[data-document-number]');
return Number(await grid.getAttribute('data-document-number'));

View file

@ -642,6 +642,10 @@ describe('HomePage', () => {
expect(mockUpdateUrlParam).toHaveBeenCalledWith({
activeTab: 'query',
isOpen: false,
query: {
expression: '',
kind: 'kuery',
},
});
});

View file

@ -43,10 +43,10 @@ const initializeTimerangeFromUrlParam = (
) => {
if (initialState != null) {
const globalLinkTo: LinkTo = { linkTo: get('global.linkTo', initialState) };
const globalType: TimeRangeKinds = get('global.timerange.kind', initialState);
const globalTimerangeKind: TimeRangeKinds = get('global.timerange.kind', initialState);
const timelineLinkTo: LinkTo = { linkTo: get('timeline.linkTo', initialState) };
const timelineType: TimeRangeKinds = get('timeline.timerange.kind', initialState);
const timelineTimerangeKind: TimeRangeKinds = get('timeline.timerange.kind', initialState);
const socTrendsLinkTo: LinkTo = { linkTo: get('socTrends.linkTo', initialState) };
const socTrendsType: TimeRangeKinds = get('socTrends.timerange.kind', initialState);
@ -76,8 +76,8 @@ const initializeTimerangeFromUrlParam = (
dispatch(inputsActions.addLinkTo([InputsModelId.global, InputsModelId.timeline]));
}
if (timelineType) {
if (timelineType === 'absolute') {
if (timelineTimerangeKind) {
if (timelineTimerangeKind === 'absolute') {
const absoluteRange = normalizeTimeRange<AbsoluteTimeRange>(
get('timeline.timerange', initialState)
);
@ -90,7 +90,7 @@ const initializeTimerangeFromUrlParam = (
);
}
if (timelineType === 'relative') {
if (timelineTimerangeKind === 'relative') {
const relativeRange = normalizeTimeRange<RelativeTimeRange>(
get('timeline.timerange', initialState)
);
@ -110,8 +110,8 @@ const initializeTimerangeFromUrlParam = (
}
}
if (globalType) {
if (globalType === 'absolute') {
if (globalTimerangeKind) {
if (globalTimerangeKind === 'absolute') {
const absoluteRange = normalizeTimeRange<AbsoluteTimeRange>(
get('global.timerange', initialState)
);
@ -123,7 +123,7 @@ const initializeTimerangeFromUrlParam = (
})
);
}
if (globalType === 'relative') {
if (globalTimerangeKind === 'relative') {
const relativeRange = normalizeTimeRange<RelativeTimeRange>(
get('global.timerange', initialState)
);

View file

@ -32,6 +32,7 @@ export const useInitTimelineFromUrlParam = () => {
timelineId: initialState.id,
openTimeline: initialState.isOpen,
savedSearchId: initialState.savedSearchId,
query: initialState.query,
});
}
},
@ -48,7 +49,7 @@ export const useInitTimelineFromUrlParam = () => {
const parsedState = safeDecode(timelineState) as TimelineUrl | null;
// Make sure we only re-initialize the timeline if there are siginificant changes to the active timeline.
// Make sure we only re-initialize the timeline if there are significant changes to the active timeline.
// Without this check, we could potentially overwrite the timeline.
if (!hasTimelineStateChanged(activeTimeline, parsedState)) {
onInitialize(parsedState);

View file

@ -17,7 +17,7 @@ import { URL_PARAM_KEY } from '../use_url_state';
export const useSyncTimelineUrlParam = () => {
const updateUrlParam = useUpdateUrlParam<TimelineUrl>(URL_PARAM_KEY.timeline);
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const { activeTab, show, savedObjectId, savedSearchId } = useShallowEqualSelector(
const { activeTab, show, savedObjectId, savedSearchId, kqlQuery } = useShallowEqualSelector(
(state) => getTimeline(state, TimelineId.active) ?? {}
);
@ -27,7 +27,11 @@ export const useSyncTimelineUrlParam = () => {
isOpen: show,
activeTab,
savedSearchId: savedSearchId ? savedSearchId : undefined,
query: {
kind: kqlQuery?.filterQuery?.kuery?.kind ?? 'kuery',
expression: kqlQuery?.filterQuery?.kuery?.expression ?? '',
},
};
updateUrlParam(params);
}, [activeTab, savedObjectId, show, updateUrlParam, savedSearchId]);
}, [activeTab, savedObjectId, show, updateUrlParam, savedSearchId, kqlQuery]);
};

View file

@ -16,11 +16,11 @@ import { useQueryTimelineByIdOnUrlChange } from './timeline/use_query_timeline_b
export const useUrlState = () => {
useSyncGlobalQueryString();
useInitSearchBarFromUrlParams();
useInitTimerangeFromUrlParam();
useUpdateTimerangeOnPageChange();
useInitTimelineFromUrlParam();
useSyncTimelineUrlParam();
useQueryTimelineByIdOnUrlChange();
useInitTimerangeFromUrlParam();
useUpdateTimerangeOnPageChange();
};
export { URL_PARAM_KEY } from './constants';

View file

@ -1,131 +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, { useMemo } from 'react';
import { ExpandableFlyoutProvider } from '@kbn/expandable-flyout';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { KibanaContextProvider, useKibana } from '@kbn/kibana-react-plugin/public';
import { NavigationProvider } from '@kbn/security-solution-navigation';
import type { CoreStart } from '@kbn/core/public';
import type { SecuritySolutionAppWrapperFeature } from '@kbn/discover-shared-plugin/public';
import type { DiscoverServices } from '@kbn/discover-plugin/public';
import { CellActionsProvider } from '@kbn/cell-actions';
import { APP_ID } from '../../../common';
import { SecuritySolutionFlyout } from '../../flyout';
import { StatefulEventContext } from '../../common/components/events_viewer/stateful_event_context';
import type { SecurityAppStore } from '../../common/store';
import { ReactQueryClientProvider } from '../../common/containers/query_client/query_client_provider';
import type { StartPluginsDependencies, StartServices } from '../../types';
import { MlCapabilitiesProvider } from '../../common/components/ml/permissions/ml_capabilities_provider';
import { UserPrivilegesProvider } from '../../common/components/user_privileges/user_privileges_context';
import { DiscoverInTimelineContextProvider } from '../../common/components/discover_in_timeline/provider';
import { UpsellingProvider } from '../../common/components/upselling_provider';
import { ConsoleManager } from '../../management/components/console';
import { AssistantProvider } from '../../assistant/provider';
import { ONE_DISCOVER_SCOPE_ID } from '../constants';
export const createSecuritySolutionDiscoverAppWrapperGetter = ({
core,
services,
plugins,
store,
}: {
core: CoreStart;
services: StartServices;
plugins: StartPluginsDependencies;
/**
* instance of Security App store that should be used in Discover
*/
store: SecurityAppStore;
}) => {
const getSecuritySolutionDiscoverAppWrapper: Awaited<
ReturnType<SecuritySolutionAppWrapperFeature['getWrapper']>
> = () => {
return function SecuritySolutionDiscoverAppWrapper({ children }) {
const { services: discoverServices } = useKibana<DiscoverServices>();
const CasesContext = useMemo(() => plugins.cases.ui.getCasesContext(), []);
const userCasesPermissions = useMemo(() => plugins.cases.helpers.canUseCases([APP_ID]), []);
/**
*
* Since this component is meant to be used only in the context of Discover,
* these services are appended/overwritten to the existing services object
* provided by the Discover plugin.
*
*/
const securitySolutionServices: StartServices = useMemo(
() => ({
...services,
/* Helps with getting correct instance of query, timeFilter and filterManager instances from discover */
data: discoverServices.data,
}),
[discoverServices]
);
const statefulEventContextValue = useMemo(
() => ({
// timelineId acts as scopeId
timelineID: ONE_DISCOVER_SCOPE_ID,
enableHostDetailsFlyout: true,
/* behaviour similar to query tab */
tabType: 'query',
enableIpDetailsFlyout: true,
}),
[]
);
return (
<KibanaContextProvider services={securitySolutionServices}>
<EuiThemeProvider>
<MlCapabilitiesProvider>
<CasesContext owner={[APP_ID]} permissions={userCasesPermissions}>
<UserPrivilegesProvider kibanaCapabilities={services.application.capabilities}>
{/* ^_^ Needed for notes addition */}
<NavigationProvider core={core}>
<CellActionsProvider
getTriggerCompatibleActions={services.uiActions.getTriggerCompatibleActions}
>
{/* ^_^ Needed for Cell Actions since it gives errors when CellActionsContext is used */}
<UpsellingProvider upsellingService={services.upselling}>
{/* ^_^ Needed for Alert Preview from Expanded Section of Entity Flyout */}
<ReduxStoreProvider store={store}>
<ReactQueryClientProvider>
<ConsoleManager>
{/* ^_^ Needed for AlertPreview -> Alert Details Flyout Action */}
<AssistantProvider>
{/* ^_^ Needed for AlertPreview -> Alert Details Flyout Action */}
<DiscoverInTimelineContextProvider>
{/* ^_^ Needed for Add to Timeline action by `useRiskInputActions`*/}
<ExpandableFlyoutProvider>
<SecuritySolutionFlyout />
{/* vv below context should not be here and should be removed */}
<StatefulEventContext.Provider
value={statefulEventContextValue}
>
{children}
</StatefulEventContext.Provider>
</ExpandableFlyoutProvider>
</DiscoverInTimelineContextProvider>
</AssistantProvider>
</ConsoleManager>
</ReactQueryClientProvider>
</ReduxStoreProvider>
</UpsellingProvider>
</CellActionsProvider>
</NavigationProvider>
</UserPrivilegesProvider>
</CasesContext>
</MlCapabilitiesProvider>
</EuiThemeProvider>
</KibanaContextProvider>
);
};
};
return getSecuritySolutionDiscoverAppWrapper;
};

View file

@ -42,16 +42,17 @@ describe('getCellRendererForGivenRecord', () => {
});
it('should return cell renderer correctly for allowed fields with correct data format', () => {
const cellRenderer = getCellRendererForGivenRecord('host.name');
const cellRenderer = getCellRendererForGivenRecord('kibana.alert.workflow_status');
expect(cellRenderer).toBeDefined();
const props: DataGridCellValueElementProps = {
columnId: 'host.name',
columnId: 'kibana.alert.workflow_status',
isDetails: false,
isExpanded: false,
row: {
id: '1',
raw: {},
flattened: {
'kibana.alert.workflow_status': 'open',
'host.name': 'host1',
'user.name': 'user1',
},
@ -72,6 +73,7 @@ describe('getCellRendererForGivenRecord', () => {
isTimeline: false,
isDetails: false,
data: [
{ field: 'kibana.alert.workflow_status', value: ['open'] },
{ field: 'host.name', value: ['host1'] },
{ field: 'user.name', value: ['user1'] },
],
@ -79,7 +81,7 @@ describe('getCellRendererForGivenRecord', () => {
scopeId: 'one-discover',
linkValues: undefined,
header: {
id: 'host.name',
id: 'kibana.alert.workflow_status',
columnHeaderType: 'not-filtered',
type: 'string',
},
@ -92,7 +94,7 @@ describe('getCellRendererForGivenRecord', () => {
isExpandable: false,
isExpanded: false,
setCellProps: props.setCellProps,
columnId: 'host.name',
columnId: 'kibana.alert.workflow_status',
},
{}
);

View file

@ -18,7 +18,13 @@ export type SecuritySolutionRowCellRendererGetter = Awaited<
ReturnType<SecuritySolutionCellRendererFeature['getRenderer']>
>;
const ALLOWED_DISCOVER_RENDERED_FIELDS = ['host.name', 'user.name', 'source.ip', 'destination.ip'];
/**
*
* This controls the list of fields that are allowed custom security solution rendering
* in Discover's contextual View
*
*/
const ALLOWED_DISCOVER_RENDERED_FIELDS = ['kibana.alert.workflow_status'];
export const getCellRendererForGivenRecord: SecuritySolutionRowCellRendererGetter = (
fieldName: string

View file

@ -6,4 +6,3 @@
*/
export { getCellRendererForGivenRecord } from './cell_renderers';
export { createSecuritySolutionDiscoverAppWrapperGetter } from './app_wrapper';

View file

@ -20,10 +20,7 @@ import type {
import { AppStatus, DEFAULT_APP_CATEGORIES } from '@kbn/core/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { uiMetricService } from '@kbn/cloud-security-posture-common/utils/ui_metrics';
import type {
SecuritySolutionAppWrapperFeature,
SecuritySolutionCellRendererFeature,
} from '@kbn/discover-shared-plugin/public/services/discover_features';
import type { SecuritySolutionCellRendererFeature } from '@kbn/discover-shared-plugin/public/services/discover_features';
import { ProductFeatureSecurityKey } from '@kbn/security-solution-features/keys';
import { ProductFeatureAssistantKey } from '@kbn/security-solution-features/src/product_features_keys';
import type { ExternalReferenceAttachmentType } from '@kbn/cases-plugin/public/client/attachment_framework/types';
@ -81,7 +78,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
// Lazily instantiated dependencies
private _subPlugins?: SubPlugins;
private _store?: SecurityAppStore;
private _securityStoreForDiscover?: SecurityAppStore;
private _actionsRegistered?: boolean = false;
constructor(private readonly initializerContext: PluginInitializerContext) {
@ -237,7 +233,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
const externalAttachmentType: ExternalReferenceAttachmentType = generateAttachmentType();
cases?.attachmentFramework?.registerExternalReference(externalAttachmentType);
this.registerDiscoverSharedFeatures(core, plugins);
this.registerDiscoverSharedFeatures(plugins);
return this.contract.getSetupContract();
}
@ -253,10 +249,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
this.services.stop();
}
public async registerDiscoverSharedFeatures(
core: CoreSetup<StartPluginsDependencies, PluginStart>,
plugins: SetupPlugins
) {
public async registerDiscoverSharedFeatures(plugins: SetupPlugins) {
const { discoverShared } = plugins;
const discoverFeatureRegistry = discoverShared.features.registry;
const cellRendererFeature: SecuritySolutionCellRendererFeature = {
@ -267,33 +260,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
},
};
const appWrapperFeature: SecuritySolutionAppWrapperFeature = {
id: 'security-solution-app-wrapper',
getWrapper: async () => {
const [coreStart, startPlugins] = await core.getStartServices();
const services = await this.services.generateServices(coreStart, startPlugins);
const subPlugins = await this.startSubPlugins(this.storage, coreStart, startPlugins);
const securityStoreForDiscover = await this.getStoreForDiscover(
coreStart,
startPlugins,
subPlugins
);
const { createSecuritySolutionDiscoverAppWrapperGetter } =
await this.getLazyDiscoverSharedDeps();
return createSecuritySolutionDiscoverAppWrapperGetter({
core: coreStart,
services,
plugins: startPlugins,
store: securityStoreForDiscover,
});
},
};
discoverFeatureRegistry.register(cellRendererFeature);
discoverFeatureRegistry.register(appWrapperFeature);
}
public async getLazyDiscoverSharedDeps() {
@ -395,31 +362,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
return this._store;
}
/**
* Lazily instantiate a `SecurityAppStore` for discover.
*/
private async getStoreForDiscover(
coreStart: CoreStart,
startPlugins: StartPlugins,
subPlugins: StartedSubPlugins
): Promise<SecurityAppStore> {
if (!this._securityStoreForDiscover) {
const { createStoreFactory } = await this.lazyApplicationDependencies();
this._securityStoreForDiscover = await createStoreFactory(
coreStart,
startPlugins,
subPlugins,
this.storage,
this.experimentalFeatures
);
}
if (startPlugins.timelines) {
startPlugins.timelines.setTimelineEmbeddedStore(this._securityStoreForDiscover);
}
return this._securityStoreForDiscover;
}
private async registerActions(
store: SecurityAppStore,
history: H.History,

View file

@ -725,6 +725,10 @@ describe('helpers', () => {
timelineType: TimelineTypeEnum.default,
onOpenTimeline,
openTimeline: true,
query: {
kind: 'kuery',
expression: 'foo: bar',
},
};
(resolveTimeline as jest.Mock).mockResolvedValue(untitledTimeline);
renderHook(async () => {
@ -738,6 +742,12 @@ describe('helpers', () => {
id: TimelineId.active,
timeline: expect.objectContaining({
columns: defaultUdtHeaders,
kqlQuery: {
filterQuery: {
serializedQuery: 'foo: bar',
kuery: { expression: 'foo: bar', kind: 'kuery' },
},
},
}),
})
);

View file

@ -10,7 +10,7 @@ import { getOr } from 'lodash/fp';
import { v4 as uuidv4 } from 'uuid';
import deepMerge from 'deepmerge';
import { useDiscoverInTimelineContext } from '../../../common/components/discover_in_timeline/use_discover_in_timeline_context';
import type { ColumnHeaderOptions } from '../../../../common/types/timeline';
import type { ColumnHeaderOptions, KueryFilterQuery } from '../../../../common/types/timeline';
import { TimelineId, TimelineTabs } from '../../../../common/types/timeline';
import type {
ColumnHeaderResult,
@ -298,6 +298,8 @@ export interface QueryTimelineById {
onOpenTimeline?: (timeline: TimelineModel) => void;
openTimeline?: boolean;
savedSearchId?: string;
/* Lucene or Kql query */
query?: KueryFilterQuery;
}
export const useQueryTimelineById = () => {
@ -313,6 +315,7 @@ export const useQueryTimelineById = () => {
onOpenTimeline,
openTimeline = true,
savedSearchId,
query,
}: QueryTimelineById) => {
if (timelineId == null) {
updateTimeline({
@ -325,6 +328,12 @@ export const useQueryTimelineById = () => {
...timelineDefaults,
columns: defaultUdtHeaders,
id: TimelineId.active,
kqlQuery: {
filterQuery: {
kuery: query ?? null,
serializedQuery: query?.expression ?? '',
},
},
activeTab: activeTimelineTab,
show: openTimeline,
initialized: true,

View file

@ -15,6 +15,7 @@ import type {
SerializedFilterQuery,
SortColumnTimeline,
TimelineEventsType,
KueryFilterQuery,
TimelineTabs,
} from '../../../common/types/timeline';
import type {
@ -200,4 +201,5 @@ export interface TimelineUrl {
isOpen: boolean;
graphEventId?: string;
savedSearchId?: string;
query?: KueryFilterQuery;
}

View file

@ -596,7 +596,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const selectedDataView = await dataViews.getSelectedName();
expect(selectedDataView).to.be.equal('search-source-*');
const documentCell = await dataGrid.getCellElement(0, 3);
const documentCell = await dataGrid.getCellElementByColumnName(0, '_source');
const firstRowContent = await documentCell.getVisibleText();
expect(firstRowContent.includes('runtime-message-fieldmock-message')).to.be.equal(true);
@ -610,7 +610,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const selectedDataView = await dataViews.getSelectedName();
expect(selectedDataView).to.be.equal('search-source-*');
const documentCell = await dataGrid.getCellElement(0, 3);
const documentCell = await dataGrid.getCellElementByColumnName(0, '_source');
const firstRowContent = await documentCell.getVisibleText();
expect(firstRowContent.includes('runtime-message-fieldmock-message')).to.be.equal(true);
});

View file

@ -235,7 +235,10 @@ describe('url state', { tags: ['@ess', '@skipInServerless'] }, () => {
.should('have.attr', 'href')
.and(
'contain',
`/app/security/network?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))`
"/app/security/network?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))" +
"&query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')" +
"&timeline=(activeTab:query,isOpen:!f,query:(expression:'',kind:kuery))" +
"&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))"
);
});
it('sets KQL in host page and detail page and check if href match on breadcrumb, tabs and subTabs', () => {
@ -249,13 +252,19 @@ describe('url state', { tags: ['@ess', '@skipInServerless'] }, () => {
.should('have.attr', 'href')
.and(
'contain',
`/app/security/hosts?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))`
"/app/security/hosts?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))" +
"&query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')" +
"&timeline=(activeTab:query,isOpen:!f,query:(expression:'',kind:kuery))" +
"&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))"
);
cy.get(NETWORK)
.should('have.attr', 'href')
.and(
'contain',
`/app/security/network?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))`
"/app/security/network?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))" +
"&query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')" +
"&timeline=(activeTab:query,isOpen:!f,query:(expression:'',kind:kuery))" +
"&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))"
);
toggleNavigationPanel(EXPLORE_PANEL_BTN);
cy.get(HOSTS_NAMES).first().should('have.text', 'siem-kibana');
@ -268,7 +277,9 @@ describe('url state', { tags: ['@ess', '@skipInServerless'] }, () => {
.should('have.attr', 'href')
.and(
'contain',
"/app/security/hosts/name/siem-kibana/anomalies?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))"
"/app/security/hosts/name/siem-kibana/anomalies?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))" +
"&timeline=(activeTab:query,isOpen:!f,query:(expression:'',kind:kuery))" +
"&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))"
);
cy.get(BREADCRUMBS)
@ -276,14 +287,20 @@ describe('url state', { tags: ['@ess', '@skipInServerless'] }, () => {
.should('have.attr', 'href')
.and(
'contain',
`/app/security/hosts?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))`
"/app/security/hosts?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))" +
"&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')" +
"&timeline=(activeTab:query,isOpen:!f,query:(expression:'',kind:kuery))" +
"&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))"
);
cy.get(BREADCRUMBS)
.eq(3)
.should('have.attr', 'href')
.and(
'contain',
`/app/security/hosts/name/siem-kibana?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))`
"/app/security/hosts/name/siem-kibana?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))" +
"&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')" +
"&timeline=(activeTab:query,isOpen:!f,query:(expression:'',kind:kuery))" +
"&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))"
);
});

View file

@ -189,7 +189,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await addSearchToDashboard('logst*-ss-_bytes-runtimefield');
await addSearchToDashboard('logst*-ss-_bytes-runtimefield-updated');
const [firstSearchCell, secondSearchCell] = await dataGrid.getAllCellElements(0, 3);
const [firstSearchCell, secondSearchCell] = await dataGrid.getAllCellElementsByColumnName(
0,
'_bytes-runtimefield'
);
const first = await firstSearchCell.getVisibleText();
const second = await secondSearchCell.getVisibleText();

View file

@ -617,7 +617,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const selectedDataView = await dataViews.getSelectedName();
expect(selectedDataView).to.be.equal('search-source-*');
const documentCell = await dataGrid.getCellElement(0, 3);
const documentCell = await dataGrid.getCellElementByColumnName(0, '_source');
const firstRowContent = await documentCell.getVisibleText();
expect(firstRowContent.includes('runtime-message-fieldmock-message')).to.be.equal(true);
@ -631,7 +631,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const selectedDataView = await dataViews.getSelectedName();
expect(selectedDataView).to.be.equal('search-source-*');
const documentCell = await dataGrid.getCellElement(0, 3);
const documentCell = await dataGrid.getCellElementByColumnName(0, '_source');
const firstRowContent = await documentCell.getVisibleText();
expect(firstRowContent.includes('runtime-message-fieldmock-message')).to.be.equal(true);
});

View file

@ -14,9 +14,6 @@ export default createTestConfig({
reportName:
'Serverless Security Discover Context Awareness Functional Tests - Security Profiles',
},
kbnServerArgs: [
`--discover.experimental.enabledProfiles=${JSON.stringify(['security-root-profile'])}`,
],
// include settings from project controller
// https://github.com/elastic/elasticsearch-controller/blob/main/helm/values.yaml
esServerArgs: ['xpack.ml.dfa.enabled=false'],

View file

@ -6,6 +6,8 @@
*/
export const SECURITY_ES_ARCHIVES_DIR = 'x-pack/test/security_solution_cypress/es_archives';
export const SECURITY_SOLUTION_DATA_VIEW =
'.alerts-security.alerts-default,apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-*';
export const CLOUD_SECURITY_POSTURE_PACKAGE_VERSION = '1.13.0';
// This version of the CSPM package is used in the Serverless Quality Gates environment

View file

@ -5,82 +5,50 @@
* 2.0.
*/
import kbnRison from '@kbn/rison';
import expect from '@kbn/expect';
import path from 'path';
import { ServerlessRoleName } from '../../../../../../shared/lib';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
import { SECURITY_ES_ARCHIVES_DIR } from '../../../constants';
import { getDiscoverESQLState } from './utils';
import { SECURITY_SOLUTION_DATA_VIEW } from '../../../constants';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'timePicker', 'discover', 'svlCommonPage']);
const testSubjects = getService('testSubjects');
const dataViews = getService('dataViews');
const esArchiver = getService('esArchiver');
const queryBar = getService('queryBar');
// Failing: See https://github.com/elastic/kibana/issues/204139
describe.skip('security root profile', () => {
describe('cell renderer', () => {
before(async () => {
await PageObjects.svlCommonPage.loginAsViewer();
await esArchiver.loadIfNeeded(path.join(SECURITY_ES_ARCHIVES_DIR, 'auditbeat_single'));
await PageObjects.svlCommonPage.loginWithRole(ServerlessRoleName.PLATFORM_ENGINEER);
await PageObjects.common.navigateToApp('security', {
path: 'alerts',
});
});
after(async () => {
await esArchiver.unload(path.join(SECURITY_ES_ARCHIVES_DIR, 'auditbeat_single'));
describe('ES|QL mode', () => {
it('should render alert workflow status badge', async () => {
const state = getDiscoverESQLState(
`from ${SECURITY_SOLUTION_DATA_VIEW} | WHERE host.name == "siem-kibana" and event.kind != "signal"`
);
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
const alertWorkflowStatus = await testSubjects.findAll('rule-status-badge', 2500);
expect(alertWorkflowStatus).to.have.length(1);
});
});
describe('cell renderers', () => {
describe('host.name', () => {
describe('DataView mode', () => {
it('should open host.name flyout', async () => {
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await dataViews.createFromSearchBar({
name: 'auditbeat-2022',
adHoc: true,
hasTimeField: true,
});
await queryBar.setQuery('host.name: "siem-kibana"');
await queryBar.clickQuerySubmitButton();
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.dragFieldToTable('host.name');
expect((await PageObjects.discover.getColumnHeaders()).join(', ')).to.be(
'@timestamp, host.name'
);
// security host.name button
const hostName = await testSubjects.findAll('host-details-button', 2500);
expect(hostName).to.have.length(1);
await hostName[0].click();
await testSubjects.existOrFail('host-panel-header', { timeout: 2500 });
await testSubjects.existOrFail('asset-criticality-selector', { timeout: 2500 });
await testSubjects.existOrFail('observedEntity-accordion', { timeout: 2500 });
});
});
describe('ES|QL mode', () => {
it('should open host.name flyout', async () => {
const state = kbnRison.encode({
dataSource: { type: 'esql' },
query: { esql: 'from auditbeat-2022 | WHERE host.name == "siem-kibana"' },
});
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.dragFieldToTable('host.name');
expect((await PageObjects.discover.getColumnHeaders()).join(', ')).to.be('host.name');
// security host.name button
const hostName = await testSubjects.findAll('host-details-button', 2500);
expect(hostName).to.have.length(1);
await hostName[0].click();
await testSubjects.existOrFail('host-panel-header', { timeout: 2500 });
await testSubjects.existOrFail('asset-criticality-selector', { timeout: 2500 });
await testSubjects.existOrFail('observedEntity-accordion', { timeout: 2500 });
});
describe('DataView mode', () => {
it('should render alert workflow status badge', async () => {
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await PageObjects.discover.selectIndexPattern(SECURITY_SOLUTION_DATA_VIEW);
await queryBar.setQuery('host.name: "siem-kibana" AND event.kind: "signal"');
await queryBar.clickQuerySubmitButton();
await PageObjects.discover.waitUntilSearchingHasFinished();
const alertWorkflowStatus = await testSubjects.findAll('rule-status-badge', 2500);
expect(alertWorkflowStatus).to.have.length(1);
});
});
});

View file

@ -0,0 +1,72 @@
/*
* 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 expect from '@kbn/expect';
import { ServerlessRoleName } from '../../../../../../shared/lib';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
import { getDiscoverESQLState } from './utils';
import { SECURITY_SOLUTION_DATA_VIEW } from '../../../constants';
const defaultEventColumns = [
'@timestamp',
'kibana.alert.workflow_status',
'message',
'event.category',
'event.action',
'host.name',
'source.ip',
'destination.ip',
'user.name',
];
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'timePicker', 'discover', 'svlCommonPage']);
const queryBar = getService('queryBar');
const retry = getService('retry');
describe('default State', () => {
before(async () => {
await PageObjects.svlCommonPage.loginWithRole(ServerlessRoleName.PLATFORM_ENGINEER);
// creates security data view if it does not exist
await PageObjects.common.navigateToApp('security', {
path: 'alerts',
});
});
describe('ES|QL mode', () => {
it('should have correct list of columns', async () => {
const state = getDiscoverESQLState();
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
await retry.try(async () => {
expect((await PageObjects.discover.getColumnHeaders()).join(', ')).to.be(
defaultEventColumns.join(', ')
);
});
});
});
describe('DataView mode', () => {
it('should have correct list of columns', async () => {
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await PageObjects.discover.selectIndexPattern(SECURITY_SOLUTION_DATA_VIEW);
await queryBar.clickQuerySubmitButton();
await PageObjects.discover.waitUntilSearchingHasFinished();
expect((await PageObjects.discover.getColumnHeaders()).join(', ')).to.be(
defaultEventColumns.join(', ')
);
});
});
});
}

View file

@ -5,19 +5,46 @@
* 2.0.
*/
import moment from 'moment';
import { v4 as uuidv4 } from 'uuid';
import path from 'path';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
import { SECURITY_ES_ARCHIVES_DIR } from '../../../constants';
export default function ({ getService, getPageObjects, loadTestFile }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects(['timePicker', 'svlCommonPage']);
const securitySolutionApi = getService('securitySolutionApi');
const from = '2017-06-10T14:00:00.000Z';
const to = '2024-06-10T16:30:00.000Z';
// next day to include alerts generated in the tests
const to = moment().add(1, 'day').toISOString();
describe('discover/security/context_awareness', function () {
this.tags(['esGate']);
before(async () => {
await esArchiver.loadIfNeeded(path.join(SECURITY_ES_ARCHIVES_DIR, 'auditbeat_single'));
const testRunUuid = uuidv4();
const ruleName = `Test Rule - ${testRunUuid}`;
await securitySolutionApi.createRule({
body: {
name: ruleName,
description: 'test rule',
type: 'query',
enabled: true,
query: '_id: *',
index: ['auditbeat-*'],
from: 'now-10y',
interval: '1m',
severity: 'high',
risk_score: 70,
},
});
await esArchiver.load(
'src/platform/test/functional/fixtures/es_archiver/discover/context_awareness'
);
@ -36,9 +63,14 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid
await kibanaServer.importExport.unload(
'src/platform/test/functional/fixtures/kbn_archiver/discover/context_awareness'
);
await PageObjects.timePicker.resetDefaultAbsoluteRangeViaUiSettings();
await esArchiver.unload(path.join(SECURITY_ES_ARCHIVES_DIR, 'auditbeat_single'));
});
loadTestFile(require.resolve('./default_state'));
loadTestFile(require.resolve('./cell_renderer'));
loadTestFile(require.resolve('./row_indicator'));
});
}

View file

@ -0,0 +1,80 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { ServerlessRoleName } from '../../../../../../shared/lib';
import { FtrProviderContext } from '../../../../../ftr_provider_context';
import { SECURITY_SOLUTION_DATA_VIEW } from '../../../constants';
import { getDiscoverESQLState } from './utils';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'timePicker', 'discover', 'svlCommonPage']);
const queryBar = getService('queryBar');
const find = getService('find');
describe('security document profile', () => {
before(async () => {
await PageObjects.svlCommonPage.loginAsViewer();
});
describe('row indicators', () => {
describe('alerts and events', () => {
before(async () => {
await PageObjects.svlCommonPage.loginWithRole(ServerlessRoleName.PLATFORM_ENGINEER);
await PageObjects.common.navigateToApp('security', {
path: 'alerts',
});
});
describe('DataView mode', () => {
it('should have row indicator for both event and alert', async () => {
await PageObjects.common.navigateToActualUrl('discover', undefined, {
ensureCurrentUrl: false,
});
await PageObjects.discover.selectIndexPattern(SECURITY_SOLUTION_DATA_VIEW);
await queryBar.clickQuerySubmitButton();
await PageObjects.discover.waitUntilSearchingHasFinished();
expect(
await find.existsByCssSelector(
'[data-test-subj="unifiedDataTableRowColorIndicatorCell"][title="alert"]'
)
).to.eql(true);
expect(
await find.existsByCssSelector(
'[data-test-subj="unifiedDataTableRowColorIndicatorCell"][title="event"]'
)
).to.eql(true);
});
});
describe('ES|QL mode', () => {
it('should have row indicator for both event and alert', async () => {
const state = getDiscoverESQLState();
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
ensureCurrentUrl: false,
});
await PageObjects.discover.waitUntilSearchingHasFinished();
expect(
await find.existsByCssSelector(
'[data-test-subj="unifiedDataTableRowColorIndicatorCell"][title="alert"]'
)
).to.eql(true);
expect(
await find.existsByCssSelector(
'[data-test-subj="unifiedDataTableRowColorIndicatorCell"][title="event"]'
)
).to.eql(true);
});
});
});
});
});
}

View file

@ -0,0 +1,18 @@
/*
* 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 kbnRison from '@kbn/rison';
import { SECURITY_SOLUTION_DATA_VIEW } from '../../../constants';
export const getDiscoverESQLState = (query?: string) => {
return kbnRison.encode({
dataSource: { type: 'esql' },
query: {
esql: query ?? `FROM ${SECURITY_SOLUTION_DATA_VIEW} | WHERE host.name == "siem-kibana"`,
},
});
};