Skip to content

Commit

Permalink
[Security Solutions] Add context to risk score visualisations attache…
Browse files Browse the repository at this point in the history
…d to cases (#175514)

## Summary

* Add description to the Lens attachment when adding the risk score
visualisation to a case.
* Set the visualisation width inside cases attachment 
![Screenshot 2024-01-25 at 12 03
32](https://github.com/elastic/kibana/assets/1490444/fa3a4181-d0b8-456f-9ec0-6e74e8e22a98)



* Update the time range to use static dates instead of relative dates to
make the attached visualisation a snapshot of the period.
![Screenshot 2024-01-25 at 12 04
28](https://github.com/elastic/kibana/assets/1490444/de24986c-b3be-41e5-b55d-127e4532a52a)



### How to test it?


* Enable the experimental flags `newUserDetailsFlyout` and
`newHostDetailsFlyout`
* Load events into security solutions
* Create alerts for those events
* Enable the risk engine 
* Open entity analytics dashboard to check if it has generated data (if
data doesn't immediately show up restart the engine to force it to run)
* Go to the alerts page and click on a `host.name` or `user.name` value
* On the opened entity details flyout interact with the lens
visualisation to add it to a case
* Open the case that you attached the visualisation to and check the
comment.

![Screenshot 2024-01-25 at 13 45
08](https://github.com/elastic/kibana/assets/1490444/87c0fa82-d61f-4534-a9ee-dcfd74ef826d)


### Checklist

Delete any items that are not applicable to this PR.

- [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
  • Loading branch information
machadoum committed Jan 29, 2024
1 parent fb1ac53 commit d1bc6f9
Show file tree
Hide file tree
Showing 16 changed files with 178 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ describe('utils', () => {
Object {
"persistableStateAttachmentState": Object {
"attributes": Object {},
"metadata": undefined,
"timeRange": Object {},
},
"persistableStateAttachmentTypeId": ".lens",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
* 2.0.
*/
import type { IEmbeddable } from '@kbn/embeddable-plugin/public';
import type { LensEmbeddableInput, LensSavedObjectAttributes } from '@kbn/lens-plugin/public';
import type { LensSavedObjectAttributes } from '@kbn/lens-plugin/public';
import { LENS_EMBEDDABLE_TYPE, type Embeddable as LensEmbeddable } from '@kbn/lens-plugin/public';
import { LENS_ATTACHMENT_TYPE } from '../../../../common/constants/visualizations';
import type { PersistableStateAttachmentPayload } from '../../../../common/types/domain';
import { AttachmentType } from '../../../../common/types/domain';
import type { LensProps } from '../types';

export const isLensEmbeddable = (embeddable: IEmbeddable): embeddable is LensEmbeddable => {
return embeddable.type === LENS_EMBEDDABLE_TYPE;
Expand All @@ -26,12 +27,14 @@ type PersistableStateAttachmentWithoutOwner = Omit<PersistableStateAttachmentPay
export const getLensCaseAttachment = ({
timeRange,
attributes,
metadata,
}: {
timeRange: LensEmbeddableInput['timeRange'];
timeRange: LensProps['timeRange'];
attributes: LensSavedObjectAttributes;
metadata?: LensProps['metadata'];
}): PersistableStateAttachmentWithoutOwner =>
({
persistableStateAttachmentState: { attributes, timeRange },
persistableStateAttachmentState: { attributes, timeRange, metadata },
persistableStateAttachmentTypeId: LENS_ATTACHMENT_TYPE,
type: AttachmentType.persistableState,
} as unknown as PersistableStateAttachmentWithoutOwner);
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function getOpenLensButton(attachmentId: string, props: LensProps) {
attachmentId={attachmentId}
attributes={props.attributes}
timeRange={props.timeRange}
metadata={props.metadata}
/>
);
}
Expand All @@ -40,9 +41,10 @@ const getVisualizationAttachmentActions = (attachmentId: string, props: LensProp

const LensAttachment = React.memo(
(props: PersistableStateAttachmentViewProps) => {
const { attributes, timeRange } = props.persistableStateAttachmentState as unknown as LensProps;
const { attributes, timeRange, metadata } =
props.persistableStateAttachmentState as unknown as LensProps;

return <LensRenderer attributes={attributes} timeRange={timeRange} />;
return <LensRenderer attributes={attributes} timeRange={timeRange} metadata={metadata} />;
},
(prevProps, nextProps) =>
deepEqual(prevProps.persistableStateAttachmentState, nextProps.persistableStateAttachmentState)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,13 @@ describe('LensRenderer', () => {

expect(screen.queryByTestId('embeddableComponent')).not.toBeInTheDocument();
});

it('renders the lens visualization with description', () => {
appMockRender.render(
// @ts-expect-error: props are correct
<LensRenderer {...lensVisualization} metadata={{ description: 'description' }} />
);

expect(screen.getByText('description')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import React from 'react';
import styled from 'styled-components';

import { createGlobalStyle } from '@kbn/kibana-react-plugin/common';
import { EuiSpacer } from '@elastic/eui';
import { useKibana } from '../../common/lib/kibana';
import type { LensProps } from './types';

Expand All @@ -25,7 +26,7 @@ const LensChartTooltipFix = createGlobalStyle`
}
`;

const LensRendererComponent: React.FC<LensProps> = ({ attributes, timeRange }) => {
const LensRendererComponent: React.FC<LensProps> = ({ attributes, timeRange, metadata }) => {
const {
lens: { EmbeddableComponent },
} = useKibana().services;
Expand All @@ -35,22 +36,30 @@ const LensRendererComponent: React.FC<LensProps> = ({ attributes, timeRange }) =
}

return (
<Container>
<EmbeddableComponent
id=""
style={{ height: LENS_VISUALIZATION_HEIGHT }}
timeRange={timeRange}
attributes={attributes}
renderMode="view"
disableTriggers
executionContext={{
type: 'cases',
}}
syncTooltips={false}
syncCursor={false}
/>
<LensChartTooltipFix />
</Container>
<>
{metadata && metadata.description && (
<>
{metadata.description}
<EuiSpacer size="s" />
</>
)}
<Container>
<EmbeddableComponent
id=""
style={{ height: LENS_VISUALIZATION_HEIGHT }}
timeRange={timeRange}
attributes={attributes}
renderMode="view"
disableTriggers
executionContext={{
type: 'cases',
}}
syncTooltips={false}
syncCursor={false}
/>
<LensChartTooltipFix />
</Container>
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,11 @@

import type { TypedLensByValueInput } from '@kbn/lens-plugin/public';

export type LensProps = Pick<TypedLensByValueInput, 'attributes' | 'timeRange'>;
export type LensProps = Pick<TypedLensByValueInput, 'attributes' | 'timeRange'> & {
/**
* Optional metadata used to customize the Lens Attachment rendering.
*/
metadata?: {
description?: string;
};
};
1 change: 1 addition & 0 deletions x-pack/plugins/cases/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,5 +172,6 @@ export type SupportedCaseAttachment =
export type CaseAttachments = SupportedCaseAttachment[];
export type CaseAttachmentWithoutOwner = DistributiveOmit<SupportedCaseAttachment, 'owner'>;
export type CaseAttachmentsWithoutOwner = CaseAttachmentWithoutOwner[];
export type { LensProps } from './components/visualizations/types';

export type ServerError = IHttpFetchError<ResponseErrorBody>;
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const VisualizationActionsComponent: React.FC<VisualizationActionsProps> = ({
scopeId = SourcererScopeName.default,
stackByField,
withActions = DEFAULT_ACTIONS,
casesAttachmentMetadata,
}) => {
const [isPopoverOpen, setPopover] = useState(false);
const [isInspectModalOpen, setIsInspectModalOpen] = useState(false);
Expand Down Expand Up @@ -121,6 +122,7 @@ const VisualizationActionsComponent: React.FC<VisualizationActionsProps> = ({
inspectActionProps,
timeRange: timerange,
withActions,
lensMetadata: casesAttachmentMetadata,
});

const panels = useAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const LensEmbeddableComponent: React.FC<LensEmbeddableComponentProps> = ({
width: wrapperWidth,
withActions = DEFAULT_ACTIONS,
disableOnClickFilter = false,
casesAttachmentMetadata,
}) => {
const style = useMemo(
() => ({
Expand Down Expand Up @@ -152,6 +153,7 @@ const LensEmbeddableComponent: React.FC<LensEmbeddableComponentProps> = ({
inspectActionProps,
timeRange: timerange,
withActions,
lensMetadata: casesAttachmentMetadata,
});

const updateDateRange = useCallback(
Expand Down Expand Up @@ -240,6 +242,7 @@ const LensEmbeddableComponent: React.FC<LensEmbeddableComponentProps> = ({
timerange={timerange}
title={inspectTitle}
withActions={withActions}
casesAttachmentMetadata={casesAttachmentMetadata}
/>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { DataViewSpec } from '@kbn/data-views-plugin/common';
import type { Action } from '@kbn/ui-actions-plugin/public';
import type { Filter, Query } from '@kbn/es-query';

import type { LensProps } from '@kbn/cases-plugin/public/types';
import type { InputsModelId } from '../../store/inputs/constants';
import type { SourcererScopeName } from '../../store/sourcerer/model';
import type { Status } from '../../../../common/api/detection_engine';
Expand Down Expand Up @@ -61,6 +62,7 @@ export interface VisualizationActionsProps {
timerange: { from: string; to: string };
title: React.ReactNode;
withActions?: VisualizationContextMenuActions[];
casesAttachmentMetadata?: LensProps['metadata'];
}

export interface EmbeddableData {
Expand Down Expand Up @@ -100,6 +102,11 @@ export interface LensEmbeddableComponentProps {
* Disable the on click filter for the visualization.
*/
disableOnClickFilter?: boolean;

/**
* Metadata for cases Attachable visualization.
*/
casesAttachmentMetadata?: LensProps['metadata'];
}

export enum RequestStatus {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { Action, Trigger } from '@kbn/ui-actions-plugin/public';

import { createAction } from '@kbn/ui-actions-plugin/public';
import type { ActionDefinition } from '@kbn/ui-actions-plugin/public/actions';
import type { LensProps } from '@kbn/cases-plugin/public/types';
import { useKibana } from '../../lib/kibana/kibana_react';
import { useAddToExistingCase } from './use_add_to_existing_case';
import { useAddToNewCase } from './use_add_to_new_case';
Expand Down Expand Up @@ -84,12 +85,14 @@ const ACTION_DEFINITION: Record<

export const useActions = ({
attributes,
lensMetadata,
extraActions = [],
inspectActionProps,
timeRange,
withActions = DEFAULT_ACTIONS,
}: {
attributes: LensAttributes | null;
lensMetadata?: LensProps['metadata'];
extraActions?: Action[];
inspectActionProps: {
handleInspectClick: () => void;
Expand Down Expand Up @@ -123,11 +126,13 @@ export const useActions = ({
useAddToExistingCase({
lensAttributes: attributes,
timeRange,
lensMetadata,
});

const { onAddToNewCaseClicked, disabled: isAddToNewCaseDisabled } = useAddToNewCase({
timeRange,
lensAttributes: attributes,
lensMetadata,
});

const { openSaveVisualizationFlyout, disableVisualizations } = useSaveToLibrary({ attributes });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ describe('useAddToExistingCase', () => {
lensAttributes: kpiHostMetricLensAttributes,
timeRange,
onAddToCaseClicked: mockOnAddToCaseClicked,
lensMetadata: undefined,
})
);
expect(mockGetUseCasesAddToExistingCaseModal).toHaveBeenCalledWith({
Expand All @@ -75,6 +76,7 @@ describe('useAddToExistingCase', () => {
lensAttributes: kpiHostMetricLensAttributes,
timeRange,
onAddToCaseClicked: mockOnAddToCaseClicked,
lensMetadata: undefined,
})
);
expect(result.current.disabled).toEqual(true);
Expand All @@ -88,6 +90,7 @@ describe('useAddToExistingCase', () => {
lensAttributes: kpiHostMetricLensAttributes,
timeRange,
onAddToCaseClicked: mockOnAddToCaseClicked,
lensMetadata: undefined,
})
);
expect(result.current.disabled).toEqual(true);
Expand All @@ -99,6 +102,7 @@ describe('useAddToExistingCase', () => {
lensAttributes: null,
timeRange,
onAddToCaseClicked: mockOnAddToCaseClicked,
lensMetadata: undefined,
})
);
expect(result.current.disabled).toEqual(true);
Expand All @@ -110,6 +114,7 @@ describe('useAddToExistingCase', () => {
lensAttributes: kpiHostMetricLensAttributes,
timeRange: null,
onAddToCaseClicked: mockOnAddToCaseClicked,
lensMetadata: undefined,
})
);
expect(result.current.disabled).toEqual(true);
Expand All @@ -118,6 +123,9 @@ describe('useAddToExistingCase', () => {
it('should open add to existing case modal', () => {
const mockOpenCaseModal = jest.fn();
const mockClick = jest.fn();
const lensMetadata = {
description: 'test_description',
};

mockGetUseCasesAddToExistingCaseModal.mockReturnValue({ open: mockOpenCaseModal });

Expand All @@ -126,6 +134,7 @@ describe('useAddToExistingCase', () => {
lensAttributes: kpiHostMetricLensAttributes,
timeRange,
onAddToCaseClicked: mockClick,
lensMetadata,
})
);

Expand All @@ -137,6 +146,7 @@ describe('useAddToExistingCase', () => {
persistableStateAttachmentState: {
attributes: kpiHostMetricLensAttributes,
timeRange,
metadata: lensMetadata,
},
persistableStateAttachmentTypeId: '.lens',
type: AttachmentType.persistableState as const,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,37 @@ import { useCallback, useMemo } from 'react';
import { AttachmentType, LENS_ATTACHMENT_TYPE } from '@kbn/cases-plugin/common';
import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public';

import type { LensProps } from '@kbn/cases-plugin/public/types';
import { APP_ID } from '../../../../common';
import { useKibana } from '../../lib/kibana';
import { ADD_TO_CASE_SUCCESS } from './translations';
import type { LensAttributes } from './types';

export const useAddToExistingCase = ({
onAddToCaseClicked,
lensAttributes,
timeRange,
lensMetadata,
}: {
onAddToCaseClicked?: () => void;
lensAttributes: LensAttributes | null;
timeRange: { from: string; to: string } | null;
lensAttributes: LensProps['attributes'] | null;
timeRange: LensProps['timeRange'] | null;
lensMetadata: LensProps['metadata'];
}) => {
const { cases } = useKibana().services;
const userCasesPermissions = cases.helpers.canUseCases([APP_ID]);
const attachments = useMemo(() => {
return [
{
persistableStateAttachmentState: { attributes: lensAttributes, timeRange },
persistableStateAttachmentState: {
attributes: lensAttributes,
timeRange,
metadata: lensMetadata,
},
persistableStateAttachmentTypeId: LENS_ATTACHMENT_TYPE,
type: AttachmentType.persistableState as const,
},
] as CaseAttachmentsWithoutOwner;
}, [lensAttributes, timeRange]);
}, [lensAttributes, lensMetadata, timeRange]);

const selectCaseModal = cases.hooks.useCasesAddToExistingCaseModal({
onClose: onAddToCaseClicked,
Expand Down
Loading

0 comments on commit d1bc6f9

Please sign in to comment.