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

[SIEM] Export timeline #58368

Merged
merged 48 commits into from
Mar 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
432a2d9
update layout
angorayc Feb 24, 2020
11c2f65
add utility bars
angorayc Feb 24, 2020
f32c095
add icon
angorayc Feb 24, 2020
2ee1a0e
adding a route for exporting timeline
angorayc Mar 4, 2020
5af33cb
Merge remote-tracking branch 'upstream/master' into timeline
angorayc Mar 4, 2020
10fb49e
organizing data
angorayc Mar 6, 2020
fefeaa5
fix types
angorayc Mar 6, 2020
1bf860f
fix incorrect props for timeline table
angorayc Mar 9, 2020
ea78736
add export timeline to tables action
angorayc Mar 10, 2020
3645310
fix types
angorayc Mar 10, 2020
d6eec2b
add client side unit test
angorayc Mar 11, 2020
a94d6f6
add server-side unit test
angorayc Mar 11, 2020
d3460fa
fix title for delete timelines
angorayc Mar 11, 2020
9562a9b
fix unit tests
angorayc Mar 11, 2020
697ce94
update snapshot
angorayc Mar 11, 2020
2d41acc
Merge remote-tracking branch 'upstream/master' into timeline
angorayc Mar 12, 2020
8ef7bcc
Merge remote-tracking branch 'upstream/master' into timeline
angorayc Mar 12, 2020
1cde970
fix dependency
angorayc Mar 12, 2020
b7fd481
add table ref
angorayc Mar 12, 2020
5628767
remove custom link
angorayc Mar 14, 2020
c00f9e7
remove custom links
angorayc Mar 14, 2020
981b243
Update x-pack/legacy/plugins/siem/common/constants.ts
angorayc Mar 14, 2020
c806bee
Merge branch 'timeline' of github.com:angorayc/kibana into timeline
angorayc Mar 14, 2020
1d3713b
remove type ExportTimelineIds
angorayc Mar 14, 2020
1846c7c
reduce props
angorayc Mar 15, 2020
e6bf4ee
Get notes and pinned events by timeline id
angorayc Mar 16, 2020
5aefe85
combine notes and pinned events data
angorayc Mar 16, 2020
74b88fb
fix unit test
angorayc Mar 16, 2020
a8cbeec
fix type error
angorayc Mar 16, 2020
f7cb9c5
Merge remote-tracking branch 'upstream/master' into timeline
angorayc Mar 16, 2020
df2f45a
Merge branch 'master' into timeline
angorayc Mar 16, 2020
8f9a167
fix type error
angorayc Mar 16, 2020
039e2d7
Merge branch 'timeline' of github.com:angorayc/kibana into timeline
angorayc Mar 16, 2020
9029f8e
fix unit tests
angorayc Mar 16, 2020
b83073d
fix for review
angorayc Mar 16, 2020
42ac248
clean up generic downloader
angorayc Mar 17, 2020
563ca72
Merge remote-tracking branch 'upstream/master' into timeline
angorayc Mar 17, 2020
8c1f571
review with angela
XavierM Mar 18, 2020
0d4307c
review utils
XavierM Mar 18, 2020
b914860
fix for code review
angorayc Mar 18, 2020
7a9ebbb
fix for review
angorayc Mar 18, 2020
9e80e46
Merge remote-tracking branch 'upstream/master' into timeline
angorayc Mar 18, 2020
237d7f8
fix tests
angorayc Mar 18, 2020
b1465a8
review
angorayc Mar 19, 2020
6aaaca2
fix title of delete modal
angorayc Mar 19, 2020
9bc0a4e
remove an extra bracket
angorayc Mar 19, 2020
d7fd4e7
Merge branch 'master' into timeline
elasticmachine Mar 19, 2020
0b0906a
Merge branch 'master' into timeline
elasticmachine Mar 20, 2020
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
3 changes: 3 additions & 0 deletions x-pack/legacy/plugins/siem/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export const DETECTION_ENGINE_TAGS_URL = `${DETECTION_ENGINE_URL}/tags`;
export const DETECTION_ENGINE_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/_find_statuses`;
export const DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/prepackaged/_status`;

export const TIMELINE_URL = '/api/timeline';
export const TIMELINE_EXPORT_URL = `${TIMELINE_URL}/_export`;

/**
* Default signals index key for kibana.dev.yml
*/
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@

import { shallow } from 'enzyme';
import React from 'react';
import { RuleDownloaderComponent } from './index';
import { GenericDownloaderComponent } from './index';

describe('RuleDownloader', () => {
describe('GenericDownloader', () => {
test('renders correctly against snapshot', () => {
const wrapper = shallow(
<RuleDownloaderComponent filename={'export_rules.ndjson'} onExportComplete={jest.fn()} />
<GenericDownloaderComponent
filename={'export_rules.ndjson'}
onExportSuccess={jest.fn()}
exportSelectedData={jest.fn()}
/>
);
expect(wrapper).toMatchSnapshot();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,28 @@
import React, { useEffect, useRef } from 'react';
import styled from 'styled-components';
import { isFunction } from 'lodash/fp';
import { exportRules } from '../../../../../containers/detection_engine/rules';
import { useStateToaster, errorToToaster } from '../../../../../components/toasters';
import * as i18n from './translations';

import { ExportDocumentsProps } from '../../containers/detection_engine/rules';
import { useStateToaster, errorToToaster } from '../toasters';

const InvisibleAnchor = styled.a`
display: none;
`;

export interface RuleDownloaderProps {
export type ExportSelectedData = ({
excludeExportDetails,
filename,
ids,
signal,
}: ExportDocumentsProps) => Promise<Blob>;

export interface GenericDownloaderProps {
filename: string;
ruleIds?: string[];
onExportComplete: (exportCount: number) => void;
ids?: string[];
exportSelectedData: ExportSelectedData;
onExportSuccess?: (exportCount: number) => void;
onExportFailure?: () => void;
}

/**
Expand All @@ -28,23 +38,26 @@ export interface RuleDownloaderProps {
* @param payload Rule[]
*
*/
export const RuleDownloaderComponent = ({

export const GenericDownloaderComponent = ({
exportSelectedData,
filename,
ruleIds,
onExportComplete,
}: RuleDownloaderProps) => {
ids,
onExportSuccess,
onExportFailure,
}: GenericDownloaderProps) => {
const anchorRef = useRef<HTMLAnchorElement>(null);
const [, dispatchToaster] = useStateToaster();

useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();

async function exportData() {
if (anchorRef && anchorRef.current && ruleIds != null && ruleIds.length > 0) {
const exportData = async () => {
if (anchorRef && anchorRef.current && ids != null && ids.length > 0) {
try {
const exportResponse = await exportRules({
ruleIds,
const exportResponse = await exportSelectedData({
ids,
signal: abortCtrl.signal,
});

Expand All @@ -61,29 +74,34 @@ export const RuleDownloaderComponent = ({
window.URL.revokeObjectURL(objectURL);
}

onExportComplete(ruleIds.length);
if (onExportSuccess != null) {
onExportSuccess(ids.length);
}
}
} catch (error) {
if (isSubscribed) {
if (onExportFailure != null) {
onExportFailure();
}
errorToToaster({ title: i18n.EXPORT_FAILURE, error, dispatchToaster });
}
}
}
}
};

exportData();

return () => {
isSubscribed = false;
abortCtrl.abort();
};
}, [ruleIds]);
}, [ids]);

return <InvisibleAnchor ref={anchorRef} />;
};

RuleDownloaderComponent.displayName = 'RuleDownloaderComponent';
GenericDownloaderComponent.displayName = 'GenericDownloaderComponent';

export const RuleDownloader = React.memo(RuleDownloaderComponent);
export const GenericDownloader = React.memo(GenericDownloaderComponent);

RuleDownloader.displayName = 'RuleDownloader';
GenericDownloader.displayName = 'GenericDownloader';
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import { DeleteTimelineModal } from './delete_timeline_modal';
import * as i18n from '../translations';

describe('DeleteTimelineModal', () => {
test('it renders the expected title when a title is specified', () => {
test('it renders the expected title when a timeline is selected', () => {
const wrapper = mountWithIntl(
<DeleteTimelineModal
title="Privilege Escalation"
title={'Privilege Escalation'}
onDelete={jest.fn()}
closeModal={jest.fn()}
/>
Expand All @@ -29,10 +29,10 @@ describe('DeleteTimelineModal', () => {
).toEqual('Delete "Privilege Escalation"?');
});

test('it trims leading and trailing whitespace around the title', () => {
test('it trims leading whitespace around the title', () => {
const wrapper = mountWithIntl(
<DeleteTimelineModal
title=" Leading and trailing whitespace "
title={' Leading and trailing whitespace '}
onDelete={jest.fn()}
closeModal={jest.fn()}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

import { EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import React, { useCallback } from 'react';
import { isEmpty } from 'lodash/fp';

import * as i18n from '../translations';

Expand All @@ -21,27 +22,34 @@ export const DELETE_TIMELINE_MODAL_WIDTH = 600; // px
/**
* Renders a modal that confirms deletion of a timeline
*/
export const DeleteTimelineModal = React.memo<Props>(({ title, closeModal, onDelete }) => (
<EuiConfirmModal
title={
export const DeleteTimelineModal = React.memo<Props>(({ title, closeModal, onDelete }) => {
const getTitle = useCallback(() => {
const trimmedTitle = title != null ? title.trim() : '';
const titleResult = !isEmpty(trimmedTitle) ? trimmedTitle : i18n.UNTITLED_TIMELINE;
return (
<FormattedMessage
id="xpack.siem.open.timeline.deleteTimelineModalTitle"
data-test-subj="title"
defaultMessage='Delete "{title}"?'
data-test-subj="title"
values={{
title: title != null && title.trim().length > 0 ? title.trim() : i18n.UNTITLED_TIMELINE,
title: titleResult,
}}
/>
}
onCancel={closeModal}
onConfirm={onDelete}
cancelButtonText={i18n.CANCEL}
confirmButtonText={i18n.DELETE}
buttonColor="danger"
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
>
<div data-test-subj="warning">{i18n.DELETE_WARNING}</div>
</EuiConfirmModal>
));
);
}, [title]);
return (
<EuiConfirmModal
buttonColor="danger"
cancelButtonText={i18n.CANCEL}
confirmButtonText={i18n.DELETE}
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
onCancel={closeModal}
onConfirm={onDelete}
title={getTitle()}
>
<div data-test-subj="warning">{i18n.DELETE_WARNING}</div>
</EuiConfirmModal>
);
});

DeleteTimelineModal.displayName = 'DeleteTimelineModal';
Original file line number Diff line number Diff line change
Expand Up @@ -4,114 +4,54 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { EuiButtonIconProps } from '@elastic/eui';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import React from 'react';

import { DeleteTimelineModalButton } from '.';
import { DeleteTimelineModalOverlay } from '.';

describe('DeleteTimelineModal', () => {
const savedObjectId = 'abcd';
const defaultProps = {
closeModal: jest.fn(),
deleteTimelines: jest.fn(),
isModalOpen: true,
savedObjectIds: [savedObjectId],
title: 'Privilege Escalation',
};

describe('showModalState', () => {
test('it disables the delete icon if deleteTimelines is not provided', () => {
const wrapper = mountWithIntl(
<DeleteTimelineModalButton savedObjectId={savedObjectId} title="Privilege Escalation" />
);
test('it does NOT render the modal when isModalOpen is false', () => {
const testProps = {
...defaultProps,
isModalOpen: false,
};
const wrapper = mountWithIntl(<DeleteTimelineModalOverlay {...testProps} />);

const props = wrapper
.find('[data-test-subj="delete-timeline"]')
.first()
.props() as EuiButtonIconProps;

expect(props.isDisabled).toBe(true);
});

test('it disables the delete icon if savedObjectId is null', () => {
const wrapper = mountWithIntl(
<DeleteTimelineModalButton
deleteTimelines={jest.fn()}
savedObjectId={null}
title="Privilege Escalation"
/>
);

const props = wrapper
.find('[data-test-subj="delete-timeline"]')
.first()
.props() as EuiButtonIconProps;

expect(props.isDisabled).toBe(true);
});

test('it disables the delete icon if savedObjectId is an empty string', () => {
const wrapper = mountWithIntl(
<DeleteTimelineModalButton
deleteTimelines={jest.fn()}
savedObjectId=""
title="Privilege Escalation"
/>
);

const props = wrapper
.find('[data-test-subj="delete-timeline"]')
.first()
.props() as EuiButtonIconProps;

expect(props.isDisabled).toBe(true);
});

test('it enables the delete icon if savedObjectId is NOT an empty string', () => {
const wrapper = mountWithIntl(
<DeleteTimelineModalButton
deleteTimelines={jest.fn()}
savedObjectId="not an empty string"
title="Privilege Escalation"
/>
);

const props = wrapper
.find('[data-test-subj="delete-timeline"]')
.first()
.props() as EuiButtonIconProps;

expect(props.isDisabled).toBe(false);
expect(
wrapper
.find('[data-test-subj="delete-timeline-modal"]')
.first()
.exists()
).toBe(false);
});

test('it does NOT render the modal when showModal is false', () => {
const wrapper = mountWithIntl(
<DeleteTimelineModalButton
deleteTimelines={jest.fn()}
savedObjectId={savedObjectId}
title="Privilege Escalation"
/>
);
test('it renders the modal when isModalOpen is true', () => {
const wrapper = mountWithIntl(<DeleteTimelineModalOverlay {...defaultProps} />);

expect(
wrapper
.find('[data-test-subj="delete-timeline-modal"]')
.first()
.exists()
).toBe(false);
).toBe(true);
});

test('it renders the modal when showModal is clicked', () => {
const wrapper = mountWithIntl(
<DeleteTimelineModalButton
deleteTimelines={jest.fn()}
savedObjectId={savedObjectId}
title="Privilege Escalation"
/>
);

wrapper
.find('[data-test-subj="delete-timeline"]')
.first()
.simulate('click');
test('it hides popover when isModalOpen is true', () => {
const wrapper = mountWithIntl(<DeleteTimelineModalOverlay {...defaultProps} />);

expect(
wrapper
.find('[data-test-subj="delete-timeline-modal"]')
.find('[data-test-subj="remove-popover"]')
.first()
.exists()
).toBe(true);
Expand Down
Loading