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

[Security Solution]Analyzer in flyout Part 2 - update analyzer event node schema and enable event preview #192643

Merged
merged 2 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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