forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Cloud Security] Show graph visualization in expanded flyout (elastic…
…#198240) ## Summary Added graph tab to the flyout visualization of alerts and events. **A couple of included changes:** - Added technical preview badge - ~Feature is now toggled using `securitySolution:enableVisualizationsInFlyout` advanced setting~ reverted back to use the experimental feature flag - Added node popover to expand the graph - Expanding a graph adds relevant filters - Added e2e tests for both alerts flyout and events flyout (through network page) **List of known issues:** - The graph API works queries `logs-*` while the filters bar works with sourcerer current dataview Id - I'm not sure how to write a UT for GraphVisualization / Popover which uses ReactPortal that makes it tricky to test (I covered most scenarios using E2E test) - Expanding graph more than once adds another filter **How to test this PR:** - 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 90 days) https://github.com/user-attachments/assets/12e19ac7-0f61-4c0a-ac11-e304dfcc83d4 ### 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) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [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 - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: kibanamachine <[email protected]>
- Loading branch information
1 parent
b1363d9
commit 749eeec
Showing
48 changed files
with
1,849 additions
and
155 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,3 +6,4 @@ | |
*/ | ||
|
||
export * from './src/components'; | ||
export { useFetchGraphData } from './src/hooks'; |
12 changes: 12 additions & 0 deletions
12
x-pack/packages/kbn-cloud-security-posture/graph/src/common/constants.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/* | ||
* 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. | ||
*/ | ||
|
||
export const EVENT_GRAPH_VISUALIZATION_API = '/internal/cloud_security_posture/graph' as const; | ||
|
||
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
258 changes: 258 additions & 0 deletions
258
...n-cloud-security-posture/graph/src/components/graph_investigation/graph_investigation.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,258 @@ | ||
/* | ||
* 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, { memo, useCallback, useMemo, useState } from 'react'; | ||
import { SearchBar } from '@kbn/unified-search-plugin/public'; | ||
import { useKibana } from '@kbn/kibana-react-plugin/public'; | ||
import type { DataView } from '@kbn/data-views-plugin/public'; | ||
import { | ||
BooleanRelation, | ||
buildEsQuery, | ||
isCombinedFilter, | ||
buildCombinedFilter, | ||
isFilter, | ||
FilterStateStore, | ||
} from '@kbn/es-query'; | ||
import type { Filter, Query, TimeRange, PhraseFilter } from '@kbn/es-query'; | ||
import { css } from '@emotion/react'; | ||
import { getEsQueryConfig } from '@kbn/data-service'; | ||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; | ||
import { Graph } from '../../..'; | ||
import { useGraphNodeExpandPopover } from './use_graph_node_expand_popover'; | ||
import { useFetchGraphData } from '../../hooks/use_fetch_graph_data'; | ||
import { GRAPH_INVESTIGATION_TEST_ID } from '../test_ids'; | ||
import { ACTOR_ENTITY_ID, RELATED_ENTITY, TARGET_ENTITY_ID } from '../../common/constants'; | ||
|
||
const CONTROLLED_BY_GRAPH_INVESTIGATION_FILTER = 'graph-investigation'; | ||
|
||
const buildPhraseFilter = (field: string, value: string, dataViewId?: string): PhraseFilter => ({ | ||
meta: { | ||
key: field, | ||
index: dataViewId, | ||
negate: false, | ||
disabled: false, | ||
type: 'phrase', | ||
field, | ||
controlledBy: CONTROLLED_BY_GRAPH_INVESTIGATION_FILTER, | ||
params: { | ||
query: value, | ||
}, | ||
}, | ||
query: { | ||
match_phrase: { | ||
[field]: value, | ||
}, | ||
}, | ||
}); | ||
|
||
/** | ||
* Adds a filter to the existing list of filters based on the provided key and value. | ||
* It will always use the first filter in the list to build a combined filter with the new filter. | ||
* | ||
* @param dataViewId - The ID of the data view to which the filter belongs. | ||
* @param prev - The previous list of filters. | ||
* @param key - The key for the filter. | ||
* @param value - The value for the filter. | ||
* @returns A new list of filters with the added filter. | ||
*/ | ||
const addFilter = (dataViewId: string, prev: Filter[], key: string, value: string) => { | ||
const [firstFilter, ...otherFilters] = prev; | ||
|
||
if (isCombinedFilter(firstFilter) && firstFilter?.meta?.relation === BooleanRelation.OR) { | ||
return [ | ||
{ | ||
...firstFilter, | ||
meta: { | ||
...firstFilter.meta, | ||
params: [ | ||
...(Array.isArray(firstFilter.meta.params) ? firstFilter.meta.params : []), | ||
buildPhraseFilter(key, value), | ||
], | ||
}, | ||
}, | ||
...otherFilters, | ||
]; | ||
} else if (isFilter(firstFilter) && firstFilter.meta?.type !== 'custom') { | ||
return [ | ||
buildCombinedFilter(BooleanRelation.OR, [firstFilter, buildPhraseFilter(key, value)], { | ||
id: dataViewId, | ||
}), | ||
...otherFilters, | ||
]; | ||
} else { | ||
return [ | ||
{ | ||
$state: { | ||
store: FilterStateStore.APP_STATE, | ||
}, | ||
...buildPhraseFilter(key, value, dataViewId), | ||
}, | ||
...prev, | ||
]; | ||
} | ||
}; | ||
|
||
const useGraphPopovers = ( | ||
dataViewId: string, | ||
setSearchFilters: React.Dispatch<React.SetStateAction<Filter[]>> | ||
) => { | ||
const nodeExpandPopover = useGraphNodeExpandPopover({ | ||
onExploreRelatedEntitiesClick: (node) => { | ||
setSearchFilters((prev) => addFilter(dataViewId, prev, RELATED_ENTITY, node.id)); | ||
}, | ||
onShowActionsByEntityClick: (node) => { | ||
setSearchFilters((prev) => addFilter(dataViewId, prev, ACTOR_ENTITY_ID, node.id)); | ||
}, | ||
onShowActionsOnEntityClick: (node) => { | ||
setSearchFilters((prev) => addFilter(dataViewId, prev, TARGET_ENTITY_ID, node.id)); | ||
}, | ||
}); | ||
|
||
const openPopoverCallback = useCallback( | ||
(cb: Function, ...args: unknown[]) => { | ||
[nodeExpandPopover].forEach(({ actions: { closePopover } }) => { | ||
closePopover(); | ||
}); | ||
cb(...args); | ||
}, | ||
[nodeExpandPopover] | ||
); | ||
|
||
return { nodeExpandPopover, openPopoverCallback }; | ||
}; | ||
|
||
interface GraphInvestigationProps { | ||
dataView: DataView; | ||
eventIds: string[]; | ||
timestamp: string | null; | ||
} | ||
|
||
/** | ||
* Graph investigation view allows the user to expand nodes and view related entities. | ||
*/ | ||
export const GraphInvestigation: React.FC<GraphInvestigationProps> = memo( | ||
({ dataView, eventIds, timestamp = new Date().toISOString() }: GraphInvestigationProps) => { | ||
const [searchFilters, setSearchFilters] = useState<Filter[]>(() => []); | ||
const [timeRange, setTimeRange] = useState<TimeRange>({ | ||
from: `${timestamp}||-30m`, | ||
to: `${timestamp}||+30m`, | ||
}); | ||
|
||
const { | ||
services: { uiSettings }, | ||
} = useKibana(); | ||
const query = useMemo( | ||
() => | ||
buildEsQuery( | ||
dataView, | ||
[], | ||
[...searchFilters], | ||
getEsQueryConfig(uiSettings as Parameters<typeof getEsQueryConfig>[0]) | ||
), | ||
[searchFilters, dataView, uiSettings] | ||
); | ||
|
||
const { nodeExpandPopover, openPopoverCallback } = useGraphPopovers( | ||
dataView?.id ?? '', | ||
setSearchFilters | ||
); | ||
const expandButtonClickHandler = (...args: unknown[]) => | ||
openPopoverCallback(nodeExpandPopover.onNodeExpandButtonClick, ...args); | ||
const isPopoverOpen = [nodeExpandPopover].some(({ state: { isOpen } }) => isOpen); | ||
const { data, refresh, isFetching } = useFetchGraphData({ | ||
req: { | ||
query: { | ||
eventIds, | ||
esQuery: query, | ||
start: timeRange.from, | ||
end: timeRange.to, | ||
}, | ||
}, | ||
options: { | ||
refetchOnWindowFocus: false, | ||
keepPreviousData: true, | ||
}, | ||
}); | ||
|
||
const nodes = useMemo(() => { | ||
return ( | ||
data?.nodes.map((node) => { | ||
const nodeHandlers = | ||
node.shape !== 'label' && node.shape !== 'group' | ||
? { | ||
expandButtonClick: expandButtonClickHandler, | ||
} | ||
: undefined; | ||
return { ...node, ...nodeHandlers }; | ||
}) ?? [] | ||
); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [data?.nodes]); | ||
|
||
return ( | ||
<> | ||
<EuiFlexGroup | ||
data-test-subj={GRAPH_INVESTIGATION_TEST_ID} | ||
direction="column" | ||
gutterSize="none" | ||
css={css` | ||
height: 100%; | ||
`} | ||
> | ||
{dataView && ( | ||
<EuiFlexItem grow={false}> | ||
<SearchBar<Query> | ||
{...{ | ||
appName: 'graph-investigation', | ||
intl: null, | ||
showFilterBar: true, | ||
showDatePicker: true, | ||
showAutoRefreshOnly: false, | ||
showSaveQuery: false, | ||
showQueryInput: false, | ||
isLoading: isFetching, | ||
isAutoRefreshDisabled: true, | ||
dateRangeFrom: timeRange.from, | ||
dateRangeTo: timeRange.to, | ||
query: { query: '', language: 'kuery' }, | ||
indexPatterns: [dataView], | ||
filters: searchFilters, | ||
submitButtonStyle: 'iconOnly', | ||
onFiltersUpdated: (newFilters) => { | ||
setSearchFilters(newFilters); | ||
}, | ||
onQuerySubmit: (payload, isUpdate) => { | ||
if (isUpdate) { | ||
setTimeRange({ ...payload.dateRange }); | ||
} else { | ||
refresh(); | ||
} | ||
}, | ||
}} | ||
/> | ||
</EuiFlexItem> | ||
)} | ||
<EuiFlexItem> | ||
<Graph | ||
css={css` | ||
height: 100%; | ||
width: 100%; | ||
`} | ||
nodes={nodes} | ||
edges={data?.edges ?? []} | ||
interactive={true} | ||
isLocked={isPopoverOpen} | ||
/> | ||
</EuiFlexItem> | ||
</EuiFlexGroup> | ||
<nodeExpandPopover.PopoverComponent /> | ||
</> | ||
); | ||
} | ||
); | ||
|
||
GraphInvestigation.displayName = 'GraphInvestigation'; |
78 changes: 78 additions & 0 deletions
78
...d-security-posture/graph/src/components/graph_investigation/graph_node_expand_popover.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
/* | ||
* 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, { memo } from 'react'; | ||
import { EuiListGroup } from '@elastic/eui'; | ||
import { i18n } from '@kbn/i18n'; | ||
import { ExpandPopoverListItem } from '../styles'; | ||
import { GraphPopover } from '../../..'; | ||
import { | ||
GRAPH_NODE_EXPAND_POPOVER_TEST_ID, | ||
GRAPH_NODE_POPOVER_SHOW_RELATED_ITEM_ID, | ||
GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID, | ||
GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_ITEM_ID, | ||
} from '../test_ids'; | ||
|
||
interface GraphNodeExpandPopoverProps { | ||
isOpen: boolean; | ||
anchorElement: HTMLElement | null; | ||
closePopover: () => void; | ||
onShowRelatedEntitiesClick: () => void; | ||
onShowActionsByEntityClick: () => void; | ||
onShowActionsOnEntityClick: () => void; | ||
} | ||
|
||
export const GraphNodeExpandPopover: React.FC<GraphNodeExpandPopoverProps> = memo( | ||
({ | ||
isOpen, | ||
anchorElement, | ||
closePopover, | ||
onShowRelatedEntitiesClick, | ||
onShowActionsByEntityClick, | ||
onShowActionsOnEntityClick, | ||
}) => { | ||
return ( | ||
<GraphPopover | ||
panelPaddingSize="s" | ||
anchorPosition="rightCenter" | ||
isOpen={isOpen} | ||
anchorElement={anchorElement} | ||
closePopover={closePopover} | ||
data-test-subj={GRAPH_NODE_EXPAND_POPOVER_TEST_ID} | ||
> | ||
<EuiListGroup gutterSize="none" bordered={false} flush={true}> | ||
<ExpandPopoverListItem | ||
iconType="users" | ||
label={i18n.translate('xpack.csp.graph.graphNodeExpandPopover.showActionsByEntity', { | ||
defaultMessage: 'Show actions by this entity', | ||
})} | ||
onClick={onShowActionsByEntityClick} | ||
data-test-subj={GRAPH_NODE_POPOVER_SHOW_ACTIONS_BY_ITEM_ID} | ||
/> | ||
<ExpandPopoverListItem | ||
iconType="storage" | ||
label={i18n.translate('xpack.csp.graph.graphNodeExpandPopover.showActionsOnEntity', { | ||
defaultMessage: 'Show actions on this entity', | ||
})} | ||
onClick={onShowActionsOnEntityClick} | ||
data-test-subj={GRAPH_NODE_POPOVER_SHOW_ACTIONS_ON_ITEM_ID} | ||
/> | ||
<ExpandPopoverListItem | ||
iconType="visTagCloud" | ||
label={i18n.translate('xpack.csp.graph.graphNodeExpandPopover.showRelatedEvents', { | ||
defaultMessage: 'Show related events', | ||
})} | ||
onClick={onShowRelatedEntitiesClick} | ||
data-test-subj={GRAPH_NODE_POPOVER_SHOW_RELATED_ITEM_ID} | ||
/> | ||
</EuiListGroup> | ||
</GraphPopover> | ||
); | ||
} | ||
); | ||
|
||
GraphNodeExpandPopover.displayName = 'GraphNodeExpandPopover'; |
Oops, something went wrong.