Skip to content

Commit

Permalink
[Security Solution]Analyzer in flyout Part 2 - update analyzer event …
Browse files Browse the repository at this point in the history
…node schema and enable event preview (elastic#192643)

## Summary

This PR added `_id` and `_index` to the resolver event query so that it
could support calling an event preview when showing analyzer in flyout.

Feature flag: `visualizationInFlyoutEnabled`

![image](https://github.com/user-attachments/assets/7dc27389-0bd5-491f-a1e1-6639c3dae2ed)

### Checklist

- [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

(cherry picked from commit 7b54d6f)
  • Loading branch information
christineweng committed Sep 26, 2024
1 parent 3094a7e commit 2741be2
Show file tree
Hide file tree
Showing 11 changed files with 237 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,14 @@ export function eventID(event: SafeResolverEvent): number | undefined | string {
}
}

export function documentID(event: SafeResolverEvent): string | undefined {
return firstNonNullValue(event._id);
}

export function indexName(event: SafeResolverEvent): string | undefined {
return firstNonNullValue(event._index);
}

/**
* Retrieve the record_id field from a winlog event.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,8 @@ export type WinlogEvent = Partial<{
* Safer version of ResolverEvent. Please use this going forward.
*/
export type SafeEndpointEvent = Partial<{
_id: ECSField<string>;
_index: ECSField<string>;
'@timestamp': ECSField<number>;
agent: Partial<{
id: ECSField<string>;
Expand Down Expand Up @@ -815,6 +817,8 @@ export type SafeEndpointEvent = Partial<{
}>;

export interface SafeLegacyEndpointEvent {
_id: ECSField<string>;
_index: ECSField<string>;
'@timestamp'?: ECSField<number>;
/**
* 'legacy' events must have an `endgame` key.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
* 2.0.
*/

import React from 'react';
import React, { useCallback } from 'react';
import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
import { FlyoutBody } from '@kbn/security-solution-common';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import type { DocumentDetailsAnalyzerPanelKey } from '../shared/constants/panel_keys';
import { DetailsPanel } from '../../../resolver/view/details_panel';
import { DocumentDetailsPreviewPanelKey } from '../shared/constants/panel_keys';
import { ALERT_PREVIEW_BANNER } from '../preview/constants';

interface AnalyzerPanelProps extends Record<string, unknown> {
/**
Expand All @@ -27,10 +30,32 @@ export interface AnalyzerPanelExpandableFlyoutProps extends FlyoutPanelProps {
* Displays node details panel for analyzer
*/
export const AnalyzerPanel: React.FC<AnalyzerPanelProps> = ({ resolverComponentInstanceID }) => {
const { openPreviewPanel } = useExpandableFlyoutApi();

const openPreview = useCallback(
({ documentId, indexName, scopeId }) =>
() => {
openPreviewPanel({
id: DocumentDetailsPreviewPanelKey,
params: {
id: documentId,
indexName,
scopeId,
isPreviewMode: true,
banner: ALERT_PREVIEW_BANNER,
},
});
},
[openPreviewPanel]
);

return (
<FlyoutBody>
<div style={{ marginTop: '-15px' }}>
<DetailsPanel resolverComponentInstanceID={resolverComponentInstanceID} />
<DetailsPanel
resolverComponentInstanceID={resolverComponentInstanceID}
nodeEventOnClick={openPreview}
/>
</div>
</FlyoutBody>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,65 +13,77 @@ import * as selectors from '../store/selectors';
import { PanelRouter } from './panels';
import { ResolverNoProcessEvents } from './resolver_no_process_events';
import type { State } from '../../common/store/types';
import type { NodeEventOnClick } from './panels/node_events_of_type';

interface DetailsPanelProps {
/**
* Id that identify the scope of analyzer
*/
resolverComponentInstanceID: string;
/**
* Optional callback when a node event is clicked
*/
nodeEventOnClick?: NodeEventOnClick;
}

/**
* Details panel component
*/
const DetailsPanelComponent = React.memo(({ resolverComponentInstanceID }: DetailsPanelProps) => {
const isLoading = useSelector((state: State) =>
selectors.isTreeLoading(state.analyzer[resolverComponentInstanceID])
);
const hasError = useSelector((state: State) =>
selectors.hadErrorLoadingTree(state.analyzer[resolverComponentInstanceID])
);
const resolverTreeHasNodes = useSelector((state: State) =>
selectors.resolverTreeHasNodes(state.analyzer[resolverComponentInstanceID])
);
const DetailsPanelComponent = React.memo(
({ resolverComponentInstanceID, nodeEventOnClick }: DetailsPanelProps) => {
const isLoading = useSelector((state: State) =>
selectors.isTreeLoading(state.analyzer[resolverComponentInstanceID])
);
const hasError = useSelector((state: State) =>
selectors.hadErrorLoadingTree(state.analyzer[resolverComponentInstanceID])
);
const resolverTreeHasNodes = useSelector((state: State) =>
selectors.resolverTreeHasNodes(state.analyzer[resolverComponentInstanceID])
);

return isLoading ? (
<div data-test-subj="resolver:panel:loading" className="loading-container">
<EuiLoadingSpinner size="xl" />
</div>
) : hasError ? (
<div data-test-subj="resolver:panel:error" className="loading-container">
<div>
{' '}
<FormattedMessage
id="xpack.securitySolution.endpoint.resolver.panel.loadingError"
defaultMessage="Error loading data."
/>
return isLoading ? (
<div data-test-subj="resolver:panel:loading" className="loading-container">
<EuiLoadingSpinner size="xl" />
</div>
</div>
) : resolverTreeHasNodes ? (
<PanelRouter id={resolverComponentInstanceID} />
) : (
<ResolverNoProcessEvents />
);
});
) : hasError ? (
<div data-test-subj="resolver:panel:error" className="loading-container">
<div>
{' '}
<FormattedMessage
id="xpack.securitySolution.endpoint.resolver.panel.loadingError"
defaultMessage="Error loading data."
/>
</div>
</div>
) : resolverTreeHasNodes ? (
<PanelRouter id={resolverComponentInstanceID} nodeEventOnClick={nodeEventOnClick} />
) : (
<ResolverNoProcessEvents />
);
}
);
DetailsPanelComponent.displayName = 'DetailsPanelComponent';

/**
* Stand alone details panel to be used when in split panel mode
*/
export const DetailsPanel = React.memo(({ resolverComponentInstanceID }: DetailsPanelProps) => {
const isAnalyzerInitialized = useSelector((state: State) =>
Boolean(state.analyzer[resolverComponentInstanceID])
);
export const DetailsPanel = React.memo(
({ resolverComponentInstanceID, nodeEventOnClick }: DetailsPanelProps) => {
const isAnalyzerInitialized = useSelector((state: State) =>
Boolean(state.analyzer[resolverComponentInstanceID])
);

return isAnalyzerInitialized ? (
<DetailsPanelComponent resolverComponentInstanceID={resolverComponentInstanceID} />
) : (
<div data-test-subj="resolver:panel:loading" className="loading-container">
<EuiLoadingSpinner size="xl" />
</div>
);
});
return isAnalyzerInitialized ? (
<DetailsPanelComponent
resolverComponentInstanceID={resolverComponentInstanceID}
nodeEventOnClick={nodeEventOnClick}
/>
) : (
<div data-test-subj="resolver:panel:loading" className="loading-container">
<EuiLoadingSpinner size="xl" />
</div>
);
}
);

DetailsPanel.displayName = 'DetailsPanel';
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,26 @@
import React, { memo } from 'react';
import { useSelector } from 'react-redux';
import * as selectors from '../../store/selectors';
import { NodeEventsInCategory } from './node_events_of_type';
import { NodeEventsInCategory, type NodeEventOnClick } from './node_events_of_type';
import { NodeEvents } from './node_events';
import { NodeDetail } from './node_detail';
import { NodeList } from './node_list';
import { EventDetail } from './event_detail';
import type { PanelViewAndParameters } from '../../types';
import type { State } from '../../../common/store/types';

/**
* Show the panel that matches the `panelViewAndParameters` (derived from the browser's location.search)
*/

// eslint-disable-next-line react/display-name
export const PanelRouter = memo(function ({ id }: { id: string }) {
export const PanelRouter = memo(function ({
id,
nodeEventOnClick,
}: {
id: string;
nodeEventOnClick?: NodeEventOnClick;
}) {
const params: PanelViewAndParameters = useSelector((state: State) =>
selectors.panelViewAndParameters(state.analyzer[id])
);
Expand All @@ -34,6 +41,7 @@ export const PanelRouter = memo(function ({ id }: { id: string }) {
id={id}
nodeID={params.panelParameters.nodeID}
eventCategory={params.panelParameters.eventCategory}
nodeEventOnClick={nodeEventOnClick}
/>
);
} else if (params.panelView === 'eventDetail') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@
* 2.0.
*/

import { act } from '@testing-library/react';
import React from 'react';
import { act, render } from '@testing-library/react';
import type { History as HistoryPackageHistoryInterface } from 'history';
import { createMemoryHistory } from 'history';

import { TestProviders } from '../../../common/mock';
import { NodeEventsListItem } from './node_events_of_type';
import { oneNodeWithPaginatedEvents } from '../../data_access_layer/mocks/one_node_with_paginated_related_events';
import { Simulator } from '../../test_utilities/simulator';
// Extend jest with a custom matcher
import '../../test_utilities/extend_jest';
import { urlSearch } from '../../test_utilities/url_search';
import { useLinkProps } from '../use_link_props';

jest.mock('../use_link_props');
const mockUseLinkProps = useLinkProps as jest.Mock;

// the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances
const resolverComponentInstanceID = 'resolverComponentInstanceID';
Expand Down Expand Up @@ -106,3 +112,35 @@ describe(`Resolver: when analyzing a tree with only the origin and paginated rel
});
});
});

describe('<NodeEventsListItem />', () => {
it('should call custom node onclick when it is available', () => {
const nodeEventOnClick = jest.fn();
mockUseLinkProps.mockReturnValue({ href: '#', onClick: jest.fn() });
const { getByTestId } = render(
<TestProviders>
<NodeEventsListItem
id="test"
nodeID="test"
eventCategory="test"
nodeEventOnClick={nodeEventOnClick}
event={{
_id: 'test _id',
_index: '_index',
'@timestamp': 1726589803115,
event: {
id: 'event id',
},
}}
/>
</TestProviders>
);
expect(getByTestId('resolver:panel:node-events-in-category:event-link')).toBeInTheDocument();
getByTestId('resolver:panel:node-events-in-category:event-link').click();
expect(nodeEventOnClick).toBeCalledWith({
documentId: 'test _id',
indexName: '_index',
scopeId: 'test',
});
});
});
Loading

0 comments on commit 2741be2

Please sign in to comment.