Skip to content

Commit

Permalink
[Cloud Security] Adds KQL filter to graph component (elastic#205570)
Browse files Browse the repository at this point in the history
## Summary

This PR includes the following changes to the graph investigation
component:
- Added KQL filter to graph investigation component
- Shows toast message on syntax error
- Includes the KQL filter in timeline investigation through the graph


https://github.com/user-attachments/assets/4653bdfd-277a-4479-abc2-65a90fd50f57

**How to test:**

To test this PR using storybook (alternatively access to storybooks
attached to this build)

```
yarn storybook cloud_security_posture_packages
```

To test e2e:

- Enable the feature flag 

`kibana.dev.yml`:

```yaml
uiSettings.overrides.securitySolution:enableVisualizationsInFlyout: true
xpack.securitySolution.enableExperimental: ['graphVisualizationInFlyoutEnabled']
```

- Load mocked data:

```bash
node scripts/es_archiver load x-pack/test/cloud_security_posture_functional/es_archives/logs_gcp_audit \ 
  --es-url http://elastic:changeme@localhost:9200 \
  --kibana-url http://elastic:changeme@localhost:5601

node scripts/es_archiver load x-pack/test/cloud_security_posture_functional/es_archives/security_alerts \
  --es-url http://elastic:changeme@localhost:9200 \
  --kibana-url http://elastic:changeme@localhost:5601
```

- Make sure you include data from Oct 13 2024. (in the video I use Last
year)

To run FTR tests:

```
yarn test:ftr:server --config x-pack/test/cloud_security_posture_functional/config.ts
yarn test:ftr:runner --config x-pack/test/cloud_security_posture_functional/config.ts --grep="Graph visualization"
```

<details>
<summary>E2E tests 📹 </summary>



https://github.com/user-attachments/assets/5f00d911-fc8f-4329-bb1d-0468a260fef2



</details>

### Checklist

- [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
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
  • Loading branch information
kfirpeled authored Jan 14, 2025
1 parent 317a897 commit fd41ba8
Show file tree
Hide file tree
Showing 16 changed files with 436 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,21 @@ export const TITLE = 'Cloud Security Posture Storybook';
/** The remote URL of the root from which Storybook loads stories for Cloud Security Solution. */
export const URL =
'https://github.com/elastic/kibana/tree/main/x-pack/packages/kbn-cloud-security-posture';

export const WEB_STORAGE_CLEAR_ACTION = 'web_storage:clear' as const;
export const WEB_STORAGE_GET_ITEM_ACTION = 'web_storage:getItem' as const;
export const WEB_STORAGE_KEY_ACTION = 'web_storage:key' as const;
export const WEB_STORAGE_REMOVE_ITEM_ACTION = 'web_storage:removeItem' as const;
export const WEB_STORAGE_SET_ITEM_ACTION = 'web_storage:setItem' as const;
export const STORAGE_SET_ACTION = 'storage:set' as const;
export const STORAGE_REMOVE_ACTION = 'storage:remove' as const;
export const STORAGE_CLEAR_ACTION = 'storage:clear' as const;
export const NOTIFICATIONS_SHOW_ACTION = 'notifications:show' as const;
export const NOTIFICATIONS_SUCCESS_ACTION = 'notifications:success' as const;
export const NOTIFICATIONS_WARNING_ACTION = 'notifications:warning' as const;
export const NOTIFICATIONS_DANGER_ACTION = 'notifications:danger' as const;
export const NOTIFICATIONS_ADD_ERROR_ACTION = 'notifications:addError' as const;
export const NOTIFICATIONS_ADD_SUCCESS_ACTION = 'notifications:addSuccess' as const;
export const NOTIFICATIONS_ADD_WARNING_ACTION = 'notifications:addWarning' as const;
export const NOTIFICATIONS_REMOVE_ACTION = 'notifications:remove' as const;
export const EDIT_DATA_VIEW_ACTION = 'editDataView' as const;
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,40 @@ import { action } from '@storybook/addon-actions';
import { createKibanaReactContext, type KibanaServices } from '@kbn/kibana-react-plugin/public';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { of } from 'rxjs';
import {
WEB_STORAGE_CLEAR_ACTION,
WEB_STORAGE_GET_ITEM_ACTION,
WEB_STORAGE_KEY_ACTION,
WEB_STORAGE_REMOVE_ITEM_ACTION,
WEB_STORAGE_SET_ITEM_ACTION,
STORAGE_SET_ACTION,
STORAGE_REMOVE_ACTION,
STORAGE_CLEAR_ACTION,
NOTIFICATIONS_SHOW_ACTION,
NOTIFICATIONS_SUCCESS_ACTION,
NOTIFICATIONS_WARNING_ACTION,
NOTIFICATIONS_DANGER_ACTION,
NOTIFICATIONS_ADD_ERROR_ACTION,
NOTIFICATIONS_ADD_SUCCESS_ACTION,
NOTIFICATIONS_ADD_WARNING_ACTION,
NOTIFICATIONS_REMOVE_ACTION,
EDIT_DATA_VIEW_ACTION,
} from '../constants';

const createMockWebStorage = () => ({
clear: action('clear'),
getItem: action('getItem'),
key: action('key'),
removeItem: action('removeItem'),
setItem: action('setItem'),
clear: action(WEB_STORAGE_CLEAR_ACTION),
getItem: action(WEB_STORAGE_GET_ITEM_ACTION),
key: action(WEB_STORAGE_KEY_ACTION),
removeItem: action(WEB_STORAGE_REMOVE_ITEM_ACTION),
setItem: action(WEB_STORAGE_SET_ITEM_ACTION),
length: 0,
});

const createMockStorage = () => ({
storage: createMockWebStorage(),
set: action('set'),
remove: action('remove'),
clear: action('clear'),
set: action(STORAGE_SET_ACTION),
remove: action(STORAGE_REMOVE_ACTION),
clear: action(STORAGE_CLEAR_ACTION),
get: () => true,
});

Expand Down Expand Up @@ -101,23 +120,24 @@ const services: Partial<KibanaServices> = {
settings: { client: { get: () => {} } },
notifications: {
toasts: {
show: action('notifications:show'),
success: action('notifications:success'),
warning: action('notifications:warning'),
danger: action('notifications:danger'),
show: action(NOTIFICATIONS_SHOW_ACTION),
success: action(NOTIFICATIONS_SUCCESS_ACTION),
warning: action(NOTIFICATIONS_WARNING_ACTION),
danger: action(NOTIFICATIONS_DANGER_ACTION),
// @ts-ignore
addError: action('notifications:addError'),
addError: action(NOTIFICATIONS_ADD_ERROR_ACTION),
// @ts-ignore
addSuccess: action('notifications:addSuccess'),
addSuccess: action(NOTIFICATIONS_ADD_SUCCESS_ACTION),
// @ts-ignore
addWarning: action('notifications:addWarning'),
remove: action('notifications:remove'),
addWarning: action(NOTIFICATIONS_ADD_WARNING_ACTION),
remove: action(NOTIFICATIONS_REMOVE_ACTION),
},
},
storage: createMockStorage(),
data: {
query: {
savedQueries: {
getSavedQueryCount: () => 0,
findSavedQueries: () =>
Promise.resolve({
queries: [],
Expand All @@ -134,7 +154,7 @@ const services: Partial<KibanaServices> = {
},
dataViewEditor: {
userPermissions: {
editDataView: action('editDataView'),
editDataView: action(EDIT_DATA_VIEW_ACTION),
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export const RELATED_ENTITY = 'related.entity' as const;
export const ACTOR_ENTITY_ID = 'actor.entity.id' as const;
export const TARGET_ENTITY_ID = 'target.entity.id' as const;
export const EVENT_ACTION = 'event.action' as const;
export const EVENT_ID = 'event.id' as const;
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,39 @@

import React from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { setProjectAnnotations } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { composeStories } from '@storybook/testing-react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import * as stories from './graph_investigation.stories';
import { type GraphInvestigationProps } from './graph_investigation';
import { GRAPH_INVESTIGATION_TEST_ID, GRAPH_ACTIONS_INVESTIGATE_IN_TIMELINE_ID } from '../test_ids';
import * as previewAnnotations from '../../../.storybook/preview';
import { NOTIFICATIONS_ADD_ERROR_ACTION } from '../../../.storybook/constants';
import { USE_FETCH_GRAPH_DATA_REFRESH_ACTION } from '../mock/constants';

setProjectAnnotations(previewAnnotations);

const { Investigation } = composeStories(stories);

// Mock the useFetchGraphData hook, which is used by the GraphInvestigation component
// Callbacks replaced with storybook actions, therefore we mock storybook's action function as well for testing
jest.mock('../../hooks/use_fetch_graph_data', () => {
return require('../mock/use_fetch_graph_data.mock');
});

const actionMocks: Record<string, jest.Mock> = {};

jest.mock('@storybook/addon-actions', () => ({
action: jest.fn((name) => {
if (!actionMocks[name]) {
actionMocks[name] = jest.fn(); // Create a new mock if not already present
}
return actionMocks[name]; // Return the mock for the given action name
}),
}));

const renderStory = (args: Partial<GraphInvestigationProps> = {}) => {
return render(
<IntlProvider locale="en">
Expand All @@ -36,7 +53,18 @@ jest.mock('../graph/constants', () => ({
ONLY_RENDER_VISIBLE_ELEMENTS: false,
}));

const QUERY_PARAM_IDX = 0;
const FILTERS_PARAM_IDX = 1;

describe('GraphInvestigation Component', () => {
beforeEach(() => {
for (const key in actionMocks) {
if (Object.prototype.hasOwnProperty.call(actionMocks, key)) {
actionMocks[key].mockClear();
}
}
});

it('renders without crashing', () => {
const { getByTestId } = renderStory();

Expand All @@ -51,6 +79,31 @@ describe('GraphInvestigation Component', () => {
expect(getAllByText('~ an hour ago')).toHaveLength(2);
});

it('shows error on bad kql syntax', async () => {
const mockDangerToast = action(NOTIFICATIONS_ADD_ERROR_ACTION);
const { getByTestId } = renderStory();

// Act
const queryInput = getByTestId('queryInput');
await userEvent.type(queryInput, '< > sdg $@#T');
const querySubmitBtn = getByTestId('querySubmitButton');
querySubmitBtn.click();

// Assert
expect(mockDangerToast).toHaveBeenCalledTimes(1);
});

it('calls refresh on submit button click', () => {
const mockRefresh = action(USE_FETCH_GRAPH_DATA_REFRESH_ACTION);
const { getByTestId } = renderStory();

// Act
getByTestId('querySubmitButton').click();

// Assert
expect(mockRefresh).toHaveBeenCalledTimes(1);
});

it('calls onInvestigateInTimeline action', () => {
const onInvestigateInTimeline = jest.fn();
const { getByTestId } = renderStory({
Expand All @@ -61,5 +114,96 @@ describe('GraphInvestigation Component', () => {
getByTestId(GRAPH_ACTIONS_INVESTIGATE_IN_TIMELINE_ID).click();

expect(onInvestigateInTimeline).toHaveBeenCalled();
expect(onInvestigateInTimeline.mock.calls[0][QUERY_PARAM_IDX]).toEqual({
query: '',
language: 'kuery',
});
expect(onInvestigateInTimeline.mock.calls[0][FILTERS_PARAM_IDX]).toEqual([
{
$state: {
store: 'appState',
},
meta: {
disabled: false,
index: '1235',
negate: false,
params: ['1', '2'].map((eventId) => ({
meta: {
controlledBy: 'graph-investigation',
field: 'event.id',
index: '1235',
key: 'event.id',
negate: false,
params: {
query: eventId,
},
type: 'phrase',
},
query: {
match_phrase: {
'event.id': eventId,
},
},
})),
type: 'combined',
relation: 'OR',
},
},
]);
});

it('query includes origin event ids onInvestigateInTimeline callback', async () => {
// Arrange
const onInvestigateInTimeline = jest.fn();
const { getByTestId } = renderStory({
onInvestigateInTimeline,
showInvestigateInTimeline: true,
});
const queryInput = getByTestId('queryInput');
await userEvent.type(queryInput, 'host1');
const querySubmitBtn = getByTestId('querySubmitButton');
querySubmitBtn.click();

// Act
getByTestId(GRAPH_ACTIONS_INVESTIGATE_IN_TIMELINE_ID).click();

// Assert
expect(onInvestigateInTimeline).toHaveBeenCalled();
expect(onInvestigateInTimeline.mock.calls[0][QUERY_PARAM_IDX]).toEqual({
query: '(host1) OR event.id: "1" OR event.id: "2"',
language: 'kuery',
});
expect(onInvestigateInTimeline.mock.calls[0][FILTERS_PARAM_IDX]).toEqual([
{
$state: {
store: 'appState',
},
meta: {
disabled: false,
index: '1235',
negate: false,
params: ['1', '2'].map((eventId) => ({
meta: {
controlledBy: 'graph-investigation',
field: 'event.id',
index: '1235',
key: 'event.id',
negate: false,
params: {
query: eventId,
},
type: 'phrase',
},
query: {
match_phrase: {
'event.id': eventId,
},
},
})),
type: 'combined',
relation: 'OR',
},
},
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ const defaultProps: GraphInvestigationProps = {
id: '1',
isAlert: false,
},
{
id: '2',
isAlert: true,
},
],
timeRange: {
from: `${hourAgo.toISOString()}||-15m`,
Expand Down
Loading

0 comments on commit fd41ba8

Please sign in to comment.