Skip to content

Commit

Permalink
[CTI] Threat Intel Card on Overview page needs to accommodate Fleet T…
Browse files Browse the repository at this point in the history
…I integrations - (elastic#120459)

* Add support integrations

* Fix types

* fix unit tests

* Fix tests and types

* fix eslint

* fix file case

* add cy tests

* Revert test

* Add tests

* Add support of installed integrations

* Fix types

* Add isntalled ingtegration case for cypress tests

* Fix cypress tests

* Fix comments

* Fix capital naming

* Fix again capital naming

* Add dynamic dashboard for a new integrations packages

* intermidiate changes, to keep it remote

* Big refactoring

* Tests and refactoring

* Remove unused constanrs

* Fix e2e tests

* PR comments fix

* fix ts

* Fix translations

* Remove stubs

* Rename isSomeIntegrationsDisabled -> allIntegrationsInstalled

* Add buildQuery tests

* Fix type

* Add tests for Enable Source button

* Remove copied file

* Move api call to api.ts

* Rename fetchFleetIntegrations

* Remove __mocks__

* Fix path
  • Loading branch information
nkhristinin authored and kibanamachine committed Dec 7, 2021
1 parent 8f57ac7 commit eec9a15
Show file tree
Hide file tree
Showing 41 changed files with 731 additions and 720 deletions.
13 changes: 2 additions & 11 deletions x-pack/plugins/security_solution/common/cti/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,5 @@ export const EVENT_ENRICHMENT_INDICATOR_FIELD_MAP = {
export const DEFAULT_EVENT_ENRICHMENT_FROM = 'now-30d';
export const DEFAULT_EVENT_ENRICHMENT_TO = 'now';

export const CTI_DATASET_KEY_MAP: { [key: string]: string } = {
'AbuseCH URL': 'ti_abusech.url',
'AbuseCH Malware': 'ti_abusech.malware',
'AbuseCH MalwareBazaar': 'ti_abusech.malwarebazaar',
'AlienVault OTX': 'ti_otx.threat',
'Anomali Limo': 'ti_anomali.limo',
'Anomali Threatstream': 'ti_anomali.threatstream',
MISP: 'ti_misp.threat',
ThreatQuotient: 'ti_threatq.threat',
Cybersixgill: 'ti_cybersixgill.threat',
};
export const TI_INTEGRATION_PREFIX = 'ti';
export const OTHER_TI_DATASET_KEY = '_others_ti_';
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
* 2.0.
*/

import type { IEsSearchResponse } from 'src/plugins/data/public';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { IEsSearchResponse, IEsSearchRequest } from 'src/plugins/data/public';
import { FactoryQueryTypes } from '../..';
import { EVENT_ENRICHMENT_INDICATOR_FIELD_MAP } from '../../../cti/constants';
import { Inspect } from '../../common';
import { Inspect, Maybe, TimerangeInput } from '../../common';
import { RequestBasicOptions } from '..';

export enum CtiQueries {
eventEnrichment = 'eventEnrichment',
dataSource = 'dataSource',
}

export interface CtiEventEnrichmentRequestOptions extends RequestBasicOptions {
Expand Down Expand Up @@ -40,3 +43,33 @@ export const validEventFields = Object.keys(EVENT_ENRICHMENT_INDICATOR_FIELD_MAP

export const isValidEventField = (field: string): field is EventField =>
validEventFields.includes(field as EventField);

export interface CtiDataSourceRequestOptions extends IEsSearchRequest {
defaultIndex: string[];
factoryQueryType?: FactoryQueryTypes;
timerange?: TimerangeInput;
}

export interface BucketItem {
key: string;
doc_count: number;
}
export interface Bucket {
buckets: Array<BucketItem & { bucket?: Bucket[] }>;
}

export type DatasetBucket = {
name?: Bucket;
dashboard?: Bucket;
} & BucketItem;

export interface CtiDataSourceStrategyResponse extends Omit<IEsSearchResponse, 'rawResponse'> {
inspect?: Maybe<Inspect>;
rawResponse: {
aggregations?: Record<string, estypes.AggregationsAggregate> & {
dataset?: {
buckets: DatasetBucket[];
};
};
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ import {
CtiEventEnrichmentRequestOptions,
CtiEventEnrichmentStrategyResponse,
CtiQueries,
CtiDataSourceRequestOptions,
CtiDataSourceStrategyResponse,
} from './cti';
import {
HostRulesRequestOptions,
Expand All @@ -85,6 +87,7 @@ import {
UserRulesStrategyResponse,
} from './ueba';

export * from './cti';
export * from './hosts';
export * from './matrix_histogram';
export * from './network';
Expand Down Expand Up @@ -178,6 +181,8 @@ export type StrategyResponseType<T extends FactoryQueryTypes> = T extends HostsQ
? MatrixHistogramStrategyResponse
: T extends CtiQueries.eventEnrichment
? CtiEventEnrichmentStrategyResponse
: T extends CtiQueries.dataSource
? CtiDataSourceStrategyResponse
: never;

export type StrategyRequestType<T extends FactoryQueryTypes> = T extends HostsQueries.hosts
Expand Down Expand Up @@ -238,6 +243,8 @@ export type StrategyRequestType<T extends FactoryQueryTypes> = T extends HostsQu
? MatrixHistogramRequestOptions
: T extends CtiQueries.eventEnrichment
? CtiEventEnrichmentRequestOptions
: T extends CtiQueries.dataSource
? CtiDataSourceRequestOptions
: never;

export interface DocValueFieldsInput {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ import {
OVERVIEW_CTI_LINKS,
OVERVIEW_CTI_LINKS_ERROR_INNER_PANEL,
OVERVIEW_CTI_LINKS_INFO_INNER_PANEL,
OVERVIEW_CTI_LINKS_WARNING_INNER_PANEL,
OVERVIEW_CTI_TOTAL_EVENT_COUNT,
OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON,
OVERVIEW_CTI_ENABLE_INTEGRATIONS_BUTTON,
} from '../../screens/overview';

import { loginAndWaitForPage } from '../../tasks/login';
Expand All @@ -28,12 +27,11 @@ describe('CTI Link Panel', () => {
it('renders disabled threat intel module as expected', () => {
loginAndWaitForPage(OVERVIEW_URL);
cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_ERROR_INNER_PANEL}`).should('exist');
cy.get(`${OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON}`).should('be.disabled');
cy.get(`${OVERVIEW_CTI_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 0 indicators');
cy.get(`${OVERVIEW_CTI_ENABLE_MODULE_BUTTON}`).should('exist');
cy.get(`${OVERVIEW_CTI_ENABLE_MODULE_BUTTON}`)
.should('have.attr', 'href')
.and('match', /filebeat-module-threatintel.html/);
.and('match', /app\/integrations\/browse\?q=threat%20intelligence/);
});

describe('enabled threat intel module', () => {
Expand All @@ -49,17 +47,16 @@ describe('CTI Link Panel', () => {
loginAndWaitForPage(
`${OVERVIEW_URL}?sourcerer=(timerange:(from:%272021-07-08T04:00:00.000Z%27,kind:absolute,to:%272021-07-09T03:59:59.999Z%27))`
);
cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_WARNING_INNER_PANEL}`).should('exist');
cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_INFO_INNER_PANEL}`).should('exist');
cy.get(`${OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON}`).should('be.disabled');
cy.get(`${OVERVIEW_CTI_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 0 indicators');
});

it('renders dashboard module as expected when there are events in the selected time period', () => {
loginAndWaitForPage(OVERVIEW_URL);
cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_WARNING_INNER_PANEL}`).should('not.exist');
cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_INFO_INNER_PANEL}`).should('exist');
cy.get(`${OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON}`).should('be.disabled');
cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_ENABLE_INTEGRATIONS_BUTTON}`).should('exist');
cy.get(OVERVIEW_CTI_LINKS).should('not.contain.text', 'Anomali');
cy.get(OVERVIEW_CTI_LINKS).should('contain.text', 'AbuseCH malware');
cy.get(`${OVERVIEW_CTI_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 1 indicator');
});
});
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/security_solution/cypress/screens/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,9 @@ export const OVERVIEW_REVENT_TIMELINES = '[data-test-subj="overview-recent-timel

export const OVERVIEW_CTI_LINKS = '[data-test-subj="cti-dashboard-links"]';
export const OVERVIEW_CTI_LINKS_ERROR_INNER_PANEL = '[data-test-subj="cti-inner-panel-danger"]';
export const OVERVIEW_CTI_LINKS_WARNING_INNER_PANEL = '[data-test-subj="cti-inner-panel-warning"]';
export const OVERVIEW_CTI_LINKS_INFO_INNER_PANEL = '[data-test-subj="cti-inner-panel-info"]';
export const OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON = '[data-test-subj="cti-view-dashboard-button"]';
export const OVERVIEW_CTI_ENABLE_INTEGRATIONS_BUTTON =
'[data-test-subj="cti-enable-integrations-button"]';
export const OVERVIEW_CTI_TOTAL_EVENT_COUNT = `${OVERVIEW_CTI_LINKS} [data-test-subj="header-panel-subtitle"]`;
export const OVERVIEW_CTI_ENABLE_MODULE_BUTTON = '[data-test-subj="cti-enable-module-button"]';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@
* 2.0.
*/

import { LinkPanelListItem } from '.';

export const isLinkPanelListItem = (
item: LinkPanelListItem | Partial<LinkPanelListItem>
): item is LinkPanelListItem =>
typeof item.title === 'string' && typeof item.path === 'string' && typeof item.count === 'number';

export interface EventCounts {
[key: string]: number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@
*/

export { InnerLinkPanel } from './inner_link_panel';
export { isLinkPanelListItem } from './helpers';
export { LinkPanel } from './link_panel';
export type { LinkPanelListItem } from './types';
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const LinkPanelComponent = ({
splitPanel,
subtitle,
}: {
button: React.ReactNode;
button?: React.ReactNode;
columns: Array<EuiTableFieldDataColumnType<LinkPanelListItem>>;
dataTestSubj: string;
defaultSortField?: string;
Expand Down Expand Up @@ -134,14 +134,16 @@ const LinkPanelComponent = ({
</HeaderSection>
{splitPanel}
{infoPanel}
<StyledTable
columns={columns}
itemId="id"
items={chunkedItems[pageIndex] || []}
onChange={onTableChange}
pagination={pagination}
sorting={sorting}
/>
{chunkedItems.length > 0 && (
<StyledTable
columns={columns}
itemId="id"
items={chunkedItems[pageIndex] || []}
onChange={onTableChange}
pagination={pagination}
sorting={sorting}
/>
)}
</EuiPanel>
</InspectButtonContainer>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ export interface LinkPanelViewProps {
listItems: LinkPanelListItem[];
splitPanel?: JSX.Element;
totalCount?: number;
allIntegrationsInstalled?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,21 @@
*/

import React from 'react';
import { EMPTY_LIST_ITEMS } from '../../containers/overview_cti_links/helpers';
import { useKibana } from '../../../common/lib/kibana';
import * as i18n from './translations';
import { DisabledLinkPanel } from '../link_panel/disabled_link_panel';
import { ThreatIntelPanelView } from './threat_intel_panel_view';
import { useIntegrationsPageLink } from './use_integrations_page_link';

export const CtiDisabledModuleComponent = () => {
const threatIntelDocLink = `${
useKibana().services.docLinks.links.filebeat.base
}/filebeat-module-threatintel.html`;
const integrationsLink = useIntegrationsPageLink();

return (
<DisabledLinkPanel
bodyCopy={i18n.DANGER_BODY}
buttonCopy={i18n.DANGER_BUTTON}
dataTestSubjPrefix="cti"
docLink={threatIntelDocLink}
listItems={EMPTY_LIST_ITEMS}
docLink={integrationsLink}
listItems={[]}
titleCopy={i18n.DANGER_TITLE}
LinkPanelViewComponent={ThreatIntelPanelView}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,15 @@ import {
mockGlobalState,
SUB_PLUGINS_REDUCER,
} from '../../../common/mock';
import { mockTheme, mockProps, mockCtiEventCountsResponse, mockCtiLinksResponse } from './mock';
import { useCtiEventCounts } from '../../containers/overview_cti_links/use_cti_event_counts';
import { mockTheme, mockProps, mockTiDataSources, mockCtiLinksResponse } from './mock';
import { useCtiDashboardLinks } from '../../containers/overview_cti_links';
import { useRequestEventCounts } from '../../containers/overview_cti_links/use_request_event_counts';
import { useTiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources';

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

jest.mock('../../containers/overview_cti_links/use_cti_event_counts');
const useCTIEventCountsMock = useCtiEventCounts as jest.Mock;
useCTIEventCountsMock.mockReturnValue(mockCtiEventCountsResponse);

jest.mock('../../containers/overview_cti_links/use_request_event_counts');
const useRequestEventCountsMock = useRequestEventCounts as jest.Mock;
useRequestEventCountsMock.mockReturnValue([true, {}]);
jest.mock('../../containers/overview_cti_links/use_ti_data_sources');
const useTiDataSourcesMock = useTiDataSources as jest.Mock;
useTiDataSourcesMock.mockReturnValue(mockTiDataSources);

jest.mock('../../containers/overview_cti_links');
const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock;
Expand All @@ -54,42 +49,12 @@ describe('CtiEnabledModule', () => {
<Provider store={store}>
<I18nProvider>
<ThemeProvider theme={mockTheme}>
<CtiEnabledModule {...mockProps} />
</ThemeProvider>
</I18nProvider>
</Provider>
);

expect(screen.getByTestId('cti-with-events')).toBeInTheDocument();
});

it('renders CtiWithNoEvents when there are no events', () => {
useCTIEventCountsMock.mockReturnValueOnce({ totalCount: 0 });
render(
<Provider store={store}>
<I18nProvider>
<ThemeProvider theme={mockTheme}>
<CtiEnabledModule {...mockProps} />
</ThemeProvider>
</I18nProvider>
</Provider>
);

expect(screen.getByTestId('cti-with-no-events')).toBeInTheDocument();
});

it('renders null while event counts are loading', () => {
useCTIEventCountsMock.mockReturnValueOnce({ totalCount: -1 });
const { container } = render(
<Provider store={store}>
<I18nProvider>
<ThemeProvider theme={mockTheme}>
<CtiEnabledModule {...mockProps} />
<CtiEnabledModule {...mockProps} allIntegrationsInstalled={true} />
</ThemeProvider>
</I18nProvider>
</Provider>
);

expect(container.firstChild).toBeNull();
expect(screen.getByText('Showing: 5 indicators')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,28 @@

import React from 'react';
import { ThreatIntelLinkPanelProps } from '.';
import { useCtiEventCounts } from '../../containers/overview_cti_links/use_cti_event_counts';
import { CtiNoEvents } from './cti_no_events';
import { CtiWithEvents } from './cti_with_events';
import { useTiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources';
import { useCtiDashboardLinks } from '../../containers/overview_cti_links';
import { ThreatIntelPanelView } from './threat_intel_panel_view';

export type CtiEnabledModuleProps = Omit<ThreatIntelLinkPanelProps, 'isThreatIntelModuleEnabled'>;
export const CtiEnabledModuleComponent: React.FC<ThreatIntelLinkPanelProps> = (props) => {
const { to, from, allIntegrationsInstalled, allTiDataSources, setQuery, deleteQuery } = props;
const { tiDataSources, totalCount } = useTiDataSources({
to,
from,
allTiDataSources,
setQuery,
deleteQuery,
});
const { listItems } = useCtiDashboardLinks({ to, from, tiDataSources });

export const CtiEnabledModuleComponent: React.FC<CtiEnabledModuleProps> = (props) => {
const { eventCountsByDataset, totalCount } = useCtiEventCounts(props);
const { to, from } = props;

switch (totalCount) {
case -1:
return null;
case 0:
return (
<div data-test-subj="cti-with-no-events">
<CtiNoEvents to={to} from={from} />
</div>
);
default:
return (
<div data-test-subj="cti-with-events">
<CtiWithEvents
eventCountsByDataset={eventCountsByDataset}
totalCount={totalCount}
to={to}
from={from}
/>
</div>
);
}
return (
<ThreatIntelPanelView
listItems={listItems}
totalCount={totalCount}
allIntegrationsInstalled={allIntegrationsInstalled}
/>
);
};

export const CtiEnabledModule = React.memo(CtiEnabledModuleComponent);
Expand Down
Loading

0 comments on commit eec9a15

Please sign in to comment.