Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cloud Security] Show graph visualization in expanded flyout #198240

Merged
merged 47 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
a982ad7
Added node button to support node click to open a popover.
kfirpeled Oct 29, 2024
c94c04a
Fixed SvgDefsMarker so it won't capture clicks
kfirpeled Oct 29, 2024
5c00b6e
Fixed missing padding when lazy loading the graph
kfirpeled Oct 29, 2024
1589ef8
Added graph visualization tab with expand capability
kfirpeled Oct 30, 2024
6f71b19
checks feature flag before showing graph tab
kfirpeled Oct 30, 2024
3dc4523
Added filterbar support
kfirpeled Oct 30, 2024
b88b074
fixed prevent scrolling when not interactive
kfirpeled Oct 31, 2024
dbb2688
Added filter support
kfirpeled Nov 4, 2024
008521f
lowered the minimum zoom
kfirpeled Nov 4, 2024
70ee31d
refactoring
kfirpeled Nov 4, 2024
dbab169
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Nov 4, 2024
78246a7
Added open in timeline + time picking is around @timestamp
kfirpeled Nov 4, 2024
5318512
Color fixes and auto fitview
kfirpeled Nov 5, 2024
74ecb0e
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Nov 5, 2024
b57dfac
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Nov 11, 2024
2d0d360
Removed trace message that might contain sensitive info
kfirpeled Nov 11, 2024
f94db1e
Removed duplicated code due to merge
kfirpeled Nov 11, 2024
0899098
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Nov 13, 2024
702ef00
merge cleanup
kfirpeled Nov 13, 2024
1ad2ce4
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Nov 13, 2024
30f73c8
refactoring, removed investigate in timeline
kfirpeled Nov 18, 2024
3821ad9
Updated shared hooks
kfirpeled Nov 18, 2024
33134f6
Switched feature flag to an existing advanced setting
kfirpeled Nov 19, 2024
c98ab19
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Nov 19, 2024
708300d
Added FTR to test e2e expanded flyout filtering
kfirpeled Nov 20, 2024
34656ae
Added technical preview tag
kfirpeled Nov 20, 2024
b0f73b9
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Nov 21, 2024
c7fb622
post merge fixes
kfirpeled Nov 21, 2024
5c50026
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Dec 2, 2024
6969f69
disables flyout expansion in rule preview and in panel preview
kfirpeled Dec 2, 2024
21d04e2
refactoring
kfirpeled Dec 2, 2024
fef5ea3
refactoring
kfirpeled Dec 9, 2024
6b81c5d
fix tests
kfirpeled Dec 9, 2024
e8c6367
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Dec 9, 2024
4b1512e
Refactored graph visualization component
kfirpeled Dec 9, 2024
b13bc2a
[CI] Auto-commit changed files from 'node scripts/notice'
kibanamachine Dec 9, 2024
2f0ea4a
tests fixes
kfirpeled Dec 9, 2024
20be49b
fix i18n
kfirpeled Dec 9, 2024
0b52af2
quick check fix
kfirpeled Dec 9, 2024
bcbe7f6
linting fixes
kfirpeled Dec 9, 2024
dc5bb1c
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Dec 10, 2024
1f0bcec
added back graph vis in flyout experimental feature
kfirpeled Dec 10, 2024
6ac1a58
minor fix - just for the code to make more sense
kfirpeled Dec 10, 2024
5e90b2c
typecheck fix
kfirpeled Dec 10, 2024
6085cbd
Merge branch 'main' into cspm/cdr-graph-viz-expanded
kfirpeled Dec 12, 2024
6a55f50
refactoring
kfirpeled Dec 12, 2024
40e66c4
refactoring
kfirpeled Dec 12, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
*/

export * from './src/components';
export { useFetchGraphData } from './src/hooks';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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;
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export const Graph: React.FC<GraphProps> = ({
minZoom={0.1}
>
{interactive && <Controls onInteractiveChange={onInteractiveStateChange} />}
<Background id={backgroundId} />{' '}
<Background id={backgroundId} />
</ReactFlow>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
/*
* 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, useEffect, 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,
isPhraseFilter,
} from '@kbn/es-query';
import type { Filter, Query, TimeRange, BoolQuery, PhraseFilter } from '@kbn/es-query';
import { css } from '@emotion/react';
import { getEsQueryConfig } from '@kbn/data-service';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { Graph, type NodeViewModel } 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';

const CONTROLLED_BY_GRAPH_INVESTIGATION_FILTER = 'graph-investigation';

const useTimeRange = (timestamp: string) => {
const [timeRange, setTimeRange] = useState<TimeRange>({
from: `${timestamp}||-30m`,
to: `${timestamp}||+30m`,
});

const setPartialTimeRange = (newTimeRange: Partial<typeof timeRange>) => {
kfirpeled marked this conversation as resolved.
Show resolved Hide resolved
setTimeRange((currTimeRange) => ({ ...currTimeRange, ...newTimeRange }));
};

return { timeRange, setTimeRange, setPartialTimeRange };
};

const useGraphData = (eventIds: string[], timeRange: TimeRange, filter: { bool: BoolQuery }) => {
const { data, refresh, isFetching } = useFetchGraphData({
req: {
query: {
eventIds,
esQuery: filter,
start: timeRange.from,
end: timeRange.to,
},
},
options: {
refetchOnWindowFocus: false,
Copy link
Contributor

Choose a reason for hiding this comment

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

as far as I can see everywhere the useFetchGraphData hook is called, the refetchOnWindowFocus is set to false? Should it be the default value then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I kept it false to be consistent with react-query defaults

wdyt?

keepPreviousData: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the logic behind different values for keepPreviousData? From the UX perspective I didn't notice any difference when changed to false here with the mocked data? If there is some edge case we want to cover by this option, it might worth a comment

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the logic is that while you query you will see the previous results.

if it is set to false, on every query your graph will be cleaned, and when the response is back it will be drawn. Which creates a flicker.

Besides that, on error you will lose your current graph data. instead of seeing your previous graph data.

I think it provides a better UX

},
});

return { data, refresh, isFetching };
};

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

const addFilter = (dataViewId: string, prev: Filter[], key: string, value: string) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

what this logic of combined vs phrase filter is about? not really sure, maybe also worth a comment explaining the edge cases and/or add unit tests wich would explain different scenarious

Copy link
Contributor Author

@kfirpeled kfirpeled Dec 12, 2024

Choose a reason for hiding this comment

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

so we keep always a single filter with OR statements
So whenever addFilter is called, it checks first if it exists or not, and then adds OR statement when needed

I'll add comments now and UT later, once I'll have graph_investigation in storybook (it requires some extra mocking and this PR is already quite big).

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 (isPhraseFilter(firstFilter)) {
return [
buildCombinedFilter(BooleanRelation.OR, [firstFilter, buildPhraseFilter(key, value)], {
id: dataViewId,
}),
...otherFilters,
];
} else {
return [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));
kfirpeled marked this conversation as resolved.
Show resolved Hide resolved
},
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 popovers = [nodeExpandPopover];
const popoverOpenWrapper = (cb: Function, ...args: unknown[]) => {
kfirpeled marked this conversation as resolved.
Show resolved Hide resolved
popovers.forEach(({ actions: { closePopover } }) => {
closePopover();
});
cb(...args);
};

return { nodeExpandPopover, popoverOpenWrapper };
};

const useGraphNodes = (
nodes: NodeViewModel[],
expandButtonClickHandler: (...args: unknown[]) => void
) => {
return useMemo(() => {
return 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
Copy link
Contributor

Choose a reason for hiding this comment

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

any reason not to add expandButtonClickHandler to the deps map?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yea, it stops behaving as expected :P
(seriously, when I remove the filters it renders an empty graph although the graph api response is not empty)

I honestly don't remember why atm. But we can try to invest more time to understand why it causes issues

}, [nodes]);
};

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 }: GraphInvestigationProps) => {
const [searchFilters, setSearchFilters] = useState<Filter[]>(() => []);
const { timeRange, setTimeRange } = useTimeRange(timestamp ?? new Date().toISOString());

const {
services: { uiSettings },
} = useKibana();
const [query, setQuery] = useState<{ bool: BoolQuery }>(
buildEsQuery(
dataView,
[],
[...searchFilters],
getEsQueryConfig(uiSettings as Parameters<typeof getEsQueryConfig>[0])
)
);

useEffect(() => {
kfirpeled marked this conversation as resolved.
Show resolved Hide resolved
setQuery(
buildEsQuery(
dataView,
[],
[...searchFilters],
getEsQueryConfig(uiSettings as Parameters<typeof getEsQueryConfig>[0])
)
);
}, [searchFilters, dataView, uiSettings]);

const { nodeExpandPopover, popoverOpenWrapper } = useGraphPopovers(
dataView?.id ?? '',
setSearchFilters
);
const expandButtonClickHandler = (...args: unknown[]) =>
popoverOpenWrapper(nodeExpandPopover.onNodeExpandButtonClick, ...args);
const isPopoverOpen = [nodeExpandPopover].some(({ state: { isOpen } }) => isOpen);
const { data, refresh, isFetching } = useGraphData(eventIds, timeRange, query);
const nodes = useGraphNodes(data?.nodes ?? [], expandButtonClickHandler);

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';
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_EXPLORE_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;
onExploreRelatedEntitiesClick: () => void;
kfirpeled marked this conversation as resolved.
Show resolved Hide resolved
onShowActionsByEntityClick: () => void;
onShowActionsOnEntityClick: () => void;
}

export const GraphNodeExpandPopover: React.FC<GraphNodeExpandPopoverProps> = memo(
({
isOpen,
anchorElement,
closePopover,
onExploreRelatedEntitiesClick,
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={onExploreRelatedEntitiesClick}
data-test-subj={GRAPH_NODE_POPOVER_EXPLORE_RELATED_ITEM_ID}
/>
</EuiListGroup>
</GraphPopover>
);
}
);

GraphNodeExpandPopover.displayName = 'GraphNodeExpandPopover';
Loading