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

[Secutiy Solution] Timeline kpis #89210

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
201c344
Stub kpi component
kqualters-elastic Jan 13, 2021
5b24e9c
search strategy scheleton timeline KPI
XavierM Jan 13, 2021
298addc
search strategy scheleton timeline KPI
XavierM Jan 13, 2021
d372ff2
Merge branch 'siem-timeline-kpis' of github.com:kqualters-elastic/kib…
XavierM Jan 13, 2021
f69aca5
Add timeline kpis component and search strategy container
kqualters-elastic Jan 25, 2021
66aed05
Merge branch 'master' into siem-timeline-kpis
kqualters-elastic Jan 25, 2021
5a82ae6
Use getEmptyValue in timeline kpis
kqualters-elastic Jan 25, 2021
88bbf7b
Prevent request from being made for blank timeline properly
kqualters-elastic Jan 25, 2021
d4173ee
Add kpi search strategy api integration test
kqualters-elastic Jan 26, 2021
bd1e9cf
Add jest tests for timeline kpis
kqualters-elastic Jan 26, 2021
f3e34fd
Clear mocks in afterAll
kqualters-elastic Jan 26, 2021
e4dd6a2
Decouple some tests from EUI structure
kqualters-elastic Jan 27, 2021
c884e25
Merge remote-tracking branch 'upstream/master' into siem-timeline-kpis
kqualters-elastic Jan 27, 2021
5f53d96
Combine some selector calls, change types to be more appropriate
kqualters-elastic Jan 29, 2021
48e8842
Simplify hook logic
kqualters-elastic Jan 29, 2021
09f8c8e
Merge branch 'master' into siem-timeline-kpis
kqualters-elastic Jan 29, 2021
ded4eda
Merge branch 'master' into siem-timeline-kpis
kibanamachine Feb 1, 2021
6de598e
Set loading and response on blank timeline
kqualters-elastic Feb 2, 2021
bf60d6c
Merge remote-tracking branch 'upstream/master' into siem-timeline-kpis
kqualters-elastic Feb 2, 2021
b21625b
Only render kpi component when query is active tab
kqualters-elastic Feb 2, 2021
bff5c8e
Use TimelineTabs enum for query tab string
kqualters-elastic Feb 2, 2021
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 @@ -11,5 +11,6 @@ export * from './last_event_time';
export enum TimelineEventsQueries {
all = 'eventsAll',
details = 'eventsDetails',
kpi = 'eventsKpi',
lastEventTime = 'eventsLastEventTime',
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ export interface TimelineEventsLastEventTimeStrategyResponse extends IEsSearchRe
inspect?: Maybe<Inspect>;
}

export interface TimelineKpiStrategyResponse extends IEsSearchResponse {
destinationIpCount: number;
inspect?: Maybe<Inspect>;
hostCount: number;
processCount: number;
sourceIpCount: number;
userCount: number;
}

export interface TimelineEventsLastEventTimeRequestOptions
extends Omit<TimelineRequestBasicOptions, 'filterQuery' | 'timerange'> {
indexKey: LastEventIndexKey;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
TimelineEventsDetailsStrategyResponse,
TimelineEventsLastEventTimeRequestOptions,
TimelineEventsLastEventTimeStrategyResponse,
TimelineKpiStrategyResponse,
} from './events';
import { DocValueFields, PaginationInputPaginated, TimerangeInput, SortField } from '../common';

Expand Down Expand Up @@ -44,6 +45,8 @@ export type TimelineStrategyResponseType<
? TimelineEventsAllStrategyResponse
: T extends TimelineEventsQueries.details
? TimelineEventsDetailsStrategyResponse
: T extends TimelineEventsQueries.kpi
? TimelineKpiStrategyResponse
: T extends TimelineEventsQueries.lastEventTime
? TimelineEventsLastEventTimeStrategyResponse
: never;
Expand All @@ -54,6 +57,8 @@ export type TimelineStrategyRequestType<
? TimelineEventsAllRequestOptions
: T extends TimelineEventsQueries.details
? TimelineEventsDetailsRequestOptions
: T extends TimelineEventsQueries.kpi
? TimelineRequestBasicOptions
: T extends TimelineEventsQueries.lastEventTime
? TimelineEventsLastEventTimeRequestOptions
: never;
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';

import { useKibana } from '../../../../common/lib/kibana';
import { TestProviders, mockIndexNames, mockIndexPattern } from '../../../../common/mock';
import { useTimelineKpis } from '../../../containers/kpis';
import { FlyoutHeader } from '.';
import { useSourcererScope } from '../../../../common/containers/sourcerer';
import { mockBrowserFields, mockDocValueFields } from '../../../../common/containers/source/mock';
import { useMountAppended } from '../../../../common/utils/use_mount_appended';
import { getEmptyValue } from '../../../../common/components/empty_value';

const mockUseSourcererScope: jest.Mock = useSourcererScope as jest.Mock;
jest.mock('../../../../common/containers/sourcerer');

const mockUseTimelineKpis: jest.Mock = useTimelineKpis as jest.Mock;
jest.mock('../../../containers/kpis', () => ({
useTimelineKpis: jest.fn(),
}));
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
jest.mock('../../../../common/lib/kibana');

const mockUseTimelineKpiResponse = {
processCount: 1,
userCount: 1,
sourceIpCount: 1,
hostCount: 1,
destinationIpCount: 1,
};
const defaultMocks = {
browserFields: mockBrowserFields,
docValueFields: mockDocValueFields,
indexPattern: mockIndexPattern,
loading: false,
selectedPatterns: mockIndexNames,
};
describe('Timeline KPIs', () => {
const mount = useMountAppended();

beforeEach(() => {
// Mocking these services is required for the header component to render.
mockUseSourcererScope.mockImplementation(() => defaultMocks);
useKibanaMock().services.application.capabilities = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you leave a comment explaining what these are and why their needed?

navLinks: {},
management: {},
catalogue: {},
actions: { show: true, crud: true },
};
});

afterEach(() => {
jest.clearAllMocks();
});

describe('when the data is not loading and the response contains data', () => {
beforeEach(() => {
mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineKpiResponse]);
});
it('renders the component, labels and values succesfully', async () => {
const wrapper = mount(
<TestProviders>
<FlyoutHeader timelineId={'timeline-1'} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="siem-timeline-kpis"]').exists()).toEqual(true);
// label
expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
expect.stringContaining('Processes')
);
// value
expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
expect.stringContaining('1')
);
});
});

describe('when the data is loading', () => {
beforeEach(() => {
mockUseTimelineKpis.mockReturnValue([true, mockUseTimelineKpiResponse]);
});
it('renders a loading indicator for values', async () => {
const wrapper = mount(
<TestProviders>
<FlyoutHeader timelineId={'timeline-1'} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
expect.stringContaining('--')
);
});
});

describe('when the response is null and timeline is blank', () => {
beforeEach(() => {
mockUseTimelineKpis.mockReturnValue([false, null]);
});
it('renders labels and the default empty string', async () => {
const wrapper = mount(
<TestProviders>
<FlyoutHeader timelineId={'timeline-1'} />
</TestProviders>
);

expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
expect.stringContaining('Processes')
);
expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
expect.stringContaining(getEmptyValue())
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,42 @@ import {
} from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import { isEmpty, get, pick } from 'lodash/fp';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { FormattedRelative } from '@kbn/i18n/react';

import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { TimelineStatus, TimelineTabs, TimelineType } from '../../../../../common/types/timeline';
import {
TimelineStatus,
TimelineTabs,
TimelineType,
TimelineId,
} from '../../../../../common/types/timeline';
import { State } from '../../../../common/store';
import { timelineActions, timelineSelectors } from '../../../store/timeline';
import { timelineDefaults } from '../../../../timelines/store/timeline/defaults';
import { AddToFavoritesButton } from '../../timeline/properties/helpers';

import { TimerangeInput } from '../../../../../common/search_strategy';
import { AddToCaseButton } from '../add_to_case_button';
import { AddTimelineButton } from '../add_timeline_button';
import { SaveTimelineButton } from '../../timeline/header/save_timeline_button';
import { useKibana } from '../../../../common/lib/kibana';
import { InspectButton } from '../../../../common/components/inspect';
import { useTimelineKpis } from '../../../containers/kpis';
import { esQuery } from '../../../../../../../../src/plugins/data/public';
import { useSourcererScope } from '../../../../common/containers/sourcerer';
import { TimelineModel } from '../../../../timelines/store/timeline/model';
import {
startSelector,
endSelector,
} from '../../../../common/components/super_date_picker/selectors';
import { combineQueries, focusActiveTimelineButton } from '../../timeline/helpers';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import { ActiveTimelines } from './active_timelines';
import * as i18n from './translations';
import * as commonI18n from '../../timeline/properties/translations';
import { getTimelineStatusByIdSelector } from './selectors';
import { focusActiveTimelineButton } from '../../timeline/helpers';
import { TimelineKPIs } from './kpis';

// to hide side borders
const StyledPanel = styled(EuiPanel)`
Expand Down Expand Up @@ -227,38 +244,106 @@ const TimelineStatusInfoComponent: React.FC<FlyoutHeaderProps> = ({ timelineId }

const TimelineStatusInfo = React.memo(TimelineStatusInfoComponent);

const FlyoutHeaderComponent: React.FC<FlyoutHeaderProps> = ({ timelineId }) => (
<StyledTimelineHeader alignItems="center">
<EuiFlexItem>
<EuiFlexGroup data-test-subj="properties-left" direction="column" gutterSize="none">
<RowFlexItem>
<TimelineName timelineId={timelineId} />
<SaveTimelineButton timelineId={timelineId} initialFocus="title" />
</RowFlexItem>
<RowFlexItem>
<TimelineDescription timelineId={timelineId} />
<SaveTimelineButton timelineId={timelineId} initialFocus="description" />
</RowFlexItem>
<EuiFlexItem>
<TimelineStatusInfo timelineId={timelineId} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
const FlyoutHeaderComponent: React.FC<FlyoutHeaderProps> = ({ timelineId }) => {
const { selectedPatterns, indexPattern, docValueFields, browserFields } = useSourcererScope(
SourcererScopeName.timeline
);
const getStartSelector = useMemo(() => startSelector(), []);
const getEndSelector = useMemo(() => endSelector(), []);
const isActive = useMemo(() => timelineId === TimelineId.active, [timelineId]);
const timerange: TimerangeInput = useDeepEqualSelector((state) => {
if (isActive) {
return {
from: getStartSelector(state.inputs.timeline),
to: getEndSelector(state.inputs.timeline),
interval: '',
};
} else {
return {
from: getStartSelector(state.inputs.global),
to: getEndSelector(state.inputs.global),
interval: '',
};
}
});
const { uiSettings } = useKibana().services;
const esQueryConfig = useMemo(() => esQuery.getEsQueryConfig(uiSettings), [uiSettings]);
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const timeline: TimelineModel = useSelector(
(state: State) => getTimeline(state, timelineId) ?? timelineDefaults
);
const { dataProviders, filters, timelineType, kqlMode, activeTab } = timeline;
const getKqlQueryTimeline = useMemo(() => timelineSelectors.getKqlFilterQuerySelector(), []);
const kqlQueryTimeline = useSelector((state: State) => getKqlQueryTimeline(state, timelineId)!);

<EuiFlexItem grow={1}>{/* KPIs PLACEHOLDER */}</EuiFlexItem>
const kqlQueryExpression =
isEmpty(dataProviders) && isEmpty(kqlQueryTimeline) && timelineType === 'template'
? ' '
: kqlQueryTimeline;
const kqlQuery = useMemo(() => ({ query: kqlQueryExpression, language: 'kuery' }), [
kqlQueryExpression,
]);

<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<AddToFavoritesButton timelineId={timelineId} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AddToCaseButton timelineId={timelineId} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</StyledTimelineHeader>
);
const isBlankTimeline: boolean = useMemo(
() => isEmpty(dataProviders) && isEmpty(filters) && isEmpty(kqlQuery.query),
[dataProviders, filters, kqlQuery]
);
const combinedQueries = useMemo(
() =>
combineQueries({
config: esQueryConfig,
dataProviders,
indexPattern,
browserFields,
filters: filters ? filters : [],
kqlQuery,
kqlMode,
}),
[browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQuery]
);
const [loading, kpis] = useTimelineKpis({
defaultIndex: selectedPatterns,
docValueFields,
timerange,
isBlankTimeline,
filterQuery: combinedQueries?.filterQuery ?? '',
});

return (
<StyledTimelineHeader alignItems="center">
<EuiFlexItem>
<EuiFlexGroup data-test-subj="properties-left" direction="column" gutterSize="none">
<RowFlexItem>
<TimelineName timelineId={timelineId} />
<SaveTimelineButton timelineId={timelineId} initialFocus="title" />
</RowFlexItem>
<RowFlexItem>
<TimelineDescription timelineId={timelineId} />
<SaveTimelineButton timelineId={timelineId} initialFocus="description" />
</RowFlexItem>
<EuiFlexItem>
<TimelineStatusInfo timelineId={timelineId} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>

<EuiFlexItem grow={1}>
{activeTab === TimelineTabs.query ? <TimelineKPIs kpis={kpis} isLoading={loading} /> : null}
</EuiFlexItem>

<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<AddToFavoritesButton timelineId={timelineId} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AddToCaseButton timelineId={timelineId} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</StyledTimelineHeader>
);
};

FlyoutHeaderComponent.displayName = 'FlyoutHeaderComponent';

Expand Down
Loading