[Security solution] [Hosts] Endpoint overview on host details page (#71466)

This commit is contained in:
Steph Milovic 2020-07-14 15:18:17 -06:00 committed by GitHub
parent 04cdb5ad6f
commit f5259ed373
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 677 additions and 164 deletions

View file

@ -6525,10 +6525,18 @@
"deprecationReason": null
},
{
"name": "lastSeen",
"name": "cloud",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Date", "ofType": null },
"type": { "kind": "OBJECT", "name": "CloudFields", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "endpoint",
"description": "",
"args": [],
"type": { "kind": "OBJECT", "name": "EndpointFields", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
@ -6540,14 +6548,6 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "cloud",
"description": "",
"args": [],
"type": { "kind": "OBJECT", "name": "CloudFields", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "inspect",
"description": "",
@ -6555,6 +6555,14 @@
"type": { "kind": "OBJECT", "name": "Inspect", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "lastSeen",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "Date", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@ -6659,6 +6667,65 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "EndpointFields",
"description": "",
"fields": [
{
"name": "endpointPolicy",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sensorVersion",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "policyStatus",
"description": "",
"args": [],
"type": { "kind": "ENUM", "name": "HostPolicyResponseActionStatus", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "HostPolicyResponseActionStatus",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "success",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "failure",
"description": "",
"isDeprecated": false,
"deprecationReason": null
},
{ "name": "warning", "description": "", "isDeprecated": false, "deprecationReason": null }
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "FirstLastSeenHost",

View file

@ -301,6 +301,12 @@ export enum HostsFields {
lastSeen = 'lastSeen',
}
export enum HostPolicyResponseActionStatus {
success = 'success',
failure = 'failure',
warning = 'warning',
}
export enum UsersFields {
name = 'name',
count = 'count',
@ -1442,13 +1448,15 @@ export interface HostsEdges {
export interface HostItem {
_id?: Maybe<string>;
lastSeen?: Maybe<string>;
cloud?: Maybe<CloudFields>;
endpoint?: Maybe<EndpointFields>;
host?: Maybe<HostEcsFields>;
cloud?: Maybe<CloudFields>;
inspect?: Maybe<Inspect>;
lastSeen?: Maybe<string>;
}
export interface CloudFields {
@ -1469,6 +1477,14 @@ export interface CloudMachine {
type?: Maybe<(Maybe<string>)[]>;
}
export interface EndpointFields {
endpointPolicy?: Maybe<string>;
sensorVersion?: Maybe<string>;
policyStatus?: Maybe<HostPolicyResponseActionStatus>;
}
export interface FirstLastSeenHost {
inspect?: Maybe<Inspect>;
@ -3044,6 +3060,8 @@ export namespace GetHostOverviewQuery {
cloud: Maybe<Cloud>;
inspect: Maybe<Inspect>;
endpoint: Maybe<Endpoint>;
};
export type Host = {
@ -3107,6 +3125,16 @@ export namespace GetHostOverviewQuery {
response: string[];
};
export type Endpoint = {
__typename?: 'EndpointFields';
endpointPolicy: Maybe<string>;
policyStatus: Maybe<HostPolicyResponseActionStatus>;
sensorVersion: Maybe<string>;
};
}
export namespace GetKpiHostDetailsQuery {

View file

@ -46,6 +46,11 @@ export const HostOverviewQuery = gql`
dsl
response
}
endpoint {
endpointPolicy
policyStatus
sensorVersion
}
}
}
}

View file

@ -0,0 +1,48 @@
/*
* 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 { mount } from 'enzyme';
import React from 'react';
import { TestProviders } from '../../../../common/mock';
import { EndpointOverview } from './index';
import { HostPolicyResponseActionStatus } from '../../../../graphql/types';
describe('EndpointOverview Component', () => {
test('it renders with endpoint data', () => {
const endpointData = {
endpointPolicy: 'demo',
policyStatus: HostPolicyResponseActionStatus.success,
sensorVersion: '7.9.0-SNAPSHOT',
};
const wrapper = mount(
<TestProviders>
<EndpointOverview data={endpointData} />
</TestProviders>
);
const findData = wrapper.find(
'dl[data-test-subj="endpoint-overview"] dd.euiDescriptionList__description'
);
expect(findData.at(0).text()).toEqual(endpointData.endpointPolicy);
expect(findData.at(1).text()).toEqual(endpointData.policyStatus);
expect(findData.at(2).text()).toContain(endpointData.sensorVersion); // contain because drag adds a space
});
test('it renders with null data', () => {
const wrapper = mount(
<TestProviders>
<EndpointOverview data={null} />
</TestProviders>
);
const findData = wrapper.find(
'dl[data-test-subj="endpoint-overview"] dd.euiDescriptionList__description'
);
expect(findData.at(0).text()).toEqual('—');
expect(findData.at(1).text()).toEqual('—');
expect(findData.at(2).text()).toContain('—'); // contain because drag adds a space
});
});

View file

@ -0,0 +1,90 @@
/*
* 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 { EuiFlexItem, EuiHealth } from '@elastic/eui';
import { getOr } from 'lodash/fp';
import React, { useCallback, useMemo } from 'react';
import { DescriptionList } from '../../../../../common/utility_types';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
import { DefaultFieldRenderer } from '../../../../timelines/components/field_renderers/field_renderers';
import { EndpointFields, HostPolicyResponseActionStatus } from '../../../../graphql/types';
import { DescriptionListStyled } from '../../../../common/components/page';
import * as i18n from './translations';
interface Props {
data: EndpointFields | null;
}
const getDescriptionList = (descriptionList: DescriptionList[], key: number) => (
<EuiFlexItem key={key}>
<DescriptionListStyled data-test-subj="endpoint-overview" listItems={descriptionList} />
</EuiFlexItem>
);
export const EndpointOverview = React.memo<Props>(({ data }) => {
const getDefaultRenderer = useCallback(
(fieldName: string, fieldData: EndpointFields, attrName: string) => (
<DefaultFieldRenderer
rowItems={[getOr('', fieldName, fieldData)]}
attrName={attrName}
idPrefix="endpoint-overview"
/>
),
[]
);
const descriptionLists: Readonly<DescriptionList[][]> = useMemo(
() => [
[
{
title: i18n.ENDPOINT_POLICY,
description:
data != null && data.endpointPolicy != null ? data.endpointPolicy : getEmptyTagValue(),
},
],
[
{
title: i18n.POLICY_STATUS,
description:
data != null && data.policyStatus != null ? (
<EuiHealth
aria-label={data.policyStatus}
color={
data.policyStatus === HostPolicyResponseActionStatus.failure
? 'danger'
: data.policyStatus
}
>
{data.policyStatus}
</EuiHealth>
) : (
getEmptyTagValue()
),
},
],
[
{
title: i18n.SENSORVERSION,
description:
data != null && data.sensorVersion != null
? getDefaultRenderer('sensorVersion', data, 'agent.version')
: getEmptyTagValue(),
},
],
[], // needs 4 columns for design
],
[data, getDefaultRenderer]
);
return (
<>
{descriptionLists.map((descriptionList, index) => getDescriptionList(descriptionList, index))}
</>
);
});
EndpointOverview.displayName = 'EndpointOverview';

View file

@ -0,0 +1,28 @@
/*
* 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 ENDPOINT_POLICY = i18n.translate(
'xpack.securitySolution.host.details.endpoint.endpointPolicy',
{
defaultMessage: 'Endpoint policy',
}
);
export const POLICY_STATUS = i18n.translate(
'xpack.securitySolution.host.details.endpoint.policyStatus',
{
defaultMessage: 'Policy status',
}
);
export const SENSORVERSION = i18n.translate(
'xpack.securitySolution.host.details.endpoint.sensorversion',
{
defaultMessage: 'Sensorversion',
}
);

View file

@ -11,7 +11,6 @@ import { TestProviders } from '../../../common/mock';
import { HostOverview } from './index';
import { mockData } from './mock';
import { mockAnomalies } from '../../../common/components/ml/mock';
describe('Host Summary Component', () => {
describe('rendering', () => {
test('it renders the default Host Summary', () => {

View file

@ -4,11 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexItem } from '@elastic/eui';
import { EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';
import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
import { getOr } from 'lodash/fp';
import React from 'react';
import React, { useCallback, useMemo } from 'react';
import { DEFAULT_DARK_MODE } from '../../../../common/constants';
import { DescriptionList } from '../../../../common/utility_types';
@ -33,6 +33,7 @@ import {
} from '../../../hosts/components/first_last_seen_host';
import * as i18n from './translations';
import { EndpointOverview } from './endpoint_overview';
interface HostSummaryProps {
data: HostItem;
@ -53,143 +54,183 @@ const getDescriptionList = (descriptionList: DescriptionList[], key: number) =>
export const HostOverview = React.memo<HostSummaryProps>(
({
data,
loading,
id,
startDate,
endDate,
isLoadingAnomaliesData,
anomaliesData,
data,
endDate,
id,
isLoadingAnomaliesData,
loading,
narrowDateRange,
startDate,
}) => {
const capabilities = useMlCapabilities();
const userPermissions = hasMlUserPermissions(capabilities);
const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE);
const getDefaultRenderer = (fieldName: string, fieldData: HostItem) => (
<DefaultFieldRenderer
rowItems={getOr([], fieldName, fieldData)}
attrName={fieldName}
idPrefix="host-overview"
/>
const getDefaultRenderer = useCallback(
(fieldName: string, fieldData: HostItem) => (
<DefaultFieldRenderer
rowItems={getOr([], fieldName, fieldData)}
attrName={fieldName}
idPrefix="host-overview"
/>
),
[]
);
const column: DescriptionList[] = [
{
title: i18n.HOST_ID,
description: data.host
? hostIdRenderer({ host: data.host, noLink: true })
: getEmptyTagValue(),
},
{
title: i18n.FIRST_SEEN,
description:
data.host != null && data.host.name && data.host.name.length ? (
<FirstLastSeenHost
hostname={data.host.name[0]}
type={FirstLastSeenHostType.FIRST_SEEN}
/>
) : (
getEmptyTagValue()
),
},
{
title: i18n.LAST_SEEN,
description:
data.host != null && data.host.name && data.host.name.length ? (
<FirstLastSeenHost
hostname={data.host.name[0]}
type={FirstLastSeenHostType.LAST_SEEN}
/>
) : (
getEmptyTagValue()
),
},
];
const firstColumn = userPermissions
? [
...column,
const column: DescriptionList[] = useMemo(
() => [
{
title: i18n.HOST_ID,
description: data.host
? hostIdRenderer({ host: data.host, noLink: true })
: getEmptyTagValue(),
},
{
title: i18n.FIRST_SEEN,
description:
data.host != null && data.host.name && data.host.name.length ? (
<FirstLastSeenHost
hostname={data.host.name[0]}
type={FirstLastSeenHostType.FIRST_SEEN}
/>
) : (
getEmptyTagValue()
),
},
{
title: i18n.LAST_SEEN,
description:
data.host != null && data.host.name && data.host.name.length ? (
<FirstLastSeenHost
hostname={data.host.name[0]}
type={FirstLastSeenHostType.LAST_SEEN}
/>
) : (
getEmptyTagValue()
),
},
],
[data]
);
const firstColumn = useMemo(
() =>
userPermissions
? [
...column,
{
title: i18n.MAX_ANOMALY_SCORE_BY_JOB,
description: (
<AnomalyScores
anomalies={anomaliesData}
startDate={startDate}
endDate={endDate}
isLoading={isLoadingAnomaliesData}
narrowDateRange={narrowDateRange}
/>
),
},
]
: column,
[
anomaliesData,
column,
endDate,
isLoadingAnomaliesData,
narrowDateRange,
startDate,
userPermissions,
]
);
const descriptionLists: Readonly<DescriptionList[][]> = useMemo(
() => [
firstColumn,
[
{
title: i18n.MAX_ANOMALY_SCORE_BY_JOB,
title: i18n.IP_ADDRESSES,
description: (
<AnomalyScores
anomalies={anomaliesData}
startDate={startDate}
endDate={endDate}
isLoading={isLoadingAnomaliesData}
narrowDateRange={narrowDateRange}
<DefaultFieldRenderer
rowItems={getOr([], 'host.ip', data)}
attrName={'host.ip'}
idPrefix="host-overview"
render={(ip) => (ip != null ? <IPDetailsLink ip={ip} /> : getEmptyTagValue())}
/>
),
},
]
: column;
const descriptionLists: Readonly<DescriptionList[][]> = [
firstColumn,
[
{
title: i18n.IP_ADDRESSES,
description: (
<DefaultFieldRenderer
rowItems={getOr([], 'host.ip', data)}
attrName={'host.ip'}
idPrefix="host-overview"
render={(ip) => (ip != null ? <IPDetailsLink ip={ip} /> : getEmptyTagValue())}
/>
),
},
{
title: i18n.MAC_ADDRESSES,
description: getDefaultRenderer('host.mac', data),
},
{ title: i18n.PLATFORM, description: getDefaultRenderer('host.os.platform', data) },
{
title: i18n.MAC_ADDRESSES,
description: getDefaultRenderer('host.mac', data),
},
{ title: i18n.PLATFORM, description: getDefaultRenderer('host.os.platform', data) },
],
[
{ title: i18n.OS, description: getDefaultRenderer('host.os.name', data) },
{ title: i18n.FAMILY, description: getDefaultRenderer('host.os.family', data) },
{ title: i18n.VERSION, description: getDefaultRenderer('host.os.version', data) },
{ title: i18n.ARCHITECTURE, description: getDefaultRenderer('host.architecture', data) },
],
[
{
title: i18n.CLOUD_PROVIDER,
description: getDefaultRenderer('cloud.provider', data),
},
{
title: i18n.REGION,
description: getDefaultRenderer('cloud.region', data),
},
{
title: i18n.INSTANCE_ID,
description: getDefaultRenderer('cloud.instance.id', data),
},
{
title: i18n.MACHINE_TYPE,
description: getDefaultRenderer('cloud.machine.type', data),
},
],
],
[
{ title: i18n.OS, description: getDefaultRenderer('host.os.name', data) },
{ title: i18n.FAMILY, description: getDefaultRenderer('host.os.family', data) },
{ title: i18n.VERSION, description: getDefaultRenderer('host.os.version', data) },
{ title: i18n.ARCHITECTURE, description: getDefaultRenderer('host.architecture', data) },
],
[
{
title: i18n.CLOUD_PROVIDER,
description: getDefaultRenderer('cloud.provider', data),
},
{
title: i18n.REGION,
description: getDefaultRenderer('cloud.region', data),
},
{
title: i18n.INSTANCE_ID,
description: getDefaultRenderer('cloud.instance.id', data),
},
{
title: i18n.MACHINE_TYPE,
description: getDefaultRenderer('cloud.machine.type', data),
},
],
];
[data, firstColumn, getDefaultRenderer]
);
return (
<InspectButtonContainer>
<OverviewWrapper>
<InspectButton queryId={id} title={i18n.INSPECT_TITLE} inspectIndex={0} />
<>
<InspectButtonContainer>
<OverviewWrapper>
<InspectButton queryId={id} title={i18n.INSPECT_TITLE} inspectIndex={0} />
{descriptionLists.map((descriptionList, index) =>
getDescriptionList(descriptionList, index)
)}
{descriptionLists.map((descriptionList, index) =>
getDescriptionList(descriptionList, index)
)}
{loading && (
<Loader
overlay
overlayBackground={
darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor
}
size="xl"
/>
)}
</OverviewWrapper>
</InspectButtonContainer>
{loading && (
<Loader
overlay
overlayBackground={
darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor
}
size="xl"
/>
)}
</OverviewWrapper>
</InspectButtonContainer>
{data.endpoint != null ? (
<>
<EuiHorizontalRule />
<OverviewWrapper>
<EndpointOverview data={data.endpoint} />
{loading && (
<Loader
overlay
overlayBackground={
darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor
}
size="xl"
/>
)}
</OverviewWrapper>
</>
) : null}
</>
);
}
);

View file

@ -7,8 +7,8 @@
import { IRouter, Logger, RequestHandlerContext } from 'kibana/server';
import { SearchResponse } from 'elasticsearch';
import { schema } from '@kbn/config-schema';
import Boom from 'boom';
import { metadataIndexPattern } from '../../../../common/endpoint/constants';
import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders';
import {

View file

@ -41,12 +41,25 @@ export const hostsSchema = gql`
region: [String]
}
enum HostPolicyResponseActionStatus {
success
failure
warning
}
type EndpointFields {
endpointPolicy: String
sensorVersion: String
policyStatus: HostPolicyResponseActionStatus
}
type HostItem {
_id: String
lastSeen: Date
host: HostEcsFields
cloud: CloudFields
endpoint: EndpointFields
host: HostEcsFields
inspect: Inspect
lastSeen: Date
}
type HostsEdges {

View file

@ -303,6 +303,12 @@ export enum HostsFields {
lastSeen = 'lastSeen',
}
export enum HostPolicyResponseActionStatus {
success = 'success',
failure = 'failure',
warning = 'warning',
}
export enum UsersFields {
name = 'name',
count = 'count',
@ -1444,13 +1450,15 @@ export interface HostsEdges {
export interface HostItem {
_id?: Maybe<string>;
lastSeen?: Maybe<string>;
cloud?: Maybe<CloudFields>;
endpoint?: Maybe<EndpointFields>;
host?: Maybe<HostEcsFields>;
cloud?: Maybe<CloudFields>;
inspect?: Maybe<Inspect>;
lastSeen?: Maybe<string>;
}
export interface CloudFields {
@ -1471,6 +1479,14 @@ export interface CloudMachine {
type?: Maybe<(Maybe<string>)[]>;
}
export interface EndpointFields {
endpointPolicy?: Maybe<string>;
sensorVersion?: Maybe<string>;
policyStatus?: Maybe<HostPolicyResponseActionStatus>;
}
export interface FirstLastSeenHost {
inspect?: Maybe<Inspect>;
@ -6325,13 +6341,15 @@ export namespace HostItemResolvers {
export interface Resolvers<TContext = SiemContext, TypeParent = HostItem> {
_id?: _IdResolver<Maybe<string>, TypeParent, TContext>;
lastSeen?: LastSeenResolver<Maybe<string>, TypeParent, TContext>;
cloud?: CloudResolver<Maybe<CloudFields>, TypeParent, TContext>;
endpoint?: EndpointResolver<Maybe<EndpointFields>, TypeParent, TContext>;
host?: HostResolver<Maybe<HostEcsFields>, TypeParent, TContext>;
cloud?: CloudResolver<Maybe<CloudFields>, TypeParent, TContext>;
inspect?: InspectResolver<Maybe<Inspect>, TypeParent, TContext>;
lastSeen?: LastSeenResolver<Maybe<string>, TypeParent, TContext>;
}
export type _IdResolver<R = Maybe<string>, Parent = HostItem, TContext = SiemContext> = Resolver<
@ -6339,8 +6357,13 @@ export namespace HostItemResolvers {
Parent,
TContext
>;
export type LastSeenResolver<
R = Maybe<string>,
export type CloudResolver<
R = Maybe<CloudFields>,
Parent = HostItem,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type EndpointResolver<
R = Maybe<EndpointFields>,
Parent = HostItem,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
@ -6349,13 +6372,13 @@ export namespace HostItemResolvers {
Parent = HostItem,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type CloudResolver<
R = Maybe<CloudFields>,
export type InspectResolver<
R = Maybe<Inspect>,
Parent = HostItem,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type InspectResolver<
R = Maybe<Inspect>,
export type LastSeenResolver<
R = Maybe<string>,
Parent = HostItem,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
@ -6418,6 +6441,36 @@ export namespace CloudMachineResolvers {
> = Resolver<R, Parent, TContext>;
}
export namespace EndpointFieldsResolvers {
export interface Resolvers<TContext = SiemContext, TypeParent = EndpointFields> {
endpointPolicy?: EndpointPolicyResolver<Maybe<string>, TypeParent, TContext>;
sensorVersion?: SensorVersionResolver<Maybe<string>, TypeParent, TContext>;
policyStatus?: PolicyStatusResolver<
Maybe<HostPolicyResponseActionStatus>,
TypeParent,
TContext
>;
}
export type EndpointPolicyResolver<
R = Maybe<string>,
Parent = EndpointFields,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type SensorVersionResolver<
R = Maybe<string>,
Parent = EndpointFields,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
export type PolicyStatusResolver<
R = Maybe<HostPolicyResponseActionStatus>,
Parent = EndpointFields,
TContext = SiemContext
> = Resolver<R, Parent, TContext>;
}
export namespace FirstLastSeenHostResolvers {
export interface Resolvers<TContext = SiemContext, TypeParent = FirstLastSeenHost> {
inspect?: InspectResolver<Maybe<Inspect>, TypeParent, TContext>;
@ -9331,6 +9384,7 @@ export type IResolvers<TContext = SiemContext> = {
CloudFields?: CloudFieldsResolvers.Resolvers<TContext>;
CloudInstance?: CloudInstanceResolvers.Resolvers<TContext>;
CloudMachine?: CloudMachineResolvers.Resolvers<TContext>;
EndpointFields?: EndpointFieldsResolvers.Resolvers<TContext>;
FirstLastSeenHost?: FirstLastSeenHostResolvers.Resolvers<TContext>;
IpOverviewData?: IpOverviewDataResolvers.Resolvers<TContext>;
Overview?: OverviewResolvers.Resolvers<TContext>;

View file

@ -32,11 +32,13 @@ import * as note from '../note/saved_object';
import * as pinnedEvent from '../pinned_event/saved_object';
import * as timeline from '../timeline/saved_object';
import { ElasticsearchMatrixHistogramAdapter, MatrixHistogram } from '../matrix_histogram';
import { EndpointAppContext } from '../../endpoint/types';
export function compose(
core: CoreSetup,
plugins: SetupPlugins,
isProductionMode: boolean
isProductionMode: boolean,
endpointContext: EndpointAppContext
): AppBackendLibs {
const framework = new KibanaBackendFrameworkAdapter(core, plugins, isProductionMode);
const sources = new Sources(new ConfigurationSourcesAdapter());
@ -46,7 +48,7 @@ export function compose(
authentications: new Authentications(new ElasticsearchAuthenticationAdapter(framework)),
events: new Events(new ElasticsearchEventsAdapter(framework)),
fields: new IndexFields(new ElasticsearchIndexFieldAdapter(framework)),
hosts: new Hosts(new ElasticsearchHostsAdapter(framework)),
hosts: new Hosts(new ElasticsearchHostsAdapter(framework, endpointContext)),
ipDetails: new IpDetails(new ElasticsearchIpDetailsAdapter(framework)),
tls: new TLS(new ElasticsearchTlsAdapter(framework)),
kpiHosts: new KpiHosts(new ElasticsearchKpiHostsAdapter(framework)),

View file

@ -9,6 +9,7 @@ import { FrameworkAdapter, FrameworkRequest } from '../framework';
import { ElasticsearchHostsAdapter, formatHostEdgesData } from './elasticsearch_adapter';
import {
mockEndpointMetadata,
mockGetHostOverviewOptions,
mockGetHostOverviewRequest,
mockGetHostOverviewResponse,
@ -26,6 +27,10 @@ import {
mockGetHostsQueryDsl,
} from './mock';
import { HostAggEsItem } from './types';
import { EndpointAppContext } from '../../endpoint/types';
import { mockLogger } from '../detection_engine/signals/__mocks__/es_results';
import { EndpointAppContextService } from '../../endpoint/endpoint_app_context_services';
import { createMockEndpointAppContextServiceStartContract } from '../../endpoint/mocks';
jest.mock('./query.hosts.dsl', () => {
return {
@ -44,6 +49,11 @@ jest.mock('./query.last_first_seen_host.dsl', () => {
buildLastFirstSeenHostQuery: jest.fn(() => mockGetHostLastFirstSeenDsl),
};
});
jest.mock('../../endpoint/routes/metadata', () => {
return {
getHostData: jest.fn(() => mockEndpointMetadata),
};
});
describe('hosts elasticsearch_adapter', () => {
describe('#formatHostsData', () => {
@ -155,6 +165,15 @@ describe('hosts elasticsearch_adapter', () => {
});
});
const endpointAppContextService = new EndpointAppContextService();
const startContract = createMockEndpointAppContextServiceStartContract();
endpointAppContextService.start(startContract);
const endpointContext: EndpointAppContext = {
logFactory: mockLogger,
service: endpointAppContextService,
config: jest.fn(),
};
describe('#getHosts', () => {
const mockCallWithRequest = jest.fn();
mockCallWithRequest.mockResolvedValue(mockGetHostsResponse);
@ -166,7 +185,7 @@ describe('hosts elasticsearch_adapter', () => {
jest.doMock('../framework', () => ({ callWithRequest: mockCallWithRequest }));
test('Happy Path', async () => {
const EsHosts = new ElasticsearchHostsAdapter(mockFramework);
const EsHosts = new ElasticsearchHostsAdapter(mockFramework, endpointContext);
const data: HostsData = await EsHosts.getHosts(
mockGetHostsRequest as FrameworkRequest,
mockGetHostsOptions
@ -186,7 +205,7 @@ describe('hosts elasticsearch_adapter', () => {
jest.doMock('../framework', () => ({ callWithRequest: mockCallWithRequest }));
test('Happy Path', async () => {
const EsHosts = new ElasticsearchHostsAdapter(mockFramework);
const EsHosts = new ElasticsearchHostsAdapter(mockFramework, endpointContext);
const data: HostItem = await EsHosts.getHostOverview(
mockGetHostOverviewRequest as FrameworkRequest,
mockGetHostOverviewOptions
@ -206,7 +225,7 @@ describe('hosts elasticsearch_adapter', () => {
jest.doMock('../framework', () => ({ callWithRequest: mockCallWithRequest }));
test('Happy Path', async () => {
const EsHosts = new ElasticsearchHostsAdapter(mockFramework);
const EsHosts = new ElasticsearchHostsAdapter(mockFramework, endpointContext);
const data: FirstLastSeenHost = await EsHosts.getHostFirstLastSeen(
mockGetHostLastFirstSeenRequest as FrameworkRequest,
mockGetHostLastFirstSeenOptions

View file

@ -6,12 +6,17 @@
import { get, getOr, has, head, set } from 'lodash/fp';
import { FirstLastSeenHost, HostItem, HostsData, HostsEdges } from '../../graphql/types';
import {
FirstLastSeenHost,
HostItem,
HostsData,
HostsEdges,
EndpointFields,
} from '../../graphql/types';
import { inspectStringifyObject } from '../../utils/build_query';
import { hostFieldsMap } from '../ecs_fields';
import { FrameworkAdapter, FrameworkRequest } from '../framework';
import { TermAggregation } from '../types';
import { buildHostOverviewQuery } from './query.detail_host.dsl';
import { buildHostsQuery } from './query.hosts.dsl';
import { buildLastFirstSeenHostQuery } from './query.last_first_seen_host.dsl';
@ -27,9 +32,14 @@ import {
HostValue,
} from './types';
import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants';
import { EndpointAppContext } from '../../endpoint/types';
import { getHostData } from '../../endpoint/routes/metadata';
export class ElasticsearchHostsAdapter implements HostsAdapter {
constructor(private readonly framework: FrameworkAdapter) {}
constructor(
private readonly framework: FrameworkAdapter,
private readonly endpointContext: EndpointAppContext
) {}
public async getHosts(
request: FrameworkRequest,
@ -83,8 +93,47 @@ export class ElasticsearchHostsAdapter implements HostsAdapter {
dsl: [inspectStringifyObject(dsl)],
response: [inspectStringifyObject(response)],
};
const formattedHostItem = formatHostItem(options.fields, aggregations);
const hostId =
formattedHostItem.host && formattedHostItem.host.id
? Array.isArray(formattedHostItem.host.id)
? formattedHostItem.host.id[0]
: formattedHostItem.host.id
: null;
const endpoint: EndpointFields | null = await this.getHostEndpoint(request, hostId);
return { inspect, _id: options.hostName, ...formattedHostItem, endpoint };
}
return { inspect, _id: options.hostName, ...formatHostItem(options.fields, aggregations) };
public async getHostEndpoint(
request: FrameworkRequest,
hostId: string | null
): Promise<EndpointFields | null> {
const logger = this.endpointContext.logFactory.get('metadata');
try {
const agentService = this.endpointContext.service.getAgentService();
if (agentService === undefined) {
throw new Error('agentService not available');
}
const metadataRequestContext = {
agentService,
logger,
requestHandlerContext: request.context,
};
const endpointData =
hostId != null && metadataRequestContext.agentService != null
? await getHostData(metadataRequestContext, hostId)
: null;
return endpointData != null && endpointData.metadata
? {
endpointPolicy: endpointData.metadata.Endpoint.policy.applied.name,
policyStatus: endpointData.metadata.Endpoint.policy.applied.status,
sensorVersion: endpointData.metadata.agent.version,
}
: null;
} catch (err) {
logger.warn(JSON.stringify(err, null, 2));
return null;
}
}
public async getHostFirstLastSeen(

View file

@ -497,6 +497,11 @@ export const mockGetHostOverviewResult = {
provider: ['gce'],
region: ['us-east-1'],
},
endpoint: {
endpointPolicy: 'demo',
policyStatus: 'success',
sensorVersion: '7.9.0-SNAPSHOT',
},
};
export const mockGetHostLastFirstSeenOptions: HostLastFirstSeenRequestOptions = {
@ -564,3 +569,64 @@ export const mockGetHostLastFirstSeenResult = {
firstSeen: '2019-02-22T03:41:32.826Z',
lastSeen: '2019-04-09T16:18:12.178Z',
};
export const mockEndpointMetadata = {
metadata: {
'@timestamp': '2020-07-13T01:08:37.68896700Z',
Endpoint: {
policy: {
applied: { id: '3de86380-aa5a-11ea-b969-0bee1b260ab8', name: 'demo', status: 'success' },
},
status: 'enrolled',
},
agent: {
build: {
original:
'version: 7.9.0-SNAPSHOT, compiled: Thu Jul 09 07:56:12 2020, branch: 7.x, commit: 713a1071de475f15b3a1f0944d3602ed532597a5',
},
id: 'c29e0de1-7476-480b-b242-38f0394bf6a1',
type: 'endpoint',
version: '7.9.0-SNAPSHOT',
},
dataset: { name: 'endpoint.metadata', namespace: 'default', type: 'metrics' },
ecs: { version: '1.5.0' },
elastic: { agent: { id: '' } },
event: {
action: 'endpoint_metadata',
category: ['host'],
created: '2020-07-13T01:08:37.68896700Z',
dataset: 'endpoint.metadata',
id: 'Lkio+AHbZGSPFb7q++++++2E',
kind: 'metric',
module: 'endpoint',
sequence: 146,
type: ['info'],
},
host: {
architecture: 'x86_64',
hostname: 'DESKTOP-4I1B23J',
id: 'a4148b63-1758-ab1f-a6d3-f95075cb1a9c',
ip: [
'172.16.166.129',
'fe80::c07e:eee9:3e8d:ea6d',
'169.254.205.96',
'fe80::1027:b13d:a4a7:cd60',
'127.0.0.1',
'::1',
],
mac: ['00:0c:29:89:ff:73', '3c:22:fb:3c:93:4c'],
name: 'DESKTOP-4I1B23J',
os: {
Ext: { variant: 'Windows 10 Pro' },
family: 'windows',
full: 'Windows 10 Pro 2004 (10.0.19041.329)',
kernel: '2004 (10.0.19041.329)',
name: 'Windows',
platform: 'windows',
version: '2004 (10.0.19041.329)',
},
},
message: 'Endpoint metadata',
},
host_status: 'error',
};

View file

@ -48,6 +48,7 @@ import { EndpointAppContextService } from './endpoint/endpoint_app_context_servi
import { EndpointAppContext } from './endpoint/types';
import { registerDownloadExceptionListRoute } from './endpoint/routes/artifacts';
import { initUsageCollectors } from './usage';
import { AppRequestContext } from './types';
export interface SetupPlugins {
alerts: AlertingSetup;
@ -127,9 +128,12 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
};
const router = core.http.createRouter();
core.http.registerRouteHandlerContext(APP_ID, (context, request, response) => ({
getAppClient: () => this.appClientFactory.create(request),
}));
core.http.registerRouteHandlerContext(
APP_ID,
(context, request, response): AppRequestContext => ({
getAppClient: () => this.appClientFactory.create(request),
})
);
this.appClientFactory.setup({
getSpaceId: plugins.spaces?.spacesService?.getSpaceId,
@ -144,7 +148,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
plugins.security,
plugins.ml
);
registerEndpointRoutes(router, endpointContext);
registerResolverRoutes(router, endpointContext);
registerPolicyRoutes(router, endpointContext);
@ -249,7 +252,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
});
}
const libs = compose(core, plugins, this.context.env.mode.prod);
const libs = compose(core, plugins, this.context.env.mode.prod, endpointContext);
initServer(libs);
return {};

View file

@ -105,6 +105,7 @@ export default function ({ getService }: FtrProviderContext) {
it('Make sure that we get Host Overview data', () => {
const expectedHost: Omit<GetHostOverviewQuery.HostOverview, 'inspect'> = {
_id: 'zeek-sensor-san-francisco',
endpoint: null,
host: {
architecture: ['x86_64'],
id: [CURSOR_ID],