Skip to content

Commit

Permalink
[CTI] Adds Dashboard Links to Overview Page
Browse files Browse the repository at this point in the history
  • Loading branch information
ecezalp committed May 27, 2021
1 parent f0e11bc commit 32afd68
Show file tree
Hide file tree
Showing 5 changed files with 509 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* 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 { cloneDeep } from 'lodash/fp';
import { mount } from 'enzyme';
import React from 'react';

import '../../../common/mock/match_media';
import {
mockGlobalState,
TestProviders,
SUB_PLUGINS_REDUCER,
kibanaObservable,
createSecuritySolutionStorageMock,
mockIndexPattern,
} from '../../../common/mock';

import { ThreatIntelLinkPanel } from '.';
import { createStore, State } from '../../../common/store';
import { useThreatIntelDashboardLinks } from '../../containers/overview_cti_links';

jest.mock('../../../common/lib/kibana');

const testProps = {
to: '2020-01-20T20:49:57.080Z',
from: '2020-01-21T20:49:57.080Z',
indexNames: [],
indexPattern: mockIndexPattern,
setQuery: jest.fn(),
deleteQuery: jest.fn(),
filters: [],
query: {
query: '',
language: 'kuery',
},
};

jest.mock('../../containers/overview_cti_links');
const useThreatIntelDashboardLinksMock = useThreatIntelDashboardLinks as jest.Mock;
useThreatIntelDashboardLinksMock.mockReturnValue({
buttonLink: { path: '/button-link-path' },
dashboardLinks: [
{ count: 1, title: 'anomali', path: '/dashboard-link-0' },
{ count: 99, title: 'alienvault', path: '/dashboard-link-1' },
],
totalEventCount: 100,
});

describe('OverviewCTILinks', () => {
const state: State = mockGlobalState;

const { storage } = createSecuritySolutionStorageMock();
let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);

beforeEach(() => {
const myState = cloneDeep(state);
store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
});

test('it appears if dashboard links are present', () => {
const wrapper = mount(
<TestProviders store={store}>
<ThreatIntelLinkPanel {...testProps} />
</TestProviders>
);

expect(wrapper.find('[data-test-subj="header-section-title"]').first().text()).toEqual(
'Threat Intelligence'
);
wrapper.unmount();
});

test('does not appear if dashboard links are not present', () => {
useThreatIntelDashboardLinksMock.mockReturnValueOnce([
true,
{
buttonLink: null,
dashboardLinks: [],
totalEventCount: 0,
},
]);
const wrapper = mount(
<TestProviders store={store}>
<ThreatIntelLinkPanel {...testProps} />
</TestProviders>
);
const element = wrapper.find('[data-test-subj="cti-dashboard-links"]').first();
expect(element).toEqual({});
wrapper.unmount();
});

test('it renders links', () => {
const wrapper = mount(
<TestProviders store={store}>
<ThreatIntelLinkPanel {...testProps} />
</TestProviders>
);

const hrefs = wrapper
.find('[data-test-subj="cti-dashboard-link"]')
.map((link) => link.props().href);

expect(hrefs).toContain('/dashboard-link-0');
expect(hrefs).toContain('/dashboard-link-1');
wrapper.unmount();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* 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, { useMemo } from 'react';
import styled from 'styled-components';

import {
EuiFlexItem,
EuiPanel,
EuiFlexGroup,
EuiSpacer,
EuiHorizontalRule,
EuiLink,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { LinkButton } from '../../../common/components/links';
import { InspectButtonContainer } from '../../../common/components/inspect';
import { HeaderSection } from '../../../common/components/header_section';

import { ID as CTIEventCountQueryId } from '../../containers/overview_cti_links/get_event_counts';
import { GlobalTimeArgs } from '../../../common/containers/use_global_time';
import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/common';
import { useThreatIntelDashboardLinks } from '../../containers/overview_cti_links';

export interface ThreatIntelLinkPanelProps
extends Pick<GlobalTimeArgs, 'from' | 'to' | 'deleteQuery' | 'setQuery'> {
filters: Filter[];
hideHeaderChildren?: boolean;
indexPattern: IIndexPattern;
indexNames: string[];
query: Query;
}

const DashboardLink = styled.li`
margin: 0 ${({ theme }) => theme.eui.paddingSizes.s} 0 ${({ theme }) => theme.eui.paddingSizes.m};
`;

const DashboardLinkItems = styled(EuiFlexGroup)`
width: 100%;
`;

const Title = styled(EuiFlexItem)`
min-width: 140px;
`;

const List = styled.ul`
margin-bottom: ${({ theme }) => theme.eui.paddingSizes.l};
`;

const DashboardRightSideElement = styled(EuiFlexItem)`
align-items: flex-end;
max-width: 160px;
`;

const RightSideLink = styled(EuiLink)`
text-align: right;
min-width: 140px;
`;

const ThreatIntelLinkPanelComponent: React.FC<ThreatIntelLinkPanelProps> = (props) => {
const { buttonLink, dashboardLinks, totalEventCount } = useThreatIntelDashboardLinks(props);

const panelTitle = useMemo(
() => (
<FormattedMessage
id="xpack.securitySolution.overview.ctiDashboardTitle"
defaultMessage="Threat Intelligence"
/>
),
[]
);

const subtitle = useMemo(() => {
return totalEventCount ? (
<FormattedMessage
defaultMessage="Showing: {totalEventCount} {totalEventCount, plural, one {event} other {events}}"
id="xpack.securitySolution.overview.ctiDashboardSubtitle"
values={{ totalEventCount }}
/>
) : null;
}, [totalEventCount]);

const button = useMemo(() => {
return buttonLink ? (
<LinkButton href={buttonLink.path}>
<FormattedMessage
id="xpack.securitySolution.overview.ctiViewDasboard"
defaultMessage="View dashboard"
/>
</LinkButton>
) : null;
}, [buttonLink]);

return dashboardLinks?.length ? (
<>
<EuiSpacer data-test-subj="spacer" size="l" />
<EuiFlexGroup
gutterSize="l"
justifyContent="spaceBetween"
data-test-subj="cti-dashboard-links"
>
<EuiFlexItem grow={1}>
<InspectButtonContainer>
<EuiPanel>
<HeaderSection id={CTIEventCountQueryId} subtitle={subtitle} title={panelTitle}>
<>{button}</>
</HeaderSection>
<List>
<EuiFlexGroup direction={'column'}>
{dashboardLinks.map(({ title, path, count }) => (
<DashboardLink key={`${title}-list-item`}>
<EuiHorizontalRule margin="s" />
<DashboardLinkItems
direction="row"
gutterSize="l"
justifyContent="spaceBetween"
>
<Title key={`${title}-link`} grow={3}>
{title}
</Title>
<EuiFlexGroup
gutterSize="s"
key={`${title}-divider`}
direction="row"
alignItems="center"
>
<DashboardRightSideElement key={`${title}-count`} grow={1}>
{count}
</DashboardRightSideElement>
<DashboardRightSideElement key={`${title}-source`} grow={3}>
<RightSideLink href={path} data-test-subj="cti-dashboard-link">
<FormattedMessage
id="xpack.securitySolution.overview.ctiViewSourceDasboard"
defaultMessage="View source dashboard"
/>
</RightSideLink>
</DashboardRightSideElement>
</EuiFlexGroup>
</DashboardLinkItems>
</DashboardLink>
))}
</EuiFlexGroup>
</List>
</EuiPanel>
</InspectButtonContainer>
</EuiFlexItem>
<EuiFlexItem grow={1} />
</EuiFlexGroup>
</>
) : null;
};

ThreatIntelLinkPanelComponent.displayName = 'ThreatIntelDashboardLinksComponent';

export const ThreatIntelLinkPanel = React.memo(ThreatIntelLinkPanelComponent);
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* 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 { useEffect, useState, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution';
import { useKibana } from '../../../common/lib/kibana';
import { ThreatIntelLinkPanelProps } from '../../components/overview_cti_links';
import { convertToBuildEsQuery } from '../../../common/lib/keury';
import { esQuery, Filter } from '../../../../../../../src/plugins/data/public';
import { useMatrixHistogram } from '../../../common/containers/matrix_histogram';
import { EVENT_DATASET } from '../../../../common/cti/constants';

export const ID = 'ctiEventCountQuery';

const ctiEventsFilter: Filter = {
meta: {
alias: null,
disabled: false,
key: 'event.dataset',
negate: false,
params: {
query: 'file',
},
type: 'phrase',
},
query: {
match_phrase: { 'event.module': 'threatintel' },
},
};

export const useCTIEventCounts = ({
deleteQuery,
filters,
from,
indexNames,
indexPattern,
query,
setQuery,
to,
}: ThreatIntelLinkPanelProps) => {
const [isInitialLoading, setIsInitialLoading] = useState(true);
const { uiSettings } = useKibana().services;

useEffect(() => {
return () => {
if (deleteQuery) {
deleteQuery({ id: ID });
}
};
}, [deleteQuery]);

const matrixHistogramRequest = useMemo(
() => ({
endDate: to,
errorMessage: i18n.translate('xpack.securitySolution.overview.errorFetchingEvents', {
defaultMessage: 'Error fetching events',
}),
filterQuery: convertToBuildEsQuery({
config: esQuery.getEsQueryConfig(uiSettings),
indexPattern,
queries: [query],
filters: [...filters, ctiEventsFilter],
}),
histogramType: MatrixHistogramType.events,
indexNames,
stackByField: EVENT_DATASET,
startDate: from,
}),
[filters, from, indexPattern, uiSettings, query, to, indexNames]
);

const [loading, { data, inspect, totalCount, refetch }] = useMatrixHistogram(
matrixHistogramRequest
);

useEffect(() => {
if (!loading && !isInitialLoading) {
setQuery({ id: ID, inspect, loading, refetch });
}
}, [setQuery, inspect, loading, refetch, isInitialLoading, setIsInitialLoading]);

useEffect(() => {
if (isInitialLoading && data) {
setIsInitialLoading(false);
}
}, [isInitialLoading, data]);

const returnVal = {
eventCounts: data.reduce((acc, item) => {
if (item.y && item.g?.match('threatintel.')) {
const id = item.g.replace('threatintel.', '');
if (typeof acc[id] === 'number') {
acc[id]! += item.y;
} else {
acc[id] = item.y;
}
}
return acc;
}, {} as { id: number | undefined; [key: string]: number | undefined }),
total: totalCount,
};

return returnVal;
};
Loading

0 comments on commit 32afd68

Please sign in to comment.