Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
5438e0b
incremental
logeekal Dec 17, 2024
20e2776
incremental save
logeekal Dec 17, 2024
cb34563
poc: Overview tab
logeekal Nov 21, 2024
986be47
feat: timeline redirect basic logic
logeekal Dec 18, 2024
6b16fda
feat: default columns + row indicator
logeekal Dec 18, 2024
7364df3
[CI] Auto-commit changed files from 'node scripts/notice'
kibanamachine Dec 18, 2024
137c4a4
make ci 🟢
logeekal Dec 18, 2024
68bb477
fix: types
logeekal Dec 18, 2024
19e2eb3
tests: add
logeekal Feb 3, 2025
0584947
test: unfocus
logeekal Feb 3, 2025
665b15a
fix: timeline redirect url + tests
logeekal Feb 17, 2025
f100b30
fix: timerange sync issue
logeekal Feb 17, 2025
79c2490
fix: housekeeping
logeekal Feb 17, 2025
0073e2f
fix: rebase issues
logeekal Feb 17, 2025
aef1bd4
fix: more rebase issues
logeekal Feb 17, 2025
841e7b9
fix: remove unnecessary files
logeekal Feb 18, 2025
12561c9
fix: tests
logeekal Feb 18, 2025
94fd20d
feat: add row action
logeekal Feb 28, 2025
fa73ec2
fix: additional files
logeekal Mar 21, 2025
7d9a639
fix: types + remove unnecessary files
logeekal Mar 21, 2025
61edf6a
tests
logeekal Mar 21, 2025
6423ec3
more tests housekeeping
logeekal Mar 21, 2025
7cb5f17
test: ftr context awareness
logeekal Mar 21, 2025
145b2b5
translations for label
logeekal Mar 21, 2025
c875217
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Mar 21, 2025
d580cc7
fix: add link button
logeekal Mar 28, 2025
81f9617
[CI] Auto-commit changed files from 'node scripts/yarn_deduplicate'
kibanamachine Mar 28, 2025
0142cf5
Merge branch 'main' into feat/timeline_redirect
logeekal Mar 28, 2025
aac8a4f
update codeowners
logeekal Mar 28, 2025
035df6e
fix: jest tests
logeekal Mar 28, 2025
48a3521
fix: FTR test
logeekal Mar 28, 2025
6f52ae9
fix: cypress
logeekal Mar 31, 2025
b2b9795
Merge branch 'main' into feat/timeline_redirect
logeekal Mar 31, 2025
9741ed1
fix: remove row action + add flyout overview tab
logeekal Jun 20, 2025
25c0a16
Merge main --> current branch
logeekal Jun 20, 2025
8293ab0
fix: tests
logeekal Jun 20, 2025
a5221dc
fix: readable url generations
logeekal Jun 20, 2025
bd76727
fix: remove dead code
logeekal Jun 20, 2025
adaedf7
fix: comment
logeekal Jun 20, 2025
34e73da
fix: import
logeekal Jun 20, 2025
106298d
fix: types
logeekal Jun 20, 2025
376295c
fix: test
logeekal Jun 22, 2025
5dd1c79
fix: cypress
logeekal Jun 22, 2025
20ba5b6
Merge branch 'main' into feat/timeline_redirect
logeekal Jun 22, 2025
2d36491
Merge branch 'main' into feat/timeline_redirect
logeekal Jun 23, 2025
31a7702
fix: translations
logeekal Jun 23, 2025
582aaca
fix: default app state
logeekal Jun 23, 2025
2877773
fix: conditional profile changes
logeekal Jun 23, 2025
115599c
Merge branch 'main' into feat/timeline_redirect
logeekal Jun 23, 2025
fade055
fix: PR Feedback
logeekal Jun 23, 2025
1ad7ae1
fix: translations
logeekal Jun 23, 2025
619e284
remove extra enum key
logeekal Jun 23, 2025
8b8ba69
Fix failing tests after enabling security profile
davismcphee Jun 23, 2025
e5507d4
Update stateful tests
davismcphee Jun 23, 2025
e2874dc
Merge pull request #5 from davismcphee/fix-security-profile-tests
logeekal Jun 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -158,5 +159,6 @@ const createDataSourceProfileProviders = (providerServices: ProfileProviderServi
*/
const createDocumentProfileProviders = (providerServices: ProfileProviderServices) => [
createExampleDocumentProfileProvider(),
createSecurityDocumentProfileProvider(providerServices),
...createObservabilityDocumentProfileProviders(providerServices),
];
Original file line number Diff line number Diff line change
@@ -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' },
],
});
});
});
Original file line number Diff line number Diff line change
@@ -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;
}
Comment on lines +15 to +19
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be best modelled as a dedicated data source profile that resolves based on solution + index pattern, rather than repeating the logic in each extension point, e.g.:

export const createSecurityAlertsDataSourceProfileProvider = (): SecurityAlertsDataSourceProfileProvider => ({
  profileId: SECURITY_ALERTS_DATA_SOURCE_PROFILE_ID,
  profile: {
    getDefaultAppState: createSecurityAlertsAppStateGetter(),
    ...
  },
  resolve: (params) => {
    if (params.rootContext.solutionType !== SolutionType.Security) {
      return { isMatch: false };
    }

    const indexPattern = extractIndexPatternFrom(params);

    if (!indexPattern.includes(ALERTS_INDEX_PATTERN)) {
      return { isMatch: false };
    }

    return {
      isMatch: true,
      context: {
        category: DataSourceCategory.Alerts,
      },
    };
  },
});

Copy link
Copy Markdown
Contributor Author

@logeekal logeekal Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks I was trying to see how can i get index pattern in context. This is great. Let me see i can make a change now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I tried to implement this Datasource Profile Provider. In Security Solution View in ECH, params.rootContext.solutionType is not SolutionType.Security but SolutionType.default as can be seen in screenshot below. Because of this profile is not get activated.

For now, i will keep it as it is and raise a follow up PR for the same.

image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries about addressing for now, it makes sense to look into as a followup. That's very odd though 🤔 To clarify, you'd still need a SecurityRootProfile to set rootContext.solutionType to SolutionType.Security, but the data source specific pieces could be extracted into a separate SecurityAlertsDataSourceProfileProvider or similar that resolves based on rootContext.solutionType + the index pattern, instead of including that logic in each root profile extension point implementation.

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',
},
],
};
};
Original file line number Diff line number Diff line change
@@ -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',
});
});
});
Original file line number Diff line number Diff line change
@@ -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']> = (
Comment thread
PhilippeOberti marked this conversation as resolved.
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,
};
};
Original file line number Diff line number Diff line change
@@ -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}`
);
});
});
});
Loading