Skip to content

Commit

Permalink
[SECURITY SOLUTION] two bugs fix for threat hunting (elastic#76060) (e…
Browse files Browse the repository at this point in the history
…lastic#76299)

* fix read only issue with timeline

* fix no feeds url for kibana setting + remove the no-index-laert index from the timeline query so you do not have to add permissions to it

* Add test + add logic to not show advance settings if user does not have access

* remove no alert indices from the timeline query

* review I

* no needs of that

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
XavierM and elasticmachine authored Sep 2, 2020
1 parent 36b398c commit 1c32526
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,37 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { EuiLink, EuiText } from '@elastic/eui';
import React from 'react';
import { EuiText } from '@elastic/eui';
import React, { useCallback } from 'react';

import * as i18n from '../translations';
import { useBasePath } from '../../../lib/kibana';
import { useKibana } from '../../../lib/kibana';
import { LinkAnchor } from '../../links';

export const NoNews = React.memo(() => {
const basePath = useBasePath();
const { getUrlForApp, navigateToApp, capabilities } = useKibana().services.application;
const canSeeAdvancedSettings = capabilities.management.kibana.settings ?? false;
const goToKibanaSettings = useCallback(
() => navigateToApp('management', { path: '/kibana/settings' }),
[navigateToApp]
);

return (
<>
<EuiText color="subdued" size="s">
{i18n.NO_NEWS_MESSAGE}{' '}
<EuiLink href={`${basePath}/app/management/kibana/settings`}>
{i18n.ADVANCED_SETTINGS_LINK_TITLE}
</EuiLink>
{'.'}
</EuiText>
</>
<EuiText color="subdued" size="s">
{canSeeAdvancedSettings ? i18n.NO_NEWS_MESSAGE_ADMIN : i18n.NO_NEWS_MESSAGE}
{canSeeAdvancedSettings && (
<>
{' '}
<LinkAnchor
onClick={goToKibanaSettings}
href={`${getUrlForApp('management', { path: '/kibana/settings' })}`}
>
{i18n.ADVANCED_SETTINGS_LINK_TITLE}
</LinkAnchor>
{'.'}
</>
)}
</EuiText>
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@
import { i18n } from '@kbn/i18n';

export const NO_NEWS_MESSAGE = i18n.translate('xpack.securitySolution.newsFeed.noNewsMessage', {
defaultMessage:
'Your current news feed URL returned no recent news. You may update the URL or disable security news via',
defaultMessage: 'Your current news feed URL returned no recent news.',
});

export const NO_NEWS_MESSAGE_ADMIN = i18n.translate(
'xpack.securitySolution.newsFeed.noNewsMessageForAdmin',
{
defaultMessage:
'Your current news feed URL returned no recent news. You may update the URL or disable security news via',
}
);

export const ADVANCED_SETTINGS_LINK_TITLE = i18n.translate(
'xpack.securitySolution.newsFeed.advancedSettingsLinkTitle',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,17 +272,14 @@ export interface NewTimelineProps {

export const NewTimeline = React.memo<NewTimelineProps>(
({ closeGearMenu, outline = false, timelineId, title = i18n.NEW_TIMELINE }) => {
const uiCapabilities = useKibana().services.application.capabilities;
const capabilitiesCanUserCRUD: boolean = !!uiCapabilities.siem.crud;

const { getButton } = useCreateTimelineButton({
timelineId,
timelineType: TimelineType.default,
closeGearMenu,
});
const button = getButton({ outline, title });

return capabilitiesCanUserCRUD ? button : null;
return button;
}
);
NewTimeline.displayName = 'NewTimeline';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
TimelineType,
} from '../../../../../common/types/timeline';
import { InspectButton, InspectButtonContainer } from '../../../../common/components/inspect';
import { useKibana } from '../../../../common/lib/kibana';
import { Note } from '../../../../common/lib/note';

import { AssociateNote } from '../../notes/helpers';
Expand Down Expand Up @@ -121,8 +120,6 @@ const PropertiesRightComponent: React.FC<PropertiesRightComponentProps> = ({
updateNote,
usersViewing,
}) => {
const uiCapabilities = useKibana().services.application.capabilities;
const capabilitiesCanUserCRUD: boolean = !!uiCapabilities.siem.crud;
return (
<PropertiesRightStyle alignItems="flexStart" data-test-subj="properties-right" gutterSize="s">
<EuiFlexItem grow={false}>
Expand All @@ -143,15 +140,13 @@ const PropertiesRightComponent: React.FC<PropertiesRightComponentProps> = ({
repositionOnScroll
>
<EuiFlexGroup alignItems="flexStart" direction="column" gutterSize="none">
{capabilitiesCanUserCRUD && (
<EuiFlexItem grow={false}>
<NewTimeline
timelineId={timelineId}
title={i18n.NEW_TIMELINE}
closeGearMenu={onClosePopover}
/>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<NewTimeline
timelineId={timelineId}
title={i18n.NEW_TIMELINE}
closeGearMenu={onClosePopover}
/>
</EuiFlexItem>

<EuiFlexItem grow={false}>
<NewTemplateTimeline
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,109 @@ describe('persistTimeline', () => {
});
});

describe('create draft timeline in read-only permission', () => {
const timelineId = null;
const initialDraftTimeline = {
columns: [
{
columnHeaderType: 'not-filtered',
id: '@timestamp',
},
{
columnHeaderType: 'not-filtered',
id: 'message',
},
{
columnHeaderType: 'not-filtered',
id: 'event.category',
},
{
columnHeaderType: 'not-filtered',
id: 'event.action',
},
{
columnHeaderType: 'not-filtered',
id: 'host.name',
},
{
columnHeaderType: 'not-filtered',
id: 'source.ip',
},
{
columnHeaderType: 'not-filtered',
id: 'destination.ip',
},
{
columnHeaderType: 'not-filtered',
id: 'user.name',
},
],
dataProviders: [],
description: 'x',
eventType: 'all',
filters: [],
kqlMode: 'filter',
kqlQuery: {
filterQuery: null,
},
title: '',
timelineType: TimelineType.default,
templateTimelineVersion: null,
templateTimelineId: null,
dateRange: {
start: 1590998565409,
end: 1591084965409,
},
savedQueryId: null,
sort: {
columnId: '@timestamp',
sortDirection: 'desc',
},
status: TimelineStatus.draft,
};

const version = null;
const fetchMock = jest.fn();
const postMock = jest.fn();
const patchMock = jest.fn();

beforeAll(() => {
jest.resetAllMocks();
jest.resetModules();

(KibanaServices.get as jest.Mock).mockReturnValue({
http: {
fetch: fetchMock.mockRejectedValue({
body: { status_code: 403, message: 'you do not have the permission' },
}),
post: postMock.mockRejectedValue({
body: { status_code: 403, message: 'you do not have the permission' },
}),
patch: patchMock.mockRejectedValue({
body: { status_code: 403, message: 'you do not have the permission' },
}),
},
});
});

test('it should return your request timeline with code and message', async () => {
const persist = await api.persistTimeline({
timelineId,
timeline: initialDraftTimeline,
version,
});
expect(persist).toEqual({
data: {
persistTimeline: {
code: 403,
message: 'you do not have the permission',
timeline: { ...initialDraftTimeline, savedObjectId: '', version: '' },
},
},
});
});
});

describe('create active timeline (import)', () => {
const timelineId = null;
const importTimeline = {
Expand Down
88 changes: 54 additions & 34 deletions x-pack/plugins/security_solution/public/timelines/containers/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { pipe } from 'fp-ts/lib/pipeable';
import isEmpty from 'lodash/isEmpty';

import { throwErrors } from '../../../../case/common/api';
import {
Expand Down Expand Up @@ -99,44 +100,63 @@ export const persistTimeline = async ({
timeline,
version,
}: RequestPersistTimeline): Promise<TimelineResponse | TimelineErrorResponse> => {
if (timelineId == null && timeline.status === TimelineStatus.draft && timeline) {
const draftTimeline = await cleanDraftTimeline({
timelineType: timeline.timelineType!,
templateTimelineId: timeline.templateTimelineId ?? undefined,
templateTimelineVersion: timeline.templateTimelineVersion ?? undefined,
});

const templateTimelineInfo =
timeline.timelineType! === TimelineType.template
? {
templateTimelineId:
draftTimeline.data.persistTimeline.timeline.templateTimelineId ??
timeline.templateTimelineId,
templateTimelineVersion:
draftTimeline.data.persistTimeline.timeline.templateTimelineVersion ??
timeline.templateTimelineVersion,
}
: {};
try {
if (isEmpty(timelineId) && timeline.status === TimelineStatus.draft && timeline) {
const draftTimeline = await cleanDraftTimeline({
timelineType: timeline.timelineType!,
templateTimelineId: timeline.templateTimelineId ?? undefined,
templateTimelineVersion: timeline.templateTimelineVersion ?? undefined,
});

const templateTimelineInfo =
timeline.timelineType! === TimelineType.template
? {
templateTimelineId:
draftTimeline.data.persistTimeline.timeline.templateTimelineId ??
timeline.templateTimelineId,
templateTimelineVersion:
draftTimeline.data.persistTimeline.timeline.templateTimelineVersion ??
timeline.templateTimelineVersion,
}
: {};

return patchTimeline({
timelineId: draftTimeline.data.persistTimeline.timeline.savedObjectId,
timeline: {
...timeline,
...templateTimelineInfo,
},
version: draftTimeline.data.persistTimeline.timeline.version ?? '',
});
}

if (isEmpty(timelineId)) {
return postTimeline({ timeline });
}

return patchTimeline({
timelineId: draftTimeline.data.persistTimeline.timeline.savedObjectId,
timeline: {
...timeline,
...templateTimelineInfo,
},
version: draftTimeline.data.persistTimeline.timeline.version ?? '',
timelineId: timelineId ?? '-1',
timeline,
version: version ?? '',
});
} catch (err) {
if (err.status_code === 403 || err.body.status_code === 403) {
return Promise.resolve({
data: {
persistTimeline: {
code: 403,
message: err.message || err.body.message,
timeline: {
...timeline,
savedObjectId: '',
version: '',
},
},
},
});
}
return Promise.resolve(err);
}

if (timelineId == null) {
return postTimeline({ timeline });
}

return patchTimeline({
timelineId,
timeline,
version: version ?? '',
});
};

export const importTimelines = async ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,11 @@ export const TimelinesPageComponent: React.FC = () => {
</EuiFlexItem>
{tabName === TimelineType.default ? (
<EuiFlexItem>
{capabilitiesCanUserCRUD && (
<NewTimeline
timelineId="timeline-1"
outline={true}
data-test-subj="create-default-btn"
/>
)}
<NewTimeline
timelineId="timeline-1"
outline={true}
data-test-subj="create-default-btn"
/>
</EuiFlexItem>
) : (
<EuiFlexItem>
Expand Down

0 comments on commit 1c32526

Please sign in to comment.