diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/all/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/all/index.ts new file mode 100644 index 00000000000000..cd8a58d03d30c0 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/all/index.ts @@ -0,0 +1,35 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; + +import { HostItem } from '../common'; +import { + CursorType, + Inspect, + Maybe, + PageInfoPaginated, + RequestOptionsPaginated, + SortField, +} from '../..'; + +export interface HostsEdges { + node: HostItem; + + cursor: CursorType; +} + +export interface HostsStrategyResponse extends IEsSearchResponse { + edges: HostsEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface HostsRequestOptions extends RequestOptionsPaginated { + sort: SortField; + defaultIndex: string[]; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts new file mode 100644 index 00000000000000..fb4fd762a86bf3 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts @@ -0,0 +1,95 @@ +/* + * 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 { CloudEcs } from '../../../../ecs/cloud'; +import { HostEcs, OsEcs } from '../../../../ecs/host'; +import { Maybe, SearchHit, TotalValue } from '../..'; + +export enum HostPolicyResponseActionStatus { + success = 'success', + failure = 'failure', + warning = 'warning', +} + +export interface EndpointFields { + endpointPolicy?: Maybe; + sensorVersion?: Maybe; + policyStatus?: Maybe; +} + +export interface HostItem { + _id?: Maybe; + cloud?: Maybe; + endpoint?: Maybe; + host?: Maybe; + lastSeen?: Maybe; +} + +export interface HostValue { + value: number; + value_as_string: string; +} + +export interface HostBucketItem { + key: string; + doc_count: number; + timestamp: HostValue; +} + +export interface HostBuckets { + buckets: HostBucketItem[]; +} + +export interface HostOsHitsItem { + hits: { + total: TotalValue | number; + max_score: number | null; + hits: Array<{ + _source: { host: { os: Maybe } }; + sort?: [number]; + _index?: string; + _type?: string; + _id?: string; + _score?: number | null; + }>; + }; +} + +export interface HostAggEsItem { + cloud_instance_id?: HostBuckets; + cloud_machine_type?: HostBuckets; + cloud_provider?: HostBuckets; + cloud_region?: HostBuckets; + firstSeen?: HostValue; + host_architecture?: HostBuckets; + host_id?: HostBuckets; + host_ip?: HostBuckets; + host_mac?: HostBuckets; + host_name?: HostBuckets; + host_os_name?: HostBuckets; + host_os_version?: HostBuckets; + host_type?: HostBuckets; + key?: string; + lastSeen?: HostValue; + os?: HostOsHitsItem; +} + +export interface HostEsData extends SearchHit { + sort: string[]; + aggregations: { + host_count: { + value: number; + }; + host_data: { + buckets: HostAggEsItem[]; + }; + }; +} + +export interface HostAggEsData extends SearchHit { + sort: string[]; + aggregations: HostAggEsItem; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts new file mode 100644 index 00000000000000..93a5c0582fb968 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { Inspect, Maybe, RequestOptionsPaginated } from '../..'; + +export interface HostFirstLastSeenRequestOptions extends Partial { + hostName: string; +} +export interface HostFirstLastSeenStrategyResponse extends IEsSearchResponse { + inspect?: Maybe; + firstSeen?: Maybe; + lastSeen?: Maybe; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts index 3a0942d2decb82..a27899e4540747 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts @@ -4,81 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; -import { CloudEcs } from '../../../ecs/cloud'; -import { HostEcs } from '../../../ecs/host'; - -import { - CursorType, - Inspect, - Maybe, - PageInfoPaginated, - RequestOptionsPaginated, - SortField, - TimerangeInput, -} from '..'; +export * from './all'; +export * from './common'; +export * from './overview'; +export * from './first_last_seen'; export enum HostsQueries { hosts = 'hosts', hostOverview = 'hostOverview', -} - -export enum HostPolicyResponseActionStatus { - success = 'success', - failure = 'failure', - warning = 'warning', -} - -export interface EndpointFields { - endpointPolicy?: Maybe; - - sensorVersion?: Maybe; - - policyStatus?: Maybe; -} - -export interface HostItem { - _id?: Maybe; - - cloud?: Maybe; - - endpoint?: Maybe; - - host?: Maybe; - - lastSeen?: Maybe; -} - -export interface HostsEdges { - node: HostItem; - - cursor: CursorType; -} - -export interface HostsStrategyResponse extends IEsSearchResponse { - edges: HostsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe; -} - -export interface HostOverviewStrategyResponse extends IEsSearchResponse, HostItem { - inspect?: Maybe; -} - -export interface HostsRequestOptions extends RequestOptionsPaginated { - sort: SortField; - defaultIndex: string[]; -} - -export interface HostLastFirstSeenRequestOptions extends Partial { - hostName: string; -} - -export interface HostOverviewRequestOptions extends HostLastFirstSeenRequestOptions { - fields: string[]; - timerange: TimerangeInput; + firstLastSeen = 'firstLastSeen', } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts new file mode 100644 index 00000000000000..bc797e7a03fb87 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts @@ -0,0 +1,22 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; + +import { HostItem } from '../common'; +import { Inspect, Maybe, RequestOptionsPaginated, TimerangeInput } from '../..'; + +export interface HostOverviewStrategyResponse extends IEsSearchResponse { + hostOverview: HostItem; + inspect?: Maybe; +} + +export interface HostOverviewRequestOptions extends Partial { + hostName: string; + skip?: boolean; + timerange: TimerangeInput; + inspect?: Maybe; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index a188eb7619e6be..073b96e400f5c4 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IEsSearchRequest } from '../../../../../../src/plugins/data/common'; +import { IEsSearchRequest, IEsSearchResponse } from '../../../../../../src/plugins/data/common'; import { ESQuery } from '../../typed_json'; import { HostOverviewStrategyResponse, HostOverviewRequestOptions, + HostFirstLastSeenStrategyResponse, + HostFirstLastSeenRequestOptions, HostsQueries, HostsRequestOptions, HostsStrategyResponse, @@ -18,6 +20,13 @@ export type Maybe = T | null; export type FactoryQueryTypes = HostsQueries; +export type SearchHit = IEsSearchResponse['rawResponse']['hits']['hits'][0]; + +export interface TotalValue { + value: number; + relation: string; +} + export interface Inspect { dsl: string[]; response: string[]; @@ -100,10 +109,14 @@ export type StrategyResponseType = T extends HostsQ ? HostsStrategyResponse : T extends HostsQueries.hostOverview ? HostOverviewStrategyResponse + : T extends HostsQueries.firstLastSeen + ? HostFirstLastSeenStrategyResponse : never; export type StrategyRequestType = T extends HostsQueries.hosts ? HostsRequestOptions : T extends HostsQueries.hostOverview ? HostOverviewRequestOptions + : T extends HostsQueries.firstLastSeen + ? HostFirstLastSeenRequestOptions : never; diff --git a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx index a2f53be7218162..606b43c6508fb9 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx @@ -4,18 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cloneDeep } from 'lodash/fp'; import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; // we don't have the types for waitFor just yet, so using "as waitFor" until when we do import { render, act, wait as waitFor } from '@testing-library/react'; -import { mockFirstLastSeenHostQuery } from '../../containers/hosts/first_last_seen/mock'; +import { useFirstLastSeenHost } from '../../containers/hosts/first_last_seen'; import { TestProviders } from '../../../common/mock'; - import { FirstLastSeenHost, FirstLastSeenHostType } from '.'; +const MOCKED_RESPONSE = { + firstSeen: '2019-04-08T16:09:40.692Z', + lastSeen: '2019-04-08T18:35:45.064Z', +}; + +jest.mock('../../containers/hosts/first_last_seen'); +const useFirstLastSeenHostMock = useFirstLastSeenHost as jest.Mock; +useFirstLastSeenHostMock.mockReturnValue([false, MOCKED_RESPONSE]); + describe('FirstLastSeen Component', () => { const firstSeen = 'Apr 8, 2019 @ 16:09:40.692'; const lastSeen = 'Apr 8, 2019 @ 18:35:45.064'; @@ -31,11 +37,10 @@ describe('FirstLastSeen Component', () => { }); test('Loading', async () => { + useFirstLastSeenHostMock.mockReturnValue([true, MOCKED_RESPONSE]); const { container } = render( - - - + ); expect(container.innerHTML).toBe( @@ -44,11 +49,10 @@ describe('FirstLastSeen Component', () => { }); test('First Seen', async () => { + useFirstLastSeenHostMock.mockReturnValue([false, MOCKED_RESPONSE]); const { container } = render( - - - + ); @@ -62,11 +66,10 @@ describe('FirstLastSeen Component', () => { }); test('Last Seen', async () => { + useFirstLastSeenHostMock.mockReturnValue([false, MOCKED_RESPONSE]); const { container } = render( - - - + ); await act(() => @@ -79,13 +82,16 @@ describe('FirstLastSeen Component', () => { }); test('First Seen is empty but not Last Seen', async () => { - const badDateTime = cloneDeep(mockFirstLastSeenHostQuery); - badDateTime[0].result.data!.source.HostFirstLastSeen.firstSeen = null; + useFirstLastSeenHostMock.mockReturnValue([ + false, + { + ...MOCKED_RESPONSE, + firstSeen: null, + }, + ]); const { container } = render( - - - + ); @@ -99,13 +105,16 @@ describe('FirstLastSeen Component', () => { }); test('Last Seen is empty but not First Seen', async () => { - const badDateTime = cloneDeep(mockFirstLastSeenHostQuery); - badDateTime[0].result.data!.source.HostFirstLastSeen.lastSeen = null; + useFirstLastSeenHostMock.mockReturnValue([ + false, + { + ...MOCKED_RESPONSE, + lastSeen: null, + }, + ]); const { container } = render( - - - + ); @@ -119,13 +128,16 @@ describe('FirstLastSeen Component', () => { }); test('First Seen With a bad date time string', async () => { - const badDateTime = cloneDeep(mockFirstLastSeenHostQuery); - badDateTime[0].result.data!.source.HostFirstLastSeen.firstSeen = 'something-invalid'; + useFirstLastSeenHostMock.mockReturnValue([ + false, + { + ...MOCKED_RESPONSE, + firstSeen: 'something-invalid', + }, + ]); const { container } = render( - - - + ); await act(() => @@ -136,13 +148,16 @@ describe('FirstLastSeen Component', () => { }); test('Last Seen With a bad date time string', async () => { - const badDateTime = cloneDeep(mockFirstLastSeenHostQuery); - badDateTime[0].result.data!.source.HostFirstLastSeen.lastSeen = 'something-invalid'; + useFirstLastSeenHostMock.mockReturnValue([ + false, + { + ...MOCKED_RESPONSE, + lastSeen: 'something-invalid', + }, + ]); const { container } = render( - - - + ); await act(() => diff --git a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx index 579c3311cf7322..a1b72fb39069ca 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx @@ -5,10 +5,9 @@ */ import { EuiIcon, EuiLoadingSpinner, EuiText, EuiToolTip } from '@elastic/eui'; -import React from 'react'; -import { ApolloConsumer } from 'react-apollo'; +import React, { useMemo } from 'react'; -import { useFirstLastSeenHostQuery } from '../../containers/hosts/first_last_seen'; +import { useFirstLastSeenHost } from '../../containers/hosts/first_last_seen'; import { getEmptyTagValue } from '../../../common/components/empty_value'; import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; @@ -17,49 +16,48 @@ export enum FirstLastSeenHostType { LAST_SEEN = 'last-seen', } -export const FirstLastSeenHost = React.memo<{ hostname: string; type: FirstLastSeenHostType }>( - ({ hostname, type }) => { +interface FirstLastSeenHostProps { + hostName: string; + type: FirstLastSeenHostType; +} + +export const FirstLastSeenHost = React.memo(({ hostName, type }) => { + const [loading, { firstSeen, lastSeen, errorMessage }] = useFirstLastSeenHost({ + hostName, + }); + const valueSeen = useMemo( + () => (type === FirstLastSeenHostType.FIRST_SEEN ? firstSeen : lastSeen), + [firstSeen, lastSeen, type] + ); + + if (errorMessage != null) { return ( - - {(client) => { - /* eslint-disable-next-line react-hooks/rules-of-hooks */ - const { loading, firstSeen, lastSeen, errorMessage } = useFirstLastSeenHostQuery( - hostname, - 'default', - client - ); - if (errorMessage != null) { - return ( - - - - ); - } - const valueSeen = type === FirstLastSeenHostType.FIRST_SEEN ? firstSeen : lastSeen; - return ( - <> - {loading && } - {!loading && valueSeen != null && new Date(valueSeen).toString() === 'Invalid Date' - ? valueSeen - : !loading && - valueSeen != null && ( - - - - )} - {!loading && valueSeen == null && getEmptyTagValue()} - - ); - }} - + + + ); } -); + + return ( + <> + {loading && } + {!loading && valueSeen != null && new Date(valueSeen).toString() === 'Invalid Date' + ? valueSeen + : !loading && + valueSeen != null && ( + + + + )} + {!loading && valueSeen == null && getEmptyTagValue()} + + ); +}); FirstLastSeenHost.displayName = 'FirstLastSeenHost'; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.ts deleted file mode 100644 index 65e379b5ba2d82..00000000000000 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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 ApolloClient from 'apollo-client'; -import { get } from 'lodash/fp'; -import React, { useEffect, useState } from 'react'; - -import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; -import { useUiSetting$ } from '../../../../common/lib/kibana'; -import { GetHostFirstLastSeenQuery } from '../../../../graphql/types'; -import { inputsModel } from '../../../../common/store'; -import { QueryTemplateProps } from '../../../../common/containers/query_template'; -import { useWithSource } from '../../../../common/containers/source'; -import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; - -export interface FirstLastSeenHostArgs { - id: string; - errorMessage: string; - firstSeen: Date; - lastSeen: Date; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface OwnProps extends QueryTemplateProps { - children: (args: FirstLastSeenHostArgs) => React.ReactNode; - hostName: string; -} - -export function useFirstLastSeenHostQuery( - hostName: string, - sourceId: string, - apolloClient: ApolloClient -) { - const [loading, updateLoading] = useState(false); - const [firstSeen, updateFirstSeen] = useState(null); - const [lastSeen, updateLastSeen] = useState(null); - const [errorMessage, updateErrorMessage] = useState(null); - const [defaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); - const { docValueFields } = useWithSource(sourceId); - - async function fetchFirstLastSeenHost(signal: AbortSignal) { - updateLoading(true); - return apolloClient - .query({ - query: HostFirstLastSeenGqlQuery, - fetchPolicy: 'cache-first', - variables: { - sourceId, - hostName, - defaultIndex, - docValueFields, - }, - context: { - fetchOptions: { - signal, - }, - }, - }) - .then( - (result) => { - updateLoading(false); - updateFirstSeen(get('data.source.HostFirstLastSeen.firstSeen', result)); - updateLastSeen(get('data.source.HostFirstLastSeen.lastSeen', result)); - updateErrorMessage(null); - }, - (error) => { - updateLoading(false); - updateFirstSeen(null); - updateLastSeen(null); - updateErrorMessage(error.message); - } - ); - } - - useEffect(() => { - const abortCtrl = new AbortController(); - const signal = abortCtrl.signal; - fetchFirstLastSeenHost(signal); - return () => abortCtrl.abort(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return { firstSeen, lastSeen, loading, errorMessage }; -} diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx new file mode 100644 index 00000000000000..3a93b1ee46e7ba --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx @@ -0,0 +1,139 @@ +/* + * 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 deepEqual from 'fast-deep-equal'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; + +import { useKibana } from '../../../../common/lib/kibana'; +import { + HostsQueries, + HostFirstLastSeenRequestOptions, + HostFirstLastSeenStrategyResponse, +} from '../../../../../common/search_strategy/security_solution'; +import { useWithSource } from '../../../../common/containers/source'; + +import * as i18n from './translations'; +import { AbortError } from '../../../../../../../../src/plugins/data/common'; + +const ID = 'firstLastSeenHostQuery'; + +export interface FirstLastSeenHostArgs { + id: string; + errorMessage: string | null; + firstSeen?: string | null; + lastSeen?: string | null; +} +interface UseHostFirstLastSeen { + hostName: string; +} + +export const useFirstLastSeenHost = ({ + hostName, +}: UseHostFirstLastSeen): [boolean, FirstLastSeenHostArgs] => { + const { docValueFields } = useWithSource('default'); + const { data, notifications, uiSettings } = useKibana().services; + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + const [firstLastSeenHostRequest, setFirstLastSeenHostRequest] = useState< + HostFirstLastSeenRequestOptions + >({ + defaultIndex, + docValueFields: docValueFields ?? [], + factoryQueryType: HostsQueries.firstLastSeen, + hostName, + }); + + const [firstLastSeenHostResponse, setFirstLastSeenHostResponse] = useState( + { + firstSeen: null, + lastSeen: null, + errorMessage: null, + id: ID, + } + ); + + const firstLastSeenHostSearch = useCallback( + (request: HostFirstLastSeenRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + signal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setFirstLastSeenHostResponse((prevResponse) => ({ + ...prevResponse, + errorMessage: null, + firstSeen: response.firstSeen, + lastSeen: response.lastSeen, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_FIRST_LAST_SEEN_HOST); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + setFirstLastSeenHostResponse((prevResponse) => ({ + ...prevResponse, + errorMessage: msg, + })); + notifications.toasts.addDanger({ + title: i18n.FAIL_FIRST_LAST_SEEN_HOST, + text: msg.message, + }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); + + useEffect(() => { + setFirstLastSeenHostRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + docValueFields: docValueFields ?? [], + hostName, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [defaultIndex, docValueFields, hostName]); + + useEffect(() => { + firstLastSeenHostSearch(firstLastSeenHostRequest); + }, [firstLastSeenHostRequest, firstLastSeenHostSearch]); + + return [loading, firstLastSeenHostResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/mock.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/mock.ts deleted file mode 100644 index 7f1b3d97eb5255..00000000000000 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/mock.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; -import { GetHostFirstLastSeenQuery } from '../../../../graphql/types'; - -import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; - -interface MockedProvidedQuery { - request: { - query: GetHostFirstLastSeenQuery.Query; - variables: GetHostFirstLastSeenQuery.Variables; - }; - result: { - data?: { - source: { - id: string; - HostFirstLastSeen: { - firstSeen: string | null; - lastSeen: string | null; - }; - }; - }; - errors?: [{ message: string }]; - }; -} -export const mockFirstLastSeenHostQuery: MockedProvidedQuery[] = [ - { - request: { - query: HostFirstLastSeenGqlQuery, - variables: { - sourceId: 'default', - hostName: 'kibana-siem', - defaultIndex: DEFAULT_INDEX_PATTERN, - docValueFields: [], - }, - }, - result: { - data: { - source: { - id: 'default', - HostFirstLastSeen: { - firstSeen: '2019-04-08T16:09:40.692Z', - lastSeen: '2019-04-08T18:35:45.064Z', - }, - }, - }, - }, - }, -]; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/translations.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/translations.ts new file mode 100644 index 00000000000000..1e0a4ad2378970 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/translations.ts @@ -0,0 +1,21 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const ERROR_FIRST_LAST_SEEN_HOST = i18n.translate( + 'xpack.securitySolution.firstLastSeenHost.errorSearchDescription', + { + defaultMessage: `An error has occurred on first last seen host search`, + } +); + +export const FAIL_FIRST_LAST_SEEN_HOST = i18n.translate( + 'xpack.securitySolution.firstLastSeenHost.failSearchDescription', + { + defaultMessage: `Failed to run search on first last seen host`, + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx index 346de9f87313f3..6e1ebbfd1e7bb6 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx @@ -47,6 +47,7 @@ interface UseAllHost { docValueFields?: DocValueFields[]; filterQuery?: ESTermQuery | string; endDate: string; + skip?: boolean; startDate: string; type: hostsModel.HostsType; } @@ -55,6 +56,7 @@ export const useAllHost = ({ docValueFields, filterQuery, endDate, + skip = false, startDate, type, }: UseAllHost): [boolean, HostsArgs] => { @@ -189,7 +191,7 @@ export const useAllHost = ({ field: sortField, }, }; - if (!deepEqual(prevRequest, myRequest)) { + if (!skip && !deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; @@ -202,6 +204,7 @@ export const useAllHost = ({ endDate, filterQuery, limit, + skip, startDate, sortField, ]); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/_index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/_index.tsx new file mode 100644 index 00000000000000..f766f068f099fc --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/_index.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +// REPLACE WHEN HOST ENDPOINT DATA IS AVAILABLE + +import deepEqual from 'fast-deep-equal'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; +import { inputsModel } from '../../../../common/store'; +import { useKibana } from '../../../../common/lib/kibana'; +import { + HostItem, + HostsQueries, + HostOverviewRequestOptions, + HostOverviewStrategyResponse, +} from '../../../../../common/search_strategy/security_solution/hosts'; + +import * as i18n from './translations'; +import { AbortError } from '../../../../../../../../src/plugins/data/common'; + +const ID = 'hostOverviewQuery'; + +export interface HostOverviewArgs { + id: string; + inspect: inputsModel.InspectQuery; + hostOverview: HostItem; + refetch: inputsModel.Refetch; + startDate: string; + endDate: string; +} + +interface UseHostOverview { + id?: string; + hostName: string; + endDate: string; + skip?: boolean; + startDate: string; +} + +export const useHostOverview = ({ + endDate, + hostName, + skip = false, + startDate, + id = ID, +}: UseHostOverview): [boolean, HostOverviewArgs] => { + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + const [hostOverviewRequest, setHostOverviewRequest] = useState({ + defaultIndex, + hostName, + factoryQueryType: HostsQueries.hostOverview, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }); + + const [hostOverviewResponse, setHostOverviewResponse] = useState({ + endDate, + hostOverview: {}, + id: ID, + inspect: { + dsl: [], + response: [], + }, + refetch: refetch.current, + startDate, + }); + + const hostOverviewSearch = useCallback( + (request: HostOverviewRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + signal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setHostOverviewResponse((prevResponse) => ({ + ...prevResponse, + hostOverview: response.hostOverview, + inspect: response.inspect ?? prevResponse.inspect, + refetch: refetch.current, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_HOST_OVERVIEW); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_HOST_OVERVIEW, + text: msg.message, + }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); + + useEffect(() => { + setHostOverviewRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + hostName, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }; + if (!skip && !deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [defaultIndex, endDate, hostName, startDate, skip]); + + useEffect(() => { + hostOverviewSearch(hostOverviewRequest); + }, [hostOverviewRequest, hostOverviewSearch]); + + return [loading, hostOverviewResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/translations.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/translations.ts new file mode 100644 index 00000000000000..e3fa319e70cc13 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/overview/translations.ts @@ -0,0 +1,21 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const ERROR_HOST_OVERVIEW = i18n.translate( + 'xpack.securitySolution.overviewHost.errorSearchDescription', + { + defaultMessage: `An error has occurred on host overview search`, + } +); + +export const FAIL_HOST_OVERVIEW = i18n.translate( + 'xpack.securitySolution.overviewHost.failSearchDescription', + { + defaultMessage: `Failed to run search on host overview`, + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx index 5232dcfd88189d..f8dcf9635c053e 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx @@ -27,7 +27,8 @@ export const HostsQueryTabBody = ({ const [ loading, { hosts, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, - ] = useAllHost({ docValueFields, endDate, filterQuery, startDate, type }); + ] = useAllHost({ docValueFields, endDate, filterQuery, skip, startDate, type }); + return ( ( description: data.host != null && data.host.name && data.host.name.length ? ( ) : ( @@ -103,7 +103,7 @@ export const HostOverview = React.memo( description: data.host != null && data.host.name && data.host.name.length ? ( ) : ( diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts new file mode 100644 index 00000000000000..5c29d2747f68d0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts @@ -0,0 +1,81 @@ +/* + * 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 { set } from '@elastic/safer-lodash-set/fp'; +import { get, has, head } from 'lodash/fp'; +import { HostsEdges } from '../../../../../../common/search_strategy/security_solution/hosts'; +import { hostFieldsMap } from '../../../../../lib/ecs_fields'; + +import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types'; + +const HOSTS_FIELDS = ['_id', 'lastSeen', 'host.id', 'host.name', 'host.os.name', 'host.os.version']; + +export const formatHostEdgesData = (bucket: HostAggEsItem): HostsEdges => + HOSTS_FIELDS.reduce( + (flattenedFields, fieldName) => { + const hostId = get('key', bucket); + flattenedFields.node._id = hostId || null; + flattenedFields.cursor.value = hostId || ''; + const fieldValue = getHostFieldValue(fieldName, bucket); + if (fieldValue != null) { + return set( + `node.${fieldName}`, + Array.isArray(fieldValue) ? fieldValue : [fieldValue], + flattenedFields + ); + } + return flattenedFields; + }, + { + node: {}, + cursor: { + value: '', + tiebreaker: null, + }, + } as HostsEdges + ); + +const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | string[] | null => { + const aggField = hostFieldsMap[fieldName] + ? hostFieldsMap[fieldName].replace(/\./g, '_') + : fieldName.replace(/\./g, '_'); + if ( + [ + 'host.ip', + 'host.mac', + 'cloud.instance.id', + 'cloud.machine.type', + 'cloud.provider', + 'cloud.region', + ].includes(fieldName) && + has(aggField, bucket) + ) { + const data: HostBuckets = get(aggField, bucket); + return data.buckets.map((obj) => obj.key); + } else if (has(`${aggField}.buckets`, bucket)) { + return getFirstItem(get(`${aggField}`, bucket)); + } else if (has(aggField, bucket)) { + const valueObj: HostValue = get(aggField, bucket); + return valueObj.value_as_string; + } else if (['host.name', 'host.os.name', 'host.os.version'].includes(fieldName)) { + switch (fieldName) { + case 'host.name': + return get('key', bucket) || null; + case 'host.os.name': + return get('os.hits.hits[0]._source.host.os.name', bucket) || null; + case 'host.os.version': + return get('os.hits.hits[0]._source.host.os.version', bucket) || null; + } + } + return null; +}; + +const getFirstItem = (data: HostBuckets): string | null => { + const firstItem = head(data.buckets); + if (firstItem == null) { + return null; + } + return firstItem.key; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.ts new file mode 100644 index 00000000000000..d4c2214b986453 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.ts @@ -0,0 +1,62 @@ +/* + * 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 { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { + HostAggEsItem, + HostsStrategyResponse, + HostsQueries, + HostsRequestOptions, +} from '../../../../../../common/search_strategy/security_solution/hosts'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../types'; +import { buildHostsQuery } from './query.all_hosts.dsl'; +import { formatHostEdgesData } from './helpers'; + +export const allHosts: SecuritySolutionFactory = { + buildDsl: (options: HostsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildHostsQuery(options); + }, + parse: async ( + options: HostsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.host_count.value', response.rawResponse); + const buckets: HostAggEsItem[] = getOr( + [], + 'aggregations.host_data.buckets', + response.rawResponse + ); + const hostsEdges = buckets.map((bucket) => formatHostEdgesData(bucket)); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const edges = hostsEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildHostsQuery(options))], + response: [inspectStringifyObject(response)], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + + return { + ...response, + inspect, + edges, + totalCount, + pageInfo: { + activePage: activePage ? activePage : 0, + fakeTotalCount, + showMorePagesIndicator, + }, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.hosts.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts similarity index 100% rename from x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.hosts.dsl.ts rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.ts diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts index 443e524d71ca3b..34676fc1932fed 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts @@ -4,89 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, getOr } from 'lodash/fp'; - -import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; - -import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../common/constants'; import { FactoryQueryTypes } from '../../../../../common/search_strategy/security_solution'; -import { - HostsStrategyResponse, - HostOverviewStrategyResponse, - HostsQueries, - HostsRequestOptions, - HostOverviewRequestOptions, -} from '../../../../../common/search_strategy/security_solution/hosts'; +import { HostsQueries } from '../../../../../common/search_strategy/security_solution/hosts'; -// TO DO need to move all this types in common -import { HostAggEsData, HostAggEsItem } from '../../../../lib/hosts/types'; - -import { inspectStringifyObject } from '../../../../utils/build_query'; import { SecuritySolutionFactory } from '../types'; -import { buildHostOverviewQuery } from './dsl/query.detail_host.dsl'; -import { buildHostsQuery } from './dsl/query.hosts.dsl'; -import { formatHostEdgesData, formatHostItem } from './helpers'; - -export const allHosts: SecuritySolutionFactory = { - buildDsl: (options: HostsRequestOptions) => { - if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { - throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); - } - return buildHostsQuery(options); - }, - parse: async ( - options: HostsRequestOptions, - response: IEsSearchResponse - ): Promise => { - const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; - const totalCount = getOr(0, 'aggregations.host_count.value', response.rawResponse); - const buckets: HostAggEsItem[] = getOr( - [], - 'aggregations.host_data.buckets', - response.rawResponse - ); - const hostsEdges = buckets.map((bucket) => formatHostEdgesData(bucket)); - const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; - const edges = hostsEdges.splice(cursorStart, querySize - cursorStart); - const inspect = { - dsl: [inspectStringifyObject(buildHostsQuery(options))], - response: [inspectStringifyObject(response)], - }; - const showMorePagesIndicator = totalCount > fakeTotalCount; - - return { - ...response, - inspect, - edges, - totalCount, - pageInfo: { - activePage: activePage ? activePage : 0, - fakeTotalCount, - showMorePagesIndicator, - }, - }; - }, -}; - -export const overviewHost: SecuritySolutionFactory = { - buildDsl: (options: HostOverviewRequestOptions) => { - return buildHostOverviewQuery(options); - }, - parse: async ( - options: HostOverviewRequestOptions, - response: IEsSearchResponse - ): Promise => { - const aggregations: HostAggEsItem = get('aggregations', response.rawResponse) || {}; - const inspect = { - dsl: [inspectStringifyObject(buildHostOverviewQuery(options))], - response: [inspectStringifyObject(response)], - }; - const formattedHostItem = formatHostItem(aggregations); - return { ...response, inspect, _id: options.hostName, ...formattedHostItem }; - }, -}; +import { allHosts } from './all'; +import { overviewHost } from './overview'; +import { firstLastSeenHost } from './last_first_seen'; export const hostsFactory: Record> = { [HostsQueries.hosts]: allHosts, [HostsQueries.hostOverview]: overviewHost, + [HostsQueries.firstLastSeen]: firstLastSeenHost, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.ts new file mode 100644 index 00000000000000..56895583c2ae9b --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/index.ts @@ -0,0 +1,41 @@ +/* + * 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 { get } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { + HostAggEsData, + HostAggEsItem, + HostFirstLastSeenStrategyResponse, + HostsQueries, + HostFirstLastSeenRequestOptions, +} from '../../../../../../common/search_strategy/security_solution/hosts'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../types'; +import { buildFirstLastSeenHostQuery } from './query.last_first_seen_host.dsl'; + +export const firstLastSeenHost: SecuritySolutionFactory = { + buildDsl: (options: HostFirstLastSeenRequestOptions) => buildFirstLastSeenHostQuery(options), + parse: async ( + options: HostFirstLastSeenRequestOptions, + response: IEsSearchResponse + ): Promise => { + const aggregations: HostAggEsItem = get('aggregations', response.rawResponse) || {}; + const inspect = { + dsl: [inspectStringifyObject(buildFirstLastSeenHostQuery(options))], + response: [inspectStringifyObject(response)], + }; + + return { + ...response, + inspect, + firstSeen: get('firstSeen.value_as_string', aggregations), + lastSeen: get('lastSeen.value_as_string', aggregations), + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.last_first_seen_host.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.last_first_seen_host.dsl.ts similarity index 80% rename from x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.last_first_seen_host.dsl.ts rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.last_first_seen_host.dsl.ts index b57bbd2960e4fa..2c65f62b258a9f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.last_first_seen_host.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/last_first_seen/query.last_first_seen_host.dsl.ts @@ -6,13 +6,13 @@ import { isEmpty } from 'lodash/fp'; import { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; -import { HostLastFirstSeenRequestOptions } from '../../../../../../common/search_strategy/security_solution'; +import { HostFirstLastSeenRequestOptions } from '../../../../../../common/search_strategy/security_solution/hosts'; -export const buildLastFirstSeenHostQuery = ({ +export const buildFirstLastSeenHostQuery = ({ hostName, defaultIndex, docValueFields, -}: HostLastFirstSeenRequestOptions): ISearchRequestParams => { +}: HostFirstLastSeenRequestOptions): ISearchRequestParams => { const filter = [{ term: { 'host.name': hostName } }]; const dslQuery = { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/helpers.ts new file mode 100644 index 00000000000000..c7b0d8acc8782c --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/helpers.ts @@ -0,0 +1,85 @@ +/* + * 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 { set } from '@elastic/safer-lodash-set/fp'; +import { get, has, head } from 'lodash/fp'; +import { HostItem } from '../../../../../../common/search_strategy/security_solution/hosts'; +import { hostFieldsMap } from '../../../../../lib/ecs_fields'; + +import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types'; + +export const HOST_FIELDS = [ + '_id', + 'host.architecture', + 'host.id', + 'host.ip', + 'host.id', + 'host.mac', + 'host.name', + 'host.os.family', + 'host.os.name', + 'host.os.platform', + 'host.os.version', + 'host.type', + 'cloud.instance.id', + 'cloud.machine.type', + 'cloud.provider', + 'cloud.region', + 'endpoint.endpointPolicy', + 'endpoint.policyStatus', + 'endpoint.sensorVersion', +]; + +export const formatHostItem = (bucket: HostAggEsItem): HostItem => + HOST_FIELDS.reduce((flattenedFields, fieldName) => { + const fieldValue = getHostFieldValue(fieldName, bucket); + if (fieldValue != null) { + return set(fieldName, fieldValue, flattenedFields); + } + return flattenedFields; + }, {}); + +const getHostFieldValue = (fieldName: string, bucket: HostAggEsItem): string | string[] | null => { + const aggField = hostFieldsMap[fieldName] + ? hostFieldsMap[fieldName].replace(/\./g, '_') + : fieldName.replace(/\./g, '_'); + if ( + [ + 'host.ip', + 'host.mac', + 'cloud.instance.id', + 'cloud.machine.type', + 'cloud.provider', + 'cloud.region', + ].includes(fieldName) && + has(aggField, bucket) + ) { + const data: HostBuckets = get(aggField, bucket); + return data.buckets.map((obj) => obj.key); + } else if (has(`${aggField}.buckets`, bucket)) { + return getFirstItem(get(`${aggField}`, bucket)); + } else if (has(aggField, bucket)) { + const valueObj: HostValue = get(aggField, bucket); + return valueObj.value_as_string; + } else if (['host.name', 'host.os.name', 'host.os.version'].includes(fieldName)) { + switch (fieldName) { + case 'host.name': + return get('key', bucket) || null; + case 'host.os.name': + return get('os.hits.hits[0]._source.host.os.name', bucket) || null; + case 'host.os.version': + return get('os.hits.hits[0]._source.host.os.version', bucket) || null; + } + } + return null; +}; + +const getFirstItem = (data: HostBuckets): string | null => { + const firstItem = head(data.buckets); + if (firstItem == null) { + return null; + } + return firstItem.key; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts new file mode 100644 index 00000000000000..8bdda9ef895b23 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts @@ -0,0 +1,40 @@ +/* + * 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 { get } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { + HostAggEsData, + HostAggEsItem, + HostOverviewStrategyResponse, + HostsQueries, + HostOverviewRequestOptions, +} from '../../../../../../common/search_strategy/security_solution/hosts'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../types'; +import { buildHostOverviewQuery } from './query.host_overview.dsl'; +import { formatHostItem } from './helpers'; + +export const overviewHost: SecuritySolutionFactory = { + buildDsl: (options: HostOverviewRequestOptions) => { + return buildHostOverviewQuery(options); + }, + parse: async ( + options: HostOverviewRequestOptions, + response: IEsSearchResponse + ): Promise => { + const aggregations: HostAggEsItem = get('aggregations', response.rawResponse) || {}; + const inspect = { + dsl: [inspectStringifyObject(buildHostOverviewQuery(options))], + response: [inspectStringifyObject(response)], + }; + const formattedHostItem = formatHostItem(aggregations); + + return { ...response, inspect, hostOverview: formattedHostItem }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.detail_host.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.host_overview.dsl.ts similarity index 91% rename from x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.detail_host.dsl.ts rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.host_overview.dsl.ts index 5c5dec92a51001..913bc90df04bed 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/dsl/query.detail_host.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.host_overview.dsl.ts @@ -9,14 +9,14 @@ import { HostOverviewRequestOptions } from '../../../../../../common/search_stra import { cloudFieldsMap, hostFieldsMap } from '../../../../../lib/ecs_fields'; import { buildFieldsTermAggregation } from '../../../../../lib/hosts/helpers'; import { reduceFields } from '../../../../../utils/build_query/reduce_fields'; +import { HOST_FIELDS } from './helpers'; export const buildHostOverviewQuery = ({ - fields, hostName, defaultIndex, timerange: { from, to }, }: HostOverviewRequestOptions): ISearchRequestParams => { - const esFields = reduceFields(fields, { ...hostFieldsMap, ...cloudFieldsMap }); + const esFields = reduceFields(HOST_FIELDS, { ...hostFieldsMap, ...cloudFieldsMap }); const filter = [ { term: { 'host.name': hostName } },