mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution][Detection & Response] 131028 implement recently opened cases table (#131029)
* First draft at possible implementation. tests need to be added, and imports,comments, logs cleaned up * Further tweaks to alerts counters * Add tests for alerts_counters hook * Working on vulnerable hosts * Add useVulnerableHostsCounters hook along with tests * add Vulnerable users and tests * Move files to components folder and wire up to detections overview page * Add translations * add querytoggle and navigation to both tables * fix bug for toggleQuery * update button navigation * remove alerts by status, as Angela built instead * Working on changing test files * test files for host and user hooks complete * Components complete * bug fixes from PR * failing tests * Undo bad edit to useRuleAlerts test * Fix show inspect on hover, and use HostDetailsLink component * missed in last commit * more fixes from PR review * recent cases table working, need tests * first pass for table and data fetching * Make changes from PR review * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * PR fixes * PR fixes * remove cases api date workaround * enable detectionsResponse for deployed instance * Fix tests * turn off detectionresponseEnabled flag * fixes from design review * stability fix. remove useUserInfo * Add comment for removing togglequery code Co-authored-by: Kristof-Pierre Cummings <kristofpierre.cummings@elastic.co> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
a632484214
commit
c43a51d7ab
18 changed files with 888 additions and 63 deletions
|
@ -43,6 +43,18 @@ describe('formatted_date', () => {
|
|||
expect(wrapper.text()).toEqual('Feb 25, 2019 @ 22:27:05.000');
|
||||
});
|
||||
|
||||
test.each([
|
||||
['MMMM D, YYYY', 'February 25, 2019'],
|
||||
['MM/D/YY', '02/25/19'],
|
||||
['d-m-y', '1-27-2019'],
|
||||
])('it renders the date in the correct format: %s', (momentDateFormat, expectedResult) => {
|
||||
const wrapper = mount(
|
||||
<PreferenceFormattedDate value={isoDate} dateFormat={momentDateFormat} />
|
||||
);
|
||||
|
||||
expect(wrapper.text()).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test('it renders a UTC ISO8601 date string supplied when no date format configuration exists', () => {
|
||||
mockUseDateFormat.mockImplementation(() => '');
|
||||
const wrapper = mount(<PreferenceFormattedDate value={isoDate} />);
|
||||
|
|
|
@ -97,16 +97,18 @@ interface FormattedDateProps {
|
|||
className?: string;
|
||||
fieldName: string;
|
||||
value?: string | number | null;
|
||||
dateFormat?: string;
|
||||
}
|
||||
export const FormattedDate = React.memo<FormattedDateProps>(
|
||||
({ value, fieldName, className = '' }): JSX.Element => {
|
||||
({ value, fieldName, className = '', dateFormat }): JSX.Element => {
|
||||
if (value == null) {
|
||||
return getOrEmptyTagFromValue(value);
|
||||
}
|
||||
|
||||
const maybeDate = getMaybeDate(value);
|
||||
return maybeDate.isValid() ? (
|
||||
<LocalizedDateTooltip date={maybeDate.toDate()} fieldName={fieldName} className={className}>
|
||||
<PreferenceFormattedDate value={maybeDate.toDate()} />
|
||||
<PreferenceFormattedDate value={maybeDate.toDate()} dateFormat={dateFormat} />
|
||||
</LocalizedDateTooltip>
|
||||
) : (
|
||||
getOrEmptyTagFromValue(value)
|
||||
|
|
|
@ -49,6 +49,11 @@ const StyledLegendFlexItem = styled(EuiFlexItem)`
|
|||
padding-top: 45px;
|
||||
`;
|
||||
|
||||
// To Do remove this styled component once togglequery is updated: #131405
|
||||
const StyledEuiPanel = styled(EuiPanel)`
|
||||
height: fit-content;
|
||||
`;
|
||||
|
||||
interface AlertsByStatusProps {
|
||||
signalIndexName: string | null;
|
||||
}
|
||||
|
@ -119,7 +124,10 @@ export const AlertsByStatus = ({ signalIndexName }: AlertsByStatusProps) => {
|
|||
return (
|
||||
<>
|
||||
<HoverVisibilityContainer show={true} targetClassNames={[INPECT_BUTTON_CLASS]}>
|
||||
<EuiPanel hasBorder data-test-subj={`${DETECTION_RESPONSE_ALERTS_BY_STATUS_ID}-panel`}>
|
||||
<StyledEuiPanel
|
||||
hasBorder
|
||||
data-test-subj={`${DETECTION_RESPONSE_ALERTS_BY_STATUS_ID}-panel`}
|
||||
>
|
||||
{loading && (
|
||||
<EuiProgress
|
||||
data-test-subj="initialLoadingPanelMatrixOverTime"
|
||||
|
@ -204,7 +212,7 @@ export const AlertsByStatus = ({ signalIndexName }: AlertsByStatusProps) => {
|
|||
<EuiSpacer size="m" />
|
||||
</>
|
||||
)}
|
||||
</EuiPanel>
|
||||
</StyledEuiPanel>
|
||||
</HoverVisibilityContainer>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiPanel, EuiText } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
|
||||
import { AxisStyle, Rotation, ScaleType } from '@elastic/charts';
|
||||
import styled from 'styled-components';
|
||||
import { FormattedNumber } from '@kbn/i18n-react';
|
||||
|
@ -112,6 +112,10 @@ const Wrapper = styled.div`
|
|||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledEuiPanel = styled(EuiPanel)`
|
||||
height: 258px;
|
||||
`;
|
||||
|
||||
const CasesByStatusComponent: React.FC = () => {
|
||||
const { toggleStatus, setToggleStatus } = useQueryToggle(CASES_BY_STATUS_ID);
|
||||
const { getAppUrl, navigateTo } = useNavigation();
|
||||
|
@ -151,7 +155,7 @@ const CasesByStatusComponent: React.FC = () => {
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder>
|
||||
<StyledEuiPanel hasBorder>
|
||||
<HeaderSection
|
||||
id={CASES_BY_STATUS_ID}
|
||||
title={CASES_BY_STATUS_SECTION_TITLE}
|
||||
|
@ -177,11 +181,8 @@ const CasesByStatusComponent: React.FC = () => {
|
|||
<>
|
||||
<b>
|
||||
<FormattedNumber value={totalCounts} />
|
||||
</b>
|
||||
<> </>
|
||||
<small>
|
||||
<EuiLink onClick={goToCases}>{CASES(totalCounts)}</EuiLink>
|
||||
</small>
|
||||
</b>{' '}
|
||||
<span> {CASES(totalCounts)}</span>
|
||||
</>
|
||||
</EuiText>
|
||||
)}
|
||||
|
@ -193,7 +194,7 @@ const CasesByStatusComponent: React.FC = () => {
|
|||
</StyledEuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiPanel>
|
||||
</StyledEuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { parsedCasesItems } from './mock_data';
|
||||
import { CasesTable } from './cases_table';
|
||||
import type { UseCaseItems } from './use_case_items';
|
||||
|
||||
const mockGetAppUrl = jest.fn();
|
||||
jest.mock('../../../../common/lib/kibana/hooks', () => {
|
||||
const original = jest.requireActual('../../../../common/lib/kibana/hooks');
|
||||
return {
|
||||
...original,
|
||||
useNavigation: () => ({
|
||||
getAppUrl: mockGetAppUrl,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
type UseCaseItemsReturn = ReturnType<UseCaseItems>;
|
||||
const defaultCaseItemsReturn: UseCaseItemsReturn = {
|
||||
items: [],
|
||||
isLoading: false,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
const mockUseCaseItems = jest.fn(() => defaultCaseItemsReturn);
|
||||
const mockUseCaseItemsReturn = (overrides: Partial<UseCaseItemsReturn>) => {
|
||||
mockUseCaseItems.mockReturnValueOnce({
|
||||
...defaultCaseItemsReturn,
|
||||
...overrides,
|
||||
});
|
||||
};
|
||||
|
||||
jest.mock('./use_case_items', () => ({
|
||||
useCaseItems: () => mockUseCaseItems(),
|
||||
}));
|
||||
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<TestProviders>
|
||||
<CasesTable />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
describe('CasesTable', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render empty table', () => {
|
||||
const { getByText, getByTestId } = renderComponent();
|
||||
|
||||
expect(getByTestId('recentlyCreatedCasesPanel')).toBeInTheDocument();
|
||||
expect(getByText('No cases to display')).toBeInTheDocument();
|
||||
expect(getByTestId('allCasesButton')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a loading table', () => {
|
||||
mockUseCaseItemsReturn({ isLoading: true });
|
||||
const { getByText, getByTestId } = renderComponent();
|
||||
|
||||
expect(getByText('Updating...')).toBeInTheDocument();
|
||||
expect(getByTestId('allCasesButton')).toBeInTheDocument();
|
||||
expect(getByTestId('recentlyCreatedCasesTable')).toHaveClass('euiBasicTable-loading');
|
||||
});
|
||||
|
||||
it('should render the updated at subtitle', () => {
|
||||
mockUseCaseItemsReturn({ isLoading: false });
|
||||
const { getByText } = renderComponent();
|
||||
|
||||
expect(getByText('Updated now')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the table columns', () => {
|
||||
mockUseCaseItemsReturn({ items: parsedCasesItems });
|
||||
const { getAllByRole } = renderComponent();
|
||||
|
||||
const columnHeaders = getAllByRole('columnheader');
|
||||
expect(columnHeaders.at(0)).toHaveTextContent('Name');
|
||||
expect(columnHeaders.at(1)).toHaveTextContent('Note');
|
||||
expect(columnHeaders.at(2)).toHaveTextContent('Time');
|
||||
expect(columnHeaders.at(3)).toHaveTextContent('Created by');
|
||||
expect(columnHeaders.at(4)).toHaveTextContent('Status');
|
||||
});
|
||||
|
||||
it('should render the table items', () => {
|
||||
mockUseCaseItemsReturn({ items: [parsedCasesItems[0]] });
|
||||
const { getByTestId } = renderComponent();
|
||||
|
||||
expect(getByTestId('recentlyCreatedCaseName')).toHaveTextContent('sdcsd');
|
||||
expect(getByTestId('recentlyCreatedCaseNote')).toHaveTextContent('klklk');
|
||||
expect(getByTestId('recentlyCreatedCaseTime')).toHaveTextContent('April 25, 2022');
|
||||
expect(getByTestId('recentlyCreatedCaseCreatedBy')).toHaveTextContent('elastic');
|
||||
expect(getByTestId('recentlyCreatedCaseStatus')).toHaveTextContent('Open');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiBasicTableColumn,
|
||||
EuiButton,
|
||||
EuiEmptyPrompt,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { CaseStatuses } from '@kbn/cases-plugin/common';
|
||||
|
||||
import { SecurityPageName } from '../../../../app/types';
|
||||
import { FormattedDate } from '../../../../common/components/formatted_date';
|
||||
import { HeaderSection } from '../../../../common/components/header_section';
|
||||
import { HoverVisibilityContainer } from '../../../../common/components/hover_visibility_container';
|
||||
import { BUTTON_CLASS as INPECT_BUTTON_CLASS } from '../../../../common/components/inspect';
|
||||
import { CaseDetailsLink } from '../../../../common/components/links';
|
||||
import { useQueryToggle } from '../../../../common/containers/query_toggle';
|
||||
import { useNavigation, NavigateTo, GetAppUrl } from '../../../../common/lib/kibana';
|
||||
import * as i18n from '../translations';
|
||||
import { LastUpdatedAt } from '../util';
|
||||
import { StatusBadge } from './status_badge';
|
||||
import { CaseItem, useCaseItems } from './use_case_items';
|
||||
|
||||
type GetTableColumns = (params: {
|
||||
getAppUrl: GetAppUrl;
|
||||
navigateTo: NavigateTo;
|
||||
}) => Array<EuiBasicTableColumn<CaseItem>>;
|
||||
|
||||
const DETECTION_RESPONSE_RECENT_CASES_QUERY_ID = 'recentlyCreatedCasesQuery';
|
||||
|
||||
export const CasesTable = React.memo(() => {
|
||||
const { getAppUrl, navigateTo } = useNavigation();
|
||||
const { toggleStatus, setToggleStatus } = useQueryToggle(
|
||||
DETECTION_RESPONSE_RECENT_CASES_QUERY_ID
|
||||
);
|
||||
const { items, isLoading, updatedAt } = useCaseItems({
|
||||
skip: !toggleStatus,
|
||||
});
|
||||
|
||||
const navigateToCases = useCallback(() => {
|
||||
navigateTo({ deepLinkId: SecurityPageName.case });
|
||||
}, [navigateTo]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => getTableColumns({ getAppUrl, navigateTo }),
|
||||
[getAppUrl, navigateTo]
|
||||
);
|
||||
|
||||
return (
|
||||
<HoverVisibilityContainer show={true} targetClassNames={[INPECT_BUTTON_CLASS]}>
|
||||
<EuiPanel hasBorder data-test-subj="recentlyCreatedCasesPanel">
|
||||
<HeaderSection
|
||||
id={DETECTION_RESPONSE_RECENT_CASES_QUERY_ID}
|
||||
title={i18n.CASES_TABLE_SECTION_TITLE}
|
||||
titleSize="s"
|
||||
toggleStatus={toggleStatus}
|
||||
toggleQuery={setToggleStatus}
|
||||
subtitle={<LastUpdatedAt updatedAt={updatedAt} isUpdating={isLoading} />}
|
||||
showInspectButton={false}
|
||||
/>
|
||||
|
||||
{toggleStatus && (
|
||||
<>
|
||||
<EuiBasicTable
|
||||
data-test-subj="recentlyCreatedCasesTable"
|
||||
columns={columns}
|
||||
items={items}
|
||||
loading={isLoading}
|
||||
noItemsMessage={
|
||||
<EuiEmptyPrompt title={<h3>{i18n.NO_CASES_FOUND}</h3>} titleSize="xs" />
|
||||
}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiButton data-test-subj="allCasesButton" onClick={navigateToCases}>
|
||||
{i18n.VIEW_RECENT_CASES}
|
||||
</EuiButton>
|
||||
</>
|
||||
)}
|
||||
</EuiPanel>
|
||||
</HoverVisibilityContainer>
|
||||
);
|
||||
});
|
||||
|
||||
CasesTable.displayName = 'CasesTable';
|
||||
|
||||
const getTableColumns: GetTableColumns = () => [
|
||||
{
|
||||
field: 'id',
|
||||
name: i18n.CASES_TABLE_COLUMN_NAME,
|
||||
truncateText: true,
|
||||
textOnly: true,
|
||||
'data-test-subj': 'recentlyCreatedCaseName',
|
||||
|
||||
render: (id: string, { name }) => <CaseDetailsLink detailName={id}>{name}</CaseDetailsLink>,
|
||||
},
|
||||
{
|
||||
field: 'note',
|
||||
name: i18n.CASES_TABLE_COLUMN_NOTE,
|
||||
truncateText: true,
|
||||
textOnly: true,
|
||||
render: (note: string) => (
|
||||
<EuiText data-test-subj="recentlyCreatedCaseNote" size="s">
|
||||
{note}
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'createdAt',
|
||||
name: i18n.CASES_TABLE_COLUMN_TIME,
|
||||
render: (createdAt: string) => (
|
||||
<FormattedDate
|
||||
fieldName={i18n.CASES_TABLE_COLUMN_TIME}
|
||||
value={createdAt}
|
||||
className="eui-textTruncate"
|
||||
dateFormat="MMMM D, YYYY"
|
||||
/>
|
||||
),
|
||||
'data-test-subj': 'recentlyCreatedCaseTime',
|
||||
},
|
||||
{
|
||||
field: 'createdBy',
|
||||
name: i18n.CASES_TABLE_COLUMN_CREATED_BY,
|
||||
render: (createdBy: string) => (
|
||||
<EuiText data-test-subj="recentlyCreatedCaseCreatedBy" size="s">
|
||||
{createdBy}
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
name: i18n.CASES_TABLE_COLUMN_STATUS,
|
||||
render: (status: CaseStatuses) => <StatusBadge status={status} />,
|
||||
'data-test-subj': 'recentlyCreatedCaseStatus',
|
||||
},
|
||||
];
|
|
@ -5,5 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { HostAlertsTable } from './host_alerts_table';
|
||||
export { UserAlertsTable } from './user_alerts_table';
|
||||
export { CasesTable } from './cases_table';
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* 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 { CaseStatuses } from '@kbn/cases-plugin/common';
|
||||
|
||||
export const mockCasesResult = {
|
||||
cases: [
|
||||
{
|
||||
id: '0ce2a510-c43a-11ec-98b5-3109bd2a1901',
|
||||
version: 'Wzg2MDIsMV0=',
|
||||
comments: [],
|
||||
totalComment: 0,
|
||||
totalAlerts: 0,
|
||||
title: 'sdcsd',
|
||||
tags: [],
|
||||
description: 'klklk',
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
owner: 'securitySolution',
|
||||
closed_at: '2022-04-25T01:50:40.435Z',
|
||||
closed_by: {
|
||||
full_name: null,
|
||||
email: null,
|
||||
username: 'elastic',
|
||||
},
|
||||
createdAt: '2022-04-25T01:50:14.499Z',
|
||||
createdBy: {
|
||||
username: 'elastic',
|
||||
email: null,
|
||||
full_name: null,
|
||||
},
|
||||
status: 'open',
|
||||
updated_at: '2022-04-25T01:50:40.435Z',
|
||||
updated_by: {
|
||||
full_name: null,
|
||||
email: null,
|
||||
username: 'elastic',
|
||||
},
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: '.none',
|
||||
fields: null,
|
||||
},
|
||||
external_service: null,
|
||||
},
|
||||
{
|
||||
id: 'dc12f930-c178-11ec-98b5-3109bd2a1901',
|
||||
version: 'Wzg5NjksMV0=',
|
||||
comments: [],
|
||||
totalComment: 0,
|
||||
totalAlerts: 0,
|
||||
title: 'zzzz',
|
||||
tags: [],
|
||||
description: 'sssss',
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
owner: 'securitySolution',
|
||||
closed_at: '2022-04-25T13:45:18.317Z',
|
||||
closed_by: {
|
||||
full_name: null,
|
||||
email: null,
|
||||
username: 'elastic',
|
||||
},
|
||||
createdAt: '2022-04-21T13:42:17.414Z',
|
||||
createdBy: {
|
||||
username: 'elastic',
|
||||
email: null,
|
||||
full_name: null,
|
||||
},
|
||||
status: 'in-progress',
|
||||
updated_at: '2022-04-25T13:45:18.317Z',
|
||||
updated_by: {
|
||||
full_name: null,
|
||||
email: null,
|
||||
username: 'elastic',
|
||||
},
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: '.none',
|
||||
fields: null,
|
||||
},
|
||||
external_service: null,
|
||||
},
|
||||
{
|
||||
id: 'd11cbac0-c178-11ec-98b5-3109bd2a1901',
|
||||
version: 'Wzg5ODQsMV0=',
|
||||
comments: [],
|
||||
totalComment: 0,
|
||||
totalAlerts: 0,
|
||||
title: 'asxa',
|
||||
tags: [],
|
||||
description: 'dsdd',
|
||||
settings: {
|
||||
syncAlerts: true,
|
||||
},
|
||||
owner: 'securitySolution',
|
||||
closed_at: '2022-04-25T13:45:22.539Z',
|
||||
closed_by: {
|
||||
full_name: null,
|
||||
email: null,
|
||||
username: 'elastic',
|
||||
},
|
||||
createdAt: '2022-04-21T13:41:59.025Z',
|
||||
createdBy: {
|
||||
username: 'elastic',
|
||||
email: null,
|
||||
full_name: null,
|
||||
},
|
||||
status: 'closed',
|
||||
updated_at: '2022-04-25T13:45:22.539Z',
|
||||
updated_by: {
|
||||
full_name: null,
|
||||
email: null,
|
||||
username: 'elastic',
|
||||
},
|
||||
connector: {
|
||||
id: 'none',
|
||||
name: 'none',
|
||||
type: '.none',
|
||||
fields: null,
|
||||
},
|
||||
external_service: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const parsedCasesItems = [
|
||||
{
|
||||
name: 'sdcsd',
|
||||
note: 'klklk',
|
||||
createdBy: 'elastic',
|
||||
status: CaseStatuses.open,
|
||||
createdAt: '2022-04-25T01:50:14.499Z',
|
||||
id: '0ce2a510-c43a-11ec-98b5-3109bd2a1901',
|
||||
},
|
||||
{
|
||||
name: 'zzzz',
|
||||
note: 'sssss',
|
||||
status: CaseStatuses['in-progress'],
|
||||
createdAt: '2022-04-21T13:42:17.414Z',
|
||||
createdBy: 'elastic',
|
||||
id: 'dc12f930-c178-11ec-98b5-3109bd2a1901',
|
||||
},
|
||||
{
|
||||
name: 'asxa',
|
||||
note: 'dsdd',
|
||||
createdBy: 'elastic',
|
||||
status: CaseStatuses.closed,
|
||||
createdAt: '2022-04-21T13:41:59.025Z',
|
||||
id: 'd11cbac0-c178-11ec-98b5-3109bd2a1901',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import { CaseStatuses } from '@kbn/cases-plugin/common';
|
||||
import { EuiBadge } from '@elastic/eui';
|
||||
|
||||
import * as i18n from '../translations';
|
||||
|
||||
interface Props {
|
||||
status: CaseStatuses;
|
||||
}
|
||||
|
||||
const statuses = {
|
||||
[CaseStatuses.open]: {
|
||||
color: 'primary',
|
||||
label: i18n.STATUS_OPEN,
|
||||
},
|
||||
[CaseStatuses['in-progress']]: {
|
||||
color: 'warning',
|
||||
label: i18n.STATUS_IN_PROGRESS,
|
||||
},
|
||||
[CaseStatuses.closed]: {
|
||||
color: 'default',
|
||||
label: i18n.STATUS_CLOSED,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const StatusBadge: React.FC<Props> = ({ status }) => {
|
||||
return (
|
||||
<EuiBadge color={statuses[status].color} data-test-subj="case-status-badge">
|
||||
{statuses[status].label}
|
||||
</EuiBadge>
|
||||
);
|
||||
};
|
||||
|
||||
StatusBadge.displayName = 'StatusBadge';
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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 { act, renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { mockCasesResult, parsedCasesItems } from './mock_data';
|
||||
import { useCaseItems, UseCaseItemsProps } from './use_case_items';
|
||||
|
||||
import type { UseCaseItems } from './use_case_items';
|
||||
|
||||
const dateNow = new Date('2022-04-08T12:00:00.000Z').valueOf();
|
||||
const mockDateNow = jest.fn().mockReturnValue(dateNow);
|
||||
Date.now = jest.fn(() => mockDateNow()) as unknown as DateConstructor['now'];
|
||||
|
||||
const defaultCasesReturn = {
|
||||
cases: [],
|
||||
};
|
||||
|
||||
const mockCasesApi = jest.fn().mockResolvedValue(defaultCasesReturn);
|
||||
const mockKibana = {
|
||||
services: {
|
||||
cases: {
|
||||
api: {
|
||||
cases: {
|
||||
find: (...props: unknown[]) => mockCasesApi(...props),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
jest.mock('../../../../common/lib/kibana', () => ({
|
||||
useKibana: () => mockKibana,
|
||||
}));
|
||||
|
||||
const from = '2020-07-07T08:20:18.966Z';
|
||||
const to = '2020-07-08T08:20:18.966Z';
|
||||
|
||||
const mockUseGlobalTime = jest.fn().mockReturnValue({ from, to });
|
||||
jest.mock('../../../../common/containers/use_global_time', () => {
|
||||
return {
|
||||
useGlobalTime: (...props: unknown[]) => mockUseGlobalTime(...props),
|
||||
};
|
||||
});
|
||||
|
||||
const renderUseCaseItems = (overrides: Partial<UseCaseItemsProps> = {}) =>
|
||||
renderHook<UseCaseItems, ReturnType<UseCaseItems>>(() =>
|
||||
useCaseItems({ skip: false, ...overrides })
|
||||
);
|
||||
|
||||
describe('useCaseItems', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockDateNow.mockReturnValue(dateNow);
|
||||
mockCasesApi.mockResolvedValue(defaultCasesReturn);
|
||||
});
|
||||
|
||||
it('should return default values', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderUseCaseItems();
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual({
|
||||
items: [],
|
||||
isLoading: false,
|
||||
updatedAt: dateNow,
|
||||
});
|
||||
});
|
||||
|
||||
expect(mockCasesApi).toBeCalledWith({
|
||||
from: '2020-07-07T08:20:18.966Z',
|
||||
to: '2020-07-08T08:20:18.966Z',
|
||||
owner: 'securitySolution',
|
||||
sortField: 'create_at',
|
||||
sortOrder: 'desc',
|
||||
page: 1,
|
||||
perPage: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return parsed items', async () => {
|
||||
mockCasesApi.mockReturnValue(mockCasesResult);
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderUseCaseItems();
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual({
|
||||
items: parsedCasesItems,
|
||||
isLoading: false,
|
||||
updatedAt: dateNow,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return new updatedAt', async () => {
|
||||
const newDateNow = new Date('2022-04-08T14:00:00.000Z').valueOf();
|
||||
mockDateNow.mockReturnValue(newDateNow);
|
||||
mockDateNow.mockReturnValueOnce(dateNow);
|
||||
mockCasesApi.mockReturnValue(mockCasesResult);
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderUseCaseItems();
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(mockDateNow).toHaveBeenCalled();
|
||||
expect(result.current).toEqual({
|
||||
items: parsedCasesItems,
|
||||
isLoading: false,
|
||||
updatedAt: newDateNow,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip the query', () => {
|
||||
const { result } = renderUseCaseItems({ skip: true });
|
||||
|
||||
expect(result.current).toEqual({
|
||||
items: [],
|
||||
isLoading: false,
|
||||
updatedAt: dateNow,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 { useState, useEffect } from 'react';
|
||||
|
||||
import { CaseStatuses } from '@kbn/cases-plugin/common';
|
||||
import { Cases } from '@kbn/cases-plugin/common/ui';
|
||||
|
||||
import { APP_ID } from '../../../../../common/constants';
|
||||
import { useGlobalTime } from '../../../../common/containers/use_global_time';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { addError } from '../../../../common/store/app/actions';
|
||||
|
||||
import * as i18n from '../translations';
|
||||
|
||||
export interface CaseItem {
|
||||
name: string;
|
||||
note: string;
|
||||
createdAt: string;
|
||||
createdBy: string;
|
||||
status: CaseStatuses;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface UseCaseItemsProps {
|
||||
skip: boolean;
|
||||
}
|
||||
|
||||
export type UseCaseItems = (props: UseCaseItemsProps) => {
|
||||
items: CaseItem[];
|
||||
isLoading: boolean;
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
export const useCaseItems: UseCaseItems = ({ skip }) => {
|
||||
const {
|
||||
services: { cases },
|
||||
} = useKibana();
|
||||
const { to, from } = useGlobalTime();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [updatedAt, setUpdatedAt] = useState(Date.now());
|
||||
const [items, setItems] = useState<CaseItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
const fetchCases = async () => {
|
||||
try {
|
||||
const casesResponse = await cases.api.cases.find({
|
||||
from,
|
||||
to,
|
||||
owner: APP_ID,
|
||||
sortField: 'create_at',
|
||||
sortOrder: 'desc',
|
||||
page: 1,
|
||||
perPage: 4,
|
||||
});
|
||||
|
||||
if (isSubscribed) {
|
||||
setItems(parseCases(casesResponse));
|
||||
setUpdatedAt(Date.now());
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSubscribed) {
|
||||
addError(error, { title: i18n.ERROR_MESSAGE_CASES });
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
if (!skip) {
|
||||
fetchCases();
|
||||
}
|
||||
|
||||
if (skip) {
|
||||
setIsLoading(false);
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
}
|
||||
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, [cases.api.cases, from, skip, to]);
|
||||
|
||||
return { items, isLoading, updatedAt };
|
||||
};
|
||||
|
||||
function parseCases(casesResponse: Cases): CaseItem[] {
|
||||
const allCases = casesResponse.cases || [];
|
||||
|
||||
return allCases.reduce<CaseItem[]>((accumulated, currentCase) => {
|
||||
accumulated.push({
|
||||
id: currentCase.id,
|
||||
name: currentCase.title,
|
||||
note: currentCase.description,
|
||||
createdAt: currentCase.createdAt,
|
||||
createdBy: currentCase.createdBy.username || '—',
|
||||
status: currentCase.status,
|
||||
});
|
||||
|
||||
return accumulated;
|
||||
}, []);
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
EuiBasicTable,
|
||||
|
@ -40,6 +41,11 @@ interface HostAlertsTableProps {
|
|||
|
||||
const DETECTION_RESPONSE_HOST_SEVERITY_QUERY_ID = 'vulnerableHostsBySeverityQuery';
|
||||
|
||||
// To Do remove this styled component once togglequery is updated: #131405
|
||||
const StyledEuiPanel = styled(EuiPanel)`
|
||||
height: fit-content;
|
||||
`;
|
||||
|
||||
export const HostAlertsTable = React.memo(({ signalIndexName }: HostAlertsTableProps) => {
|
||||
const { getAppUrl, navigateTo } = useNavigation();
|
||||
const { toggleStatus, setToggleStatus } = useQueryToggle(
|
||||
|
@ -62,8 +68,9 @@ export const HostAlertsTable = React.memo(({ signalIndexName }: HostAlertsTableP
|
|||
);
|
||||
|
||||
return (
|
||||
// <div>
|
||||
<HoverVisibilityContainer show={true} targetClassNames={[INPECT_BUTTON_CLASS]}>
|
||||
<EuiPanel hasBorder data-test-subj="severityHostAlertsPanel">
|
||||
<StyledEuiPanel hasBorder data-test-subj="severityHostAlertsPanel">
|
||||
<HeaderSection
|
||||
id={DETECTION_RESPONSE_HOST_SEVERITY_QUERY_ID}
|
||||
title={i18n.HOST_ALERTS_SECTION_TITLE}
|
||||
|
@ -89,8 +96,9 @@ export const HostAlertsTable = React.memo(({ signalIndexName }: HostAlertsTableP
|
|||
</EuiButton>
|
||||
</>
|
||||
)}
|
||||
</EuiPanel>
|
||||
</StyledEuiPanel>
|
||||
</HoverVisibilityContainer>
|
||||
// </div>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -191,7 +191,7 @@ function parseHostsData(
|
|||
return [
|
||||
...accumalatedAlertsByHost,
|
||||
{
|
||||
hostName: currentHost.key,
|
||||
hostName: currentHost.key || '—',
|
||||
totalAlerts: currentHost.doc_count,
|
||||
low: currentHost.low.doc_count,
|
||||
medium: currentHost.medium.doc_count,
|
||||
|
|
|
@ -103,12 +103,26 @@ export const USER_ALERTS_SECTION_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const CASES_TABLE_SECTION_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.detectionResponse.caseSectionTitle',
|
||||
{
|
||||
defaultMessage: 'Recently created cases',
|
||||
}
|
||||
);
|
||||
|
||||
export const NO_ALERTS_FOUND = i18n.translate(
|
||||
'xpack.securitySolution.detectionResponse.noRuleAlerts',
|
||||
{
|
||||
defaultMessage: 'No alerts to display',
|
||||
}
|
||||
);
|
||||
|
||||
export const NO_CASES_FOUND = i18n.translate(
|
||||
'xpack.securitySolution.detectionResponse.noRecentCases',
|
||||
{
|
||||
defaultMessage: 'No cases to display',
|
||||
}
|
||||
);
|
||||
export const RULE_ALERTS_COLUMN_RULE_NAME = i18n.translate(
|
||||
'xpack.securitySolution.detectionResponse.ruleAlertsColumnRuleName',
|
||||
{
|
||||
|
@ -156,14 +170,21 @@ export const OPEN_ALL_ALERTS_BUTTON = i18n.translate(
|
|||
export const VIEW_ALL_USER_ALERTS = i18n.translate(
|
||||
'xpack.securitySolution.detectionResponse.viewAllUserAlerts',
|
||||
{
|
||||
defaultMessage: 'View all other user alerts',
|
||||
defaultMessage: 'View all users',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_RECENT_CASES = i18n.translate(
|
||||
'xpack.securitySolution.detectionResponse.viewRecentCases',
|
||||
{
|
||||
defaultMessage: 'View recent cases',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_ALL_HOST_ALERTS = i18n.translate(
|
||||
'xpack.securitySolution.detectionResponse.viewAllHostAlerts',
|
||||
{
|
||||
defaultMessage: 'View all other host alerts',
|
||||
defaultMessage: 'View all hosts',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -180,3 +201,45 @@ export const USER_ALERTS_USERNAME_COLUMN = i18n.translate(
|
|||
defaultMessage: 'User name',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASES_TABLE_COLUMN_NAME = i18n.translate(
|
||||
'xpack.securitySolution.detectionResponse.caseColumnName',
|
||||
{
|
||||
defaultMessage: 'Name',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASES_TABLE_COLUMN_NOTE = i18n.translate(
|
||||
'xpack.securitySolution.detectionResponse.caseColumnNote',
|
||||
{
|
||||
defaultMessage: 'Note',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASES_TABLE_COLUMN_TIME = i18n.translate(
|
||||
'xpack.securitySolution.detectionResponse.caseColumnTime',
|
||||
{
|
||||
defaultMessage: 'Time',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASES_TABLE_COLUMN_CREATED_BY = i18n.translate(
|
||||
'xpack.securitySolution.detectionResponse.caseColumnCreatedBy',
|
||||
{
|
||||
defaultMessage: 'Created by',
|
||||
}
|
||||
);
|
||||
|
||||
export const CASES_TABLE_COLUMN_STATUS = i18n.translate(
|
||||
'xpack.securitySolution.detectionResponse.caseColumnStatus',
|
||||
{
|
||||
defaultMessage: 'Status',
|
||||
}
|
||||
);
|
||||
|
||||
export const ERROR_MESSAGE_CASES = i18n.translate(
|
||||
'xpack.securitySolution.detectionResponse.errorMessage',
|
||||
{
|
||||
defaultMessage: 'Error fetching case data',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -190,7 +190,7 @@ function parseUsersData(rawAggregation: AlertCountersBySeverityAggregation): Use
|
|||
return [
|
||||
...accumalatedAlertsByUser,
|
||||
{
|
||||
userName: currentUser.key,
|
||||
userName: currentUser.key || '—',
|
||||
totalAlerts: currentUser.doc_count,
|
||||
low: currentUser.low.doc_count,
|
||||
medium: currentUser.medium.doc_count,
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
EuiBasicTable,
|
||||
|
@ -40,6 +41,11 @@ type GetTableColumns = (params: {
|
|||
|
||||
const DETECTION_RESPONSE_USER_SEVERITY_QUERY_ID = 'vulnerableUsersBySeverityQuery';
|
||||
|
||||
// To Do remove this styled component once togglequery is updated: #131405
|
||||
const StyledEuiPanel = styled(EuiPanel)`
|
||||
height: fit-content;
|
||||
`;
|
||||
|
||||
export const UserAlertsTable = React.memo(({ signalIndexName }: UserAlertsTableProps) => {
|
||||
const { getAppUrl, navigateTo } = useNavigation();
|
||||
const { toggleStatus, setToggleStatus } = useQueryToggle(
|
||||
|
@ -62,7 +68,7 @@ export const UserAlertsTable = React.memo(({ signalIndexName }: UserAlertsTableP
|
|||
|
||||
return (
|
||||
<HoverVisibilityContainer show={true} targetClassNames={[INPECT_BUTTON_CLASS]}>
|
||||
<EuiPanel hasBorder data-test-subj="severityUserAlertsPanel">
|
||||
<StyledEuiPanel hasBorder data-test-subj="severityUserAlertsPanel">
|
||||
<HeaderSection
|
||||
id={DETECTION_RESPONSE_USER_SEVERITY_QUERY_ID}
|
||||
title={i18n.USER_ALERTS_SECTION_TITLE}
|
||||
|
@ -89,7 +95,7 @@ export const UserAlertsTable = React.memo(({ signalIndexName }: UserAlertsTableP
|
|||
</EuiButton>
|
||||
</>
|
||||
)}
|
||||
</EuiPanel>
|
||||
</StyledEuiPanel>
|
||||
</HoverVisibilityContainer>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -11,12 +11,24 @@ import { render } from '@testing-library/react';
|
|||
import { DetectionResponse } from './detection_response';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
|
||||
jest.mock('../components/detection_response/alerts_by_status', () => ({
|
||||
AlertsByStatus: () => <div data-test-subj="mock_AlertsByStatus" />,
|
||||
}));
|
||||
|
||||
jest.mock('../components/detection_response/cases_table', () => ({
|
||||
CasesTable: () => <div data-test-subj="mock_CasesTable" />,
|
||||
}));
|
||||
|
||||
jest.mock('../components/detection_response/host_alerts_table', () => ({
|
||||
HostAlertsTable: () => <div data-test-subj="mock_HostAlertsTable" />,
|
||||
}));
|
||||
|
||||
jest.mock('../components/detection_response/rule_alerts_table', () => ({
|
||||
RuleAlertsTable: () => <div data-test-subj="mock_RuleAlertsTable" />,
|
||||
}));
|
||||
|
||||
jest.mock('../components/detection_response/alerts_by_status', () => ({
|
||||
AlertsByStatus: () => <div data-test-subj="mock_AlertsByStatus" />,
|
||||
jest.mock('../components/detection_response/user_alerts_table', () => ({
|
||||
UserAlertsTable: () => <div data-test-subj="mock_UserAlertsTable" />,
|
||||
}));
|
||||
|
||||
jest.mock('../components/detection_response/cases_by_status', () => ({
|
||||
|
@ -37,14 +49,22 @@ jest.mock('../../common/containers/sourcerer', () => ({
|
|||
useSourcererDataView: () => mockUseSourcererDataView(),
|
||||
}));
|
||||
|
||||
const defaultUseUserInfoReturn = {
|
||||
signalIndexName: '',
|
||||
canUserREAD: true,
|
||||
const defaultUseAlertsPrivilegesReturn = {
|
||||
hasKibanaREAD: true,
|
||||
hasIndexRead: true,
|
||||
};
|
||||
const mockUseUserInfo = jest.fn(() => defaultUseUserInfoReturn);
|
||||
jest.mock('../../detections/components/user_info', () => ({
|
||||
useUserInfo: () => mockUseUserInfo(),
|
||||
|
||||
const defaultUseSignalIndexReturn = {
|
||||
signalIndexName: '',
|
||||
};
|
||||
|
||||
const mockUseSignalIndex = jest.fn(() => defaultUseSignalIndexReturn);
|
||||
jest.mock('../../detections/containers/detection_engine/alerts/use_signal_index', () => ({
|
||||
useSignalIndex: () => mockUseSignalIndex(),
|
||||
}));
|
||||
const mockUseAlertsPrivileges = jest.fn(() => defaultUseAlertsPrivilegesReturn);
|
||||
jest.mock('../../detections/containers/detection_engine/alerts/use_alerts_privileges', () => ({
|
||||
useAlertsPrivileges: () => mockUseAlertsPrivileges(),
|
||||
}));
|
||||
|
||||
const defaultUseCasesPermissionsReturn = { read: true };
|
||||
|
@ -61,7 +81,8 @@ describe('DetectionResponse', () => {
|
|||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUseSourcererDataView.mockReturnValue(defaultUseSourcererReturn);
|
||||
mockUseUserInfo.mockReturnValue(defaultUseUserInfoReturn);
|
||||
mockUseAlertsPrivileges.mockReturnValue(defaultUseAlertsPrivilegesReturn);
|
||||
mockUseSignalIndex.mockReturnValue(defaultUseSignalIndexReturn);
|
||||
mockUseCasesPermissions.mockReturnValue(defaultUseCasesPermissionsReturn);
|
||||
});
|
||||
|
||||
|
@ -121,9 +142,9 @@ describe('DetectionResponse', () => {
|
|||
});
|
||||
|
||||
it('should not render alerts data sections if user has not index read permission', () => {
|
||||
mockUseUserInfo.mockReturnValue({
|
||||
...defaultUseUserInfoReturn,
|
||||
mockUseAlertsPrivileges.mockReturnValue({
|
||||
hasIndexRead: false,
|
||||
hasKibanaREAD: true,
|
||||
});
|
||||
|
||||
const result = render(
|
||||
|
@ -135,18 +156,19 @@ describe('DetectionResponse', () => {
|
|||
);
|
||||
|
||||
expect(result.queryByTestId('detectionResponsePage')).toBeInTheDocument();
|
||||
// TODO: assert other alert sections are not in the document
|
||||
expect(result.queryByTestId('mock_RuleAlertsTable')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('mock_AlertsByStatus')).not.toBeInTheDocument();
|
||||
|
||||
// TODO: assert cases sections are in the document
|
||||
expect(result.queryByTestId('mock_CasesTable')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('mock_CasesByStatus')).toBeInTheDocument();
|
||||
|
||||
expect(result.queryByTestId('mock_RuleAlertsTable')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('mock_HostAlertsTable')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('mock_UserAlertsTable')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('mock_AlertsByStatus')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render alerts data sections if user has not kibana read permission', () => {
|
||||
mockUseUserInfo.mockReturnValue({
|
||||
...defaultUseUserInfoReturn,
|
||||
canUserREAD: false,
|
||||
mockUseAlertsPrivileges.mockReturnValue({
|
||||
hasIndexRead: true,
|
||||
hasKibanaREAD: false,
|
||||
});
|
||||
|
||||
const result = render(
|
||||
|
@ -158,13 +180,13 @@ describe('DetectionResponse', () => {
|
|||
);
|
||||
|
||||
expect(result.queryByTestId('detectionResponsePage')).toBeInTheDocument();
|
||||
|
||||
// TODO: assert all alert sections are not in the document
|
||||
expect(result.queryByTestId('mock_RuleAlertsTable')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('mock_AlertsByStatus')).not.toBeInTheDocument();
|
||||
|
||||
// TODO: assert all cases sections are in the document
|
||||
expect(result.queryByTestId('mock_CasesTable')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('mock_CasesByStatus')).toBeInTheDocument();
|
||||
|
||||
expect(result.queryByTestId('mock_RuleAlertsTable')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('mock_HostAlertsTable')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('mock_UserAlertsTable')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('mock_AlertsByStatus')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render cases data sections if user has not cases read permission', () => {
|
||||
|
@ -178,18 +200,20 @@ describe('DetectionResponse', () => {
|
|||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(result.queryByTestId('detectionResponsePage')).toBeInTheDocument();
|
||||
// TODO: assert all alert sections are in the document
|
||||
expect(result.queryByTestId('mock_RuleAlertsTable')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('mock_AlertsByStatus')).toBeInTheDocument();
|
||||
// TODO: assert all cases sections are not in the document
|
||||
expect(result.queryByTestId('mock_CasesTable')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('mock_CasesByStatus')).not.toBeInTheDocument();
|
||||
|
||||
expect(result.queryByTestId('detectionResponsePage')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('mock_RuleAlertsTable')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('mock_HostAlertsTable')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('mock_UserAlertsTable')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('mock_AlertsByStatus')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render page permissions message if user has any read permission', () => {
|
||||
mockUseCasesPermissions.mockReturnValue({ read: false });
|
||||
mockUseUserInfo.mockReturnValue({
|
||||
...defaultUseUserInfoReturn,
|
||||
mockUseAlertsPrivileges.mockReturnValue({
|
||||
hasKibanaREAD: true,
|
||||
hasIndexRead: false,
|
||||
});
|
||||
|
||||
|
|
|
@ -11,17 +11,20 @@ import { SecuritySolutionPageWrapper } from '../../common/components/page_wrappe
|
|||
import { SpyRoute } from '../../common/utils/route/spy_routes';
|
||||
import { SecurityPageName } from '../../app/types';
|
||||
import { useSourcererDataView } from '../../common/containers/sourcerer';
|
||||
import { useUserInfo } from '../../detections/components/user_info';
|
||||
import { HeaderPage } from '../../common/components/header_page';
|
||||
import { useKibana, useGetUserCasesPermissions } from '../../common/lib/kibana';
|
||||
import { HostAlertsTable, UserAlertsTable } from '../components/detection_response';
|
||||
|
||||
import { LandingPageComponent } from '../../common/components/landing_page';
|
||||
import { RuleAlertsTable } from '../components/detection_response/rule_alerts_table';
|
||||
import * as i18n from './translations';
|
||||
import { EmptyPage } from '../../common/components/empty_page';
|
||||
import { CasesByStatus } from '../components/detection_response/cases_by_status';
|
||||
import { LandingPageComponent } from '../../common/components/landing_page';
|
||||
import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index';
|
||||
import { useAlertsPrivileges } from '../../detections/containers/detection_engine/alerts/use_alerts_privileges';
|
||||
import { AlertsByStatus } from '../components/detection_response/alerts_by_status';
|
||||
import { HostAlertsTable } from '../components/detection_response/host_alerts_table';
|
||||
import { RuleAlertsTable } from '../components/detection_response/rule_alerts_table';
|
||||
import { UserAlertsTable } from '../components/detection_response/user_alerts_table';
|
||||
import * as i18n from './translations';
|
||||
import { CasesTable } from '../components/detection_response/cases_table';
|
||||
import { CasesByStatus } from '../components/detection_response/cases_by_status';
|
||||
|
||||
const NoPrivilegePage: React.FC = () => {
|
||||
const { docLinks } = useKibana().services;
|
||||
|
@ -48,9 +51,10 @@ const NoPrivilegePage: React.FC = () => {
|
|||
|
||||
const DetectionResponseComponent = () => {
|
||||
const { indicesExist, indexPattern, loading: isSourcererLoading } = useSourcererDataView();
|
||||
const { signalIndexName, canUserREAD, hasIndexRead } = useUserInfo();
|
||||
const { signalIndexName } = useSignalIndex();
|
||||
const { hasKibanaREAD, hasIndexRead } = useAlertsPrivileges();
|
||||
const canReadCases = useGetUserCasesPermissions()?.read;
|
||||
const canReadAlerts = canUserREAD && hasIndexRead;
|
||||
const canReadAlerts = hasKibanaREAD && hasIndexRead;
|
||||
|
||||
if (!canReadAlerts && !canReadCases) {
|
||||
return <NoPrivilegePage />;
|
||||
|
@ -90,7 +94,11 @@ const DetectionResponseComponent = () => {
|
|||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
{canReadCases && <EuiFlexItem>{'[cases table]'}</EuiFlexItem>}
|
||||
{canReadCases && (
|
||||
<EuiFlexItem>
|
||||
<CasesTable />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
{canReadAlerts && (
|
||||
<EuiFlexItem>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue