Skip to content

Commit

Permalink
[Security Solution] Handle invalid savedSearchId (#182937)
Browse files Browse the repository at this point in the history
## Summary

Handles #182823

This PR resolves the issue where user opens a timeline with a
`savedSearchId` which no longer exists.


## Desk Testing Guide

1. Create an `Untitled Timeline` and add `ESQL` query and save the
timeline.
2. Make sure `Saved Objects` in Stack Management contains a new saved
object. with name - `Saved search for timeline -
<name_of_timeline_above>`.
3. Export the above created timeline as `ndjson` as shown below.  
![Screenshot 2024-05-08 at 14 26
21](https://github.com/elastic/kibana/assets/7485038/cc134d53-7d07-40d9-8ee8-7e4e7a0c2cc9)
5. Delete the above created timeline
6. Make sure that corresponding saved objects is also deleted in `Saved
Objects` in Stack Management.
7. `Import` the timeline export in Step 3 on the Timelines Page. 
8. Once imported.. Navigate to ESQL tab and save a arbitrary query.
9. Save the timeline... Switch to another timeline and then back.
10. The query you saved should be restored.

---------

Co-authored-by: Jan Monschke <janmonschke@fastmail.com>
  • Loading branch information
logeekal and janmonschke authored May 9, 2024
1 parent ed14baf commit 02a22fd
Show file tree
Hide file tree
Showing 3 changed files with 31 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const useDiscoverInTimelineActions = (

const queryClient = useQueryClient();

const { mutateAsync: saveSavedSearch, status } = useMutation({
const { mutateAsync: saveSavedSearch, status: saveSavedSearchStatus } = useMutation({
mutationFn: ({
savedSearch,
savedSearchOptions,
Expand Down Expand Up @@ -189,7 +189,7 @@ export const useDiscoverInTimelineActions = (
*
* */
const updateSavedSearch = useCallback(
async (savedSearch: SavedSearch, timelineId: string) => {
async (savedSearch: SavedSearch, timelineId: string, onUpdate?: () => void) => {
savedSearch.timeRestore = true;
savedSearch.timeRange =
savedSearch.timeRange ?? discoverDataService.query.timefilter.timefilter.getTime();
Expand Down Expand Up @@ -219,7 +219,7 @@ export const useDiscoverInTimelineActions = (
// If no saved search exists. Create a new saved search instance and associate it with the timeline.
try {
// Make sure we're not creating a saved search while a previous creation call is in progress
if (status !== 'idle') {
if (saveSavedSearchStatus === 'loading') {
return;
}
dispatch(
Expand All @@ -244,6 +244,7 @@ export const useDiscoverInTimelineActions = (
);
// Also save the timeline, this will only happen once, in case there is no saved search id yet
dispatch(timelineActions.saveTimeline({ id: TimelineId.active, saveAsNew: false }));
onUpdate?.();
}
} catch (err) {
dispatch(
Expand All @@ -254,7 +255,7 @@ export const useDiscoverInTimelineActions = (
}
}
},
[persistSavedSearch, savedSearchId, dispatch, discoverDataService, status]
[persistSavedSearch, savedSearchId, dispatch, discoverDataService, saveSavedSearchStatus]
);

const initializeLocalSavedSearch = useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { isEqualWith } from 'lodash';
import type { SavedSearch } from '@kbn/saved-search-plugin/common';
import type { TimeRange } from '@kbn/es-query';
import { useDispatch } from 'react-redux';
import { updateSavedSearchId } from '../../../../store/actions';
import { useDiscoverInTimelineContext } from '../../../../../common/components/discover_in_timeline/use_discover_in_timeline_context';
import { useSourcererDataView } from '../../../../../common/containers/sourcerer';
import { useKibana } from '../../../../../common/lib/kibana';
Expand Down Expand Up @@ -89,7 +90,11 @@ export const DiscoverTabContent: FC<DiscoverTabContentProps> = ({ timelineId })
);
const { status, savedSearchId, activeTab, savedObjectId, title, description } = timeline;

const { data: savedSearchById, isFetching } = useQuery({
const {
data: savedSearchById,
isFetching,
status: savedSearchByIdStatus,
} = useQuery({
queryKey: ['savedSearchById', savedSearchId ?? ''],
queryFn: () => (savedSearchId ? savedSearchService.get(savedSearchId) : Promise.resolve(null)),
});
Expand Down Expand Up @@ -117,6 +122,12 @@ export const DiscoverTabContent: FC<DiscoverTabContentProps> = ({ timelineId })

useEffect(() => {
if (isFetching) return;
if (savedSearchByIdStatus === 'error' && savedSearchId) {
// when a timeline json is uploaded with a saved search Id that not longer
// exists, we need to reset the saved search Id in the timeline and remove th saved search
dispatch(updateSavedSearchId({ id: timelineId, savedSearchId: null }));
return;
}
if (!savedObjectId) return;
if (!status || status === 'draft') return;
const latestState = getCombinedDiscoverSavedSearchState();
Expand All @@ -126,8 +137,9 @@ export const DiscoverTabContent: FC<DiscoverTabContentProps> = ({ timelineId })
if (!index) return;
if (!latestState || combinedDiscoverSavedSearchStateRef.current === latestState) return;
if (isEqualWith(latestState, savedSearchById, savedSearchComparator)) return;
updateSavedSearch(latestState, timelineId);
combinedDiscoverSavedSearchStateRef.current = latestState;
updateSavedSearch(latestState, timelineId, function onUpdate() {
combinedDiscoverSavedSearchStateRef.current = latestState;
});
}, [
getCombinedDiscoverSavedSearchState,
savedSearchById,
Expand All @@ -139,6 +151,8 @@ export const DiscoverTabContent: FC<DiscoverTabContentProps> = ({ timelineId })
isFetching,
timelineId,
dispatch,
savedSearchId,
savedSearchByIdStatus,
]);

useEffect(() => {
Expand Down Expand Up @@ -166,9 +180,14 @@ export const DiscoverTabContent: FC<DiscoverTabContentProps> = ({ timelineId })
setDiscoverStateContainer(stateContainer);
let savedSearchAppState;
if (savedSearchId) {
const localSavedSearch = await savedSearchService.get(savedSearchId);
initializeLocalSavedSearch(localSavedSearch, timelineId);
savedSearchAppState = getAppStateFromSavedSearch(localSavedSearch);
try {
const localSavedSearch = await savedSearchService.get(savedSearchId);
initializeLocalSavedSearch(localSavedSearch, timelineId);
savedSearchAppState = getAppStateFromSavedSearch(localSavedSearch);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Stale Saved search Id which no longer exists', e);
}
}

const finalAppState =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ export const updateTotalCount = actionCreator<{ id: string; totalCount: number }

export const updateSavedSearchId = actionCreator<{
id: string;
savedSearchId: string;
savedSearchId: string | null;
}>('UPDATE_DISCOVER_SAVED_SEARCH_ID');

export const initializeSavedSearch = actionCreator<{
Expand Down

0 comments on commit 02a22fd

Please sign in to comment.