[Security Solution] Give notice when endpoint policy is out of date (#83469)
This commit is contained in:
parent
a11f70f9bb
commit
2cd2528ac8
|
@ -13,7 +13,13 @@ import {
|
|||
} from '../common';
|
||||
|
||||
export { default as apm } from 'elastic-apm-node';
|
||||
export { AgentService, ESIndexPatternService, getRegistryUrl, PackageService } from './services';
|
||||
export {
|
||||
AgentService,
|
||||
ESIndexPatternService,
|
||||
getRegistryUrl,
|
||||
PackageService,
|
||||
AgentPolicyServiceInterface,
|
||||
} from './services';
|
||||
export { FleetSetupContract, FleetSetupDeps, FleetStartContract, ExternalCallback } from './plugin';
|
||||
|
||||
export const config: PluginConfigDescriptor = {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { FleetAppContext } from './plugin';
|
|||
import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks';
|
||||
import { securityMock } from '../../security/server/mocks';
|
||||
import { PackagePolicyServiceInterface } from './services/package_policy';
|
||||
import { AgentPolicyServiceInterface, AgentService } from './services';
|
||||
|
||||
export const createAppContextStartContractMock = (): FleetAppContext => {
|
||||
return {
|
||||
|
@ -35,3 +36,28 @@ export const createPackagePolicyServiceMock = () => {
|
|||
update: jest.fn(),
|
||||
} as jest.Mocked<PackagePolicyServiceInterface>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create mock AgentPolicyService
|
||||
*/
|
||||
|
||||
export const createMockAgentPolicyService = (): jest.Mocked<AgentPolicyServiceInterface> => {
|
||||
return {
|
||||
get: jest.fn(),
|
||||
list: jest.fn(),
|
||||
getDefaultAgentPolicyId: jest.fn(),
|
||||
getFullAgentPolicy: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock AgentService
|
||||
*/
|
||||
export const createMockAgentService = (): jest.Mocked<AgentService> => {
|
||||
return {
|
||||
getAgentStatusById: jest.fn(),
|
||||
authenticateAgentWithAccessToken: jest.fn(),
|
||||
getAgent: jest.fn(),
|
||||
listAgents: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -58,6 +58,8 @@ import {
|
|||
ESIndexPatternSavedObjectService,
|
||||
ESIndexPatternService,
|
||||
AgentService,
|
||||
AgentPolicyServiceInterface,
|
||||
agentPolicyService,
|
||||
packagePolicyService,
|
||||
PackageService,
|
||||
} from './services';
|
||||
|
@ -134,6 +136,7 @@ export interface FleetStartContract {
|
|||
* Services for Fleet's package policies
|
||||
*/
|
||||
packagePolicyService: typeof packagePolicyService;
|
||||
agentPolicyService: AgentPolicyServiceInterface;
|
||||
/**
|
||||
* Register callbacks for inclusion in fleet API processing
|
||||
* @param args
|
||||
|
@ -292,6 +295,12 @@ export class FleetPlugin
|
|||
getAgentStatusById,
|
||||
authenticateAgentWithAccessToken,
|
||||
},
|
||||
agentPolicyService: {
|
||||
get: agentPolicyService.get,
|
||||
list: agentPolicyService.list,
|
||||
getDefaultAgentPolicyId: agentPolicyService.getDefaultAgentPolicyId,
|
||||
getFullAgentPolicy: agentPolicyService.getFullAgentPolicy,
|
||||
},
|
||||
packagePolicyService,
|
||||
registerExternalCallback: (...args: ExternalCallback) => {
|
||||
return appContextService.addExternalCallback(...args);
|
||||
|
|
|
@ -9,6 +9,7 @@ import { AgentStatus, Agent, EsAssetReference } from '../types';
|
|||
import * as settingsService from './settings';
|
||||
import { getAgent, listAgents } from './agents';
|
||||
export { ESIndexPatternSavedObjectService } from './es_index_pattern';
|
||||
import { agentPolicyService } from './agent_policy';
|
||||
|
||||
export { getRegistryUrl } from './epm/registry/registry_url';
|
||||
|
||||
|
@ -59,6 +60,13 @@ export interface AgentService {
|
|||
listAgents: typeof listAgents;
|
||||
}
|
||||
|
||||
export interface AgentPolicyServiceInterface {
|
||||
get: typeof agentPolicyService['get'];
|
||||
list: typeof agentPolicyService['list'];
|
||||
getDefaultAgentPolicyId: typeof agentPolicyService['getDefaultAgentPolicyId'];
|
||||
getFullAgentPolicy: typeof agentPolicyService['getFullAgentPolicy'];
|
||||
}
|
||||
|
||||
// Saved object services
|
||||
export { agentPolicyService } from './agent_policy';
|
||||
export { packagePolicyService } from './package_policy';
|
||||
|
|
|
@ -118,21 +118,29 @@ const APPLIED_POLICIES: Array<{
|
|||
name: string;
|
||||
id: string;
|
||||
status: HostPolicyResponseActionStatus;
|
||||
endpoint_policy_version: number;
|
||||
version: number;
|
||||
}> = [
|
||||
{
|
||||
name: 'Default',
|
||||
id: '00000000-0000-0000-0000-000000000000',
|
||||
status: HostPolicyResponseActionStatus.success,
|
||||
endpoint_policy_version: 1,
|
||||
version: 3,
|
||||
},
|
||||
{
|
||||
name: 'With Eventing',
|
||||
id: 'C2A9093E-E289-4C0A-AA44-8C32A414FA7A',
|
||||
status: HostPolicyResponseActionStatus.success,
|
||||
endpoint_policy_version: 3,
|
||||
version: 5,
|
||||
},
|
||||
{
|
||||
name: 'Detect Malware Only',
|
||||
id: '47d7965d-6869-478b-bd9c-fb0d2bb3959f',
|
||||
status: HostPolicyResponseActionStatus.success,
|
||||
endpoint_policy_version: 4,
|
||||
version: 9,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -251,6 +259,8 @@ interface HostInfo {
|
|||
id: string;
|
||||
status: HostPolicyResponseActionStatus;
|
||||
name: string;
|
||||
endpoint_policy_version: number;
|
||||
version: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -1332,7 +1342,7 @@ export class EndpointDocGenerator {
|
|||
allStatus?: HostPolicyResponseActionStatus;
|
||||
policyDataStream?: DataStream;
|
||||
} = {}): HostPolicyResponse {
|
||||
const policyVersion = this.seededUUIDv4();
|
||||
const policyVersion = this.randomN(10);
|
||||
const status = () => {
|
||||
return allStatus || this.randomHostPolicyResponseActionStatus();
|
||||
};
|
||||
|
@ -1501,6 +1511,8 @@ export class EndpointDocGenerator {
|
|||
status: this.commonInfo.Endpoint.policy.applied.status,
|
||||
version: policyVersion,
|
||||
name: this.commonInfo.Endpoint.policy.applied.name,
|
||||
endpoint_policy_version: this.commonInfo.Endpoint.policy.applied
|
||||
.endpoint_policy_version,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -299,6 +299,8 @@ export interface HostResultList {
|
|||
request_page_index: number;
|
||||
/* the version of the query strategy */
|
||||
query_strategy_version: MetadataQueryStrategyVersions;
|
||||
/* policy IDs and versions */
|
||||
policy_info?: HostInfo['policy_info'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -520,9 +522,30 @@ export enum MetadataQueryStrategyVersions {
|
|||
VERSION_2 = 'v2',
|
||||
}
|
||||
|
||||
export type PolicyInfo = Immutable<{
|
||||
revision: number;
|
||||
id: string;
|
||||
}>;
|
||||
|
||||
export type HostInfo = Immutable<{
|
||||
metadata: HostMetadata;
|
||||
host_status: HostStatus;
|
||||
policy_info?: {
|
||||
agent: {
|
||||
/**
|
||||
* As set in Kibana
|
||||
*/
|
||||
configured: PolicyInfo;
|
||||
/**
|
||||
* Last reported running in agent (may lag behind configured)
|
||||
*/
|
||||
applied: PolicyInfo;
|
||||
};
|
||||
/**
|
||||
* Current intended 'endpoint' package policy
|
||||
*/
|
||||
endpoint: PolicyInfo;
|
||||
};
|
||||
/* the version of the query strategy */
|
||||
query_strategy_version: MetadataQueryStrategyVersions;
|
||||
}>;
|
||||
|
@ -558,6 +581,8 @@ export type HostMetadata = Immutable<{
|
|||
id: string;
|
||||
status: HostPolicyResponseActionStatus;
|
||||
name: string;
|
||||
endpoint_policy_version: number;
|
||||
version: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -1068,7 +1093,8 @@ export interface HostPolicyResponse {
|
|||
Endpoint: {
|
||||
policy: {
|
||||
applied: {
|
||||
version: string;
|
||||
version: number;
|
||||
endpoint_policy_version: number;
|
||||
id: string;
|
||||
name: string;
|
||||
status: HostPolicyResponseActionStatus;
|
||||
|
|
|
@ -63,6 +63,7 @@ describe('EndpointList store concerns', () => {
|
|||
agentsWithEndpointsTotalError: undefined,
|
||||
endpointsTotalError: undefined,
|
||||
queryStrategyVersion: undefined,
|
||||
policyVersionInfo: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ export const initialEndpointListState: Immutable<EndpointState> = {
|
|||
endpointsTotal: 0,
|
||||
endpointsTotalError: undefined,
|
||||
queryStrategyVersion: undefined,
|
||||
policyVersionInfo: undefined,
|
||||
};
|
||||
|
||||
/* eslint-disable-next-line complexity */
|
||||
|
@ -55,6 +56,7 @@ export const endpointListReducer: ImmutableReducer<EndpointState, AppAction> = (
|
|||
request_page_size: pageSize,
|
||||
request_page_index: pageIndex,
|
||||
query_strategy_version: queryStrategyVersion,
|
||||
policy_info: policyVersionInfo,
|
||||
} = action.payload;
|
||||
return {
|
||||
...state,
|
||||
|
@ -63,6 +65,7 @@ export const endpointListReducer: ImmutableReducer<EndpointState, AppAction> = (
|
|||
pageSize,
|
||||
pageIndex,
|
||||
queryStrategyVersion,
|
||||
policyVersionInfo,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
};
|
||||
|
@ -104,6 +107,7 @@ export const endpointListReducer: ImmutableReducer<EndpointState, AppAction> = (
|
|||
return {
|
||||
...state,
|
||||
details: action.payload.metadata,
|
||||
policyVersionInfo: action.payload.policy_info,
|
||||
detailsLoading: false,
|
||||
detailsError: undefined,
|
||||
};
|
||||
|
|
|
@ -55,6 +55,8 @@ export const isAutoRefreshEnabled = (state: Immutable<EndpointState>) => state.i
|
|||
|
||||
export const autoRefreshInterval = (state: Immutable<EndpointState>) => state.autoRefreshInterval;
|
||||
|
||||
export const policyVersionInfo = (state: Immutable<EndpointState>) => state.policyVersionInfo;
|
||||
|
||||
export const areEndpointsEnrolling = (state: Immutable<EndpointState>) => {
|
||||
return state.agentsWithEndpointsTotal > state.endpointsTotal;
|
||||
};
|
||||
|
|
|
@ -76,6 +76,8 @@ export interface EndpointState {
|
|||
endpointsTotalError?: ServerApiError;
|
||||
/** The query strategy version that informs whether the transform for KQL is enabled or not */
|
||||
queryStrategyVersion?: MetadataQueryStrategyVersions;
|
||||
/** The policy IDs and revision number of the corresponding agent, and endpoint. May be more recent than what's running */
|
||||
policyVersionInfo?: HostInfo['policy_info'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { HostInfo, HostMetadata } from '../../../../common/endpoint/types';
|
||||
|
||||
export const isPolicyOutOfDate = (
|
||||
reported: HostMetadata['Endpoint']['policy']['applied'],
|
||||
current: HostInfo['policy_info']
|
||||
): boolean => {
|
||||
if (current === undefined || current === null) {
|
||||
return false; // we don't know, can't declare it out-of-date
|
||||
}
|
||||
return !(
|
||||
reported.id === current.endpoint.id && // endpoint package policy not reassigned
|
||||
current.agent.configured.id === current.agent.applied.id && // agent policy wasn't reassigned and not-yet-applied
|
||||
// all revisions match up
|
||||
reported.version >= current.agent.applied.revision &&
|
||||
reported.version >= current.agent.configured.revision &&
|
||||
reported.endpoint_policy_version >= current.endpoint.revision
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiText, EuiIcon } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
export const OutOfDate = React.memo<{ style?: React.CSSProperties }>(({ style, ...otherProps }) => {
|
||||
return (
|
||||
<EuiText color="subdued" size="xs" className="eui-textNoWrap" style={style} {...otherProps}>
|
||||
<EuiIcon size="m" type="alert" color="warning" />
|
||||
<FormattedMessage id="xpack.securitySolution.outOfDateLabel" defaultMessage="Out-of-date" />
|
||||
</EuiText>
|
||||
);
|
||||
});
|
||||
|
||||
OutOfDate.displayName = 'OutOfDate';
|
|
@ -18,7 +18,8 @@ import {
|
|||
import React, { memo, useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { HostMetadata } from '../../../../../../common/endpoint/types';
|
||||
import { isPolicyOutOfDate } from '../../utils';
|
||||
import { HostInfo, HostMetadata } from '../../../../../../common/endpoint/types';
|
||||
import { useEndpointSelector, useAgentDetailsIngestUrl } from '../hooks';
|
||||
import { useNavigateToAppEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
|
||||
import { policyResponseStatus, uiQueryParams } from '../../store/selectors';
|
||||
|
@ -31,6 +32,7 @@ import { SecurityPageName } from '../../../../../app/types';
|
|||
import { useFormatUrl } from '../../../../../common/components/link_to';
|
||||
import { AgentDetailsReassignPolicyAction } from '../../../../../../../fleet/public';
|
||||
import { EndpointPolicyLink } from '../components/endpoint_policy_link';
|
||||
import { OutOfDate } from '../components/out_of_date';
|
||||
|
||||
const HostIds = styled(EuiListGroupItem)`
|
||||
margin-top: 0;
|
||||
|
@ -51,187 +53,190 @@ const LinkToExternalApp = styled.div`
|
|||
|
||||
const openReassignFlyoutSearch = '?openReassignFlyout=true';
|
||||
|
||||
export const EndpointDetails = memo(({ details }: { details: HostMetadata }) => {
|
||||
const agentId = details.elastic.agent.id;
|
||||
const {
|
||||
url: agentDetailsUrl,
|
||||
appId: ingestAppId,
|
||||
appPath: agentDetailsAppPath,
|
||||
} = useAgentDetailsIngestUrl(agentId);
|
||||
const queryParams = useEndpointSelector(uiQueryParams);
|
||||
const policyStatus = useEndpointSelector(
|
||||
policyResponseStatus
|
||||
) as keyof typeof POLICY_STATUS_TO_HEALTH_COLOR;
|
||||
const { formatUrl } = useFormatUrl(SecurityPageName.administration);
|
||||
export const EndpointDetails = memo(
|
||||
({ details, policyInfo }: { details: HostMetadata; policyInfo?: HostInfo['policy_info'] }) => {
|
||||
const agentId = details.elastic.agent.id;
|
||||
const {
|
||||
url: agentDetailsUrl,
|
||||
appId: ingestAppId,
|
||||
appPath: agentDetailsAppPath,
|
||||
} = useAgentDetailsIngestUrl(agentId);
|
||||
const queryParams = useEndpointSelector(uiQueryParams);
|
||||
const policyStatus = useEndpointSelector(
|
||||
policyResponseStatus
|
||||
) as keyof typeof POLICY_STATUS_TO_HEALTH_COLOR;
|
||||
const { formatUrl } = useFormatUrl(SecurityPageName.administration);
|
||||
|
||||
const detailsResultsUpper = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.details.os', {
|
||||
defaultMessage: 'OS',
|
||||
}),
|
||||
description: details.host.os.full,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.details.lastSeen', {
|
||||
defaultMessage: 'Last Seen',
|
||||
}),
|
||||
description: <FormattedDateAndTime date={new Date(details['@timestamp'])} />,
|
||||
},
|
||||
];
|
||||
}, [details]);
|
||||
const detailsResultsUpper = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.details.os', {
|
||||
defaultMessage: 'OS',
|
||||
}),
|
||||
description: details.host.os.full,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.details.lastSeen', {
|
||||
defaultMessage: 'Last Seen',
|
||||
}),
|
||||
description: <FormattedDateAndTime date={new Date(details['@timestamp'])} />,
|
||||
},
|
||||
];
|
||||
}, [details]);
|
||||
|
||||
const [policyResponseUri, policyResponseRoutePath] = useMemo(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { selected_endpoint, show, ...currentUrlParams } = queryParams;
|
||||
return [
|
||||
formatUrl(
|
||||
const [policyResponseUri, policyResponseRoutePath] = useMemo(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { selected_endpoint, show, ...currentUrlParams } = queryParams;
|
||||
return [
|
||||
formatUrl(
|
||||
getEndpointDetailsPath({
|
||||
name: 'endpointPolicyResponse',
|
||||
...currentUrlParams,
|
||||
selected_endpoint: details.agent.id,
|
||||
})
|
||||
),
|
||||
getEndpointDetailsPath({
|
||||
name: 'endpointPolicyResponse',
|
||||
...currentUrlParams,
|
||||
selected_endpoint: details.agent.id,
|
||||
})
|
||||
),
|
||||
getEndpointDetailsPath({
|
||||
name: 'endpointPolicyResponse',
|
||||
...currentUrlParams,
|
||||
selected_endpoint: details.agent.id,
|
||||
}),
|
||||
];
|
||||
}, [details.agent.id, formatUrl, queryParams]);
|
||||
}),
|
||||
];
|
||||
}, [details.agent.id, formatUrl, queryParams]);
|
||||
|
||||
const agentDetailsWithFlyoutPath = `${agentDetailsAppPath}${openReassignFlyoutSearch}`;
|
||||
const agentDetailsWithFlyoutUrl = `${agentDetailsUrl}${openReassignFlyoutSearch}`;
|
||||
const handleReassignEndpointsClick = useNavigateToAppEventHandler<
|
||||
AgentDetailsReassignPolicyAction
|
||||
>(ingestAppId, {
|
||||
path: agentDetailsWithFlyoutPath,
|
||||
state: {
|
||||
onDoneNavigateTo: [
|
||||
'securitySolution:administration',
|
||||
const agentDetailsWithFlyoutPath = `${agentDetailsAppPath}${openReassignFlyoutSearch}`;
|
||||
const agentDetailsWithFlyoutUrl = `${agentDetailsUrl}${openReassignFlyoutSearch}`;
|
||||
const handleReassignEndpointsClick = useNavigateToAppEventHandler<
|
||||
AgentDetailsReassignPolicyAction
|
||||
>(ingestAppId, {
|
||||
path: agentDetailsWithFlyoutPath,
|
||||
state: {
|
||||
onDoneNavigateTo: [
|
||||
'securitySolution:administration',
|
||||
{
|
||||
path: getEndpointDetailsPath({
|
||||
name: 'endpointDetails',
|
||||
selected_endpoint: details.agent.id,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath);
|
||||
|
||||
const detailsResultsPolicy = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
path: getEndpointDetailsPath({
|
||||
name: 'endpointDetails',
|
||||
selected_endpoint: details.agent.id,
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.details.policy', {
|
||||
defaultMessage: 'Integration Policy',
|
||||
}),
|
||||
description: (
|
||||
<>
|
||||
<EndpointPolicyLink
|
||||
policyId={details.Endpoint.policy.applied.id}
|
||||
data-test-subj="policyDetailsValue"
|
||||
>
|
||||
{details.Endpoint.policy.applied.name}
|
||||
</EndpointPolicyLink>
|
||||
{isPolicyOutOfDate(details.Endpoint.policy.applied, policyInfo) && <OutOfDate />}
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath);
|
||||
|
||||
const detailsResultsPolicy = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.details.policy', {
|
||||
defaultMessage: 'Integration Policy',
|
||||
}),
|
||||
description: (
|
||||
<>
|
||||
<EndpointPolicyLink
|
||||
policyId={details.Endpoint.policy.applied.id}
|
||||
data-test-subj="policyDetailsValue"
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.details.policyStatus', {
|
||||
defaultMessage: 'Policy Response',
|
||||
}),
|
||||
description: (
|
||||
<EuiHealth
|
||||
color={POLICY_STATUS_TO_HEALTH_COLOR[policyStatus] || 'subdued'}
|
||||
data-test-subj="policyStatusHealth"
|
||||
>
|
||||
{details.Endpoint.policy.applied.name}
|
||||
</EndpointPolicyLink>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.details.policyStatus', {
|
||||
defaultMessage: 'Policy Response',
|
||||
}),
|
||||
description: (
|
||||
<EuiHealth
|
||||
color={POLICY_STATUS_TO_HEALTH_COLOR[policyStatus] || 'subdued'}
|
||||
data-test-subj="policyStatusHealth"
|
||||
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
|
||||
<EuiLink
|
||||
data-test-subj="policyStatusValue"
|
||||
href={policyResponseUri}
|
||||
onClick={policyStatusClickHandler}
|
||||
>
|
||||
<EuiText size="m">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.details.policyStatusValue"
|
||||
defaultMessage="{policyStatus, select, success {Success} warning {Warning} failure {Failed} other {Unknown}}"
|
||||
values={{ policyStatus }}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiLink>
|
||||
</EuiHealth>
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [details, policyResponseUri, policyStatus, policyStatusClickHandler, policyInfo]);
|
||||
const detailsResultsLower = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.details.ipAddress', {
|
||||
defaultMessage: 'IP Address',
|
||||
}),
|
||||
description: (
|
||||
<EuiListGroup flush>
|
||||
{details.host.ip.map((ip: string, index: number) => (
|
||||
<HostIds key={index} label={ip} />
|
||||
))}
|
||||
</EuiListGroup>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.details.hostname', {
|
||||
defaultMessage: 'Hostname',
|
||||
}),
|
||||
description: details.host.hostname,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.details.endpointVersion', {
|
||||
defaultMessage: 'Endpoint Version',
|
||||
}),
|
||||
description: details.agent.version,
|
||||
},
|
||||
];
|
||||
}, [details.agent.version, details.host.hostname, details.host.ip]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiDescriptionList
|
||||
type="column"
|
||||
listItems={detailsResultsUpper}
|
||||
data-test-subj="endpointDetailsUpperList"
|
||||
/>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<EuiDescriptionList
|
||||
type="column"
|
||||
listItems={detailsResultsPolicy}
|
||||
data-test-subj="endpointDetailsPolicyList"
|
||||
/>
|
||||
<LinkToExternalApp>
|
||||
<LinkToApp
|
||||
appId={ingestAppId}
|
||||
appPath={agentDetailsWithFlyoutPath}
|
||||
href={agentDetailsWithFlyoutUrl}
|
||||
onClick={handleReassignEndpointsClick}
|
||||
data-test-subj="endpointDetailsLinkToIngest"
|
||||
>
|
||||
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
|
||||
<EuiLink
|
||||
data-test-subj="policyStatusValue"
|
||||
href={policyResponseUri}
|
||||
onClick={policyStatusClickHandler}
|
||||
>
|
||||
<EuiText size="m">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.details.policyStatusValue"
|
||||
defaultMessage="{policyStatus, select, success {Success} warning {Warning} failure {Failed} other {Unknown}}"
|
||||
values={{ policyStatus }}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiLink>
|
||||
</EuiHealth>
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [details, policyResponseUri, policyStatus, policyStatusClickHandler]);
|
||||
const detailsResultsLower = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.details.ipAddress', {
|
||||
defaultMessage: 'IP Address',
|
||||
}),
|
||||
description: (
|
||||
<EuiListGroup flush>
|
||||
{details.host.ip.map((ip: string, index: number) => (
|
||||
<HostIds key={index} label={ip} />
|
||||
))}
|
||||
</EuiListGroup>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.details.hostname', {
|
||||
defaultMessage: 'Hostname',
|
||||
}),
|
||||
description: details.host.hostname,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.details.endpointVersion', {
|
||||
defaultMessage: 'Endpoint Version',
|
||||
}),
|
||||
description: details.agent.version,
|
||||
},
|
||||
];
|
||||
}, [details.agent.version, details.host.hostname, details.host.ip]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiDescriptionList
|
||||
type="column"
|
||||
listItems={detailsResultsUpper}
|
||||
data-test-subj="endpointDetailsUpperList"
|
||||
/>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<EuiDescriptionList
|
||||
type="column"
|
||||
listItems={detailsResultsPolicy}
|
||||
data-test-subj="endpointDetailsPolicyList"
|
||||
/>
|
||||
<LinkToExternalApp>
|
||||
<LinkToApp
|
||||
appId={ingestAppId}
|
||||
appPath={agentDetailsWithFlyoutPath}
|
||||
href={agentDetailsWithFlyoutUrl}
|
||||
onClick={handleReassignEndpointsClick}
|
||||
data-test-subj="endpointDetailsLinkToIngest"
|
||||
>
|
||||
<EuiIcon type="savedObjectsApp" className="linkToAppIcon" />
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.details.linkToIngestTitle"
|
||||
defaultMessage="Reassign Policy"
|
||||
/>
|
||||
<EuiIcon type="popout" className="linkToAppPopoutIcon" />
|
||||
</LinkToApp>
|
||||
</LinkToExternalApp>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<EuiDescriptionList
|
||||
type="column"
|
||||
listItems={detailsResultsLower}
|
||||
data-test-subj="endpointDetailsLowerList"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
<EuiIcon type="savedObjectsApp" className="linkToAppIcon" />
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.details.linkToIngestTitle"
|
||||
defaultMessage="Reassign Policy"
|
||||
/>
|
||||
<EuiIcon type="popout" className="linkToAppPopoutIcon" />
|
||||
</LinkToApp>
|
||||
</LinkToExternalApp>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<EuiDescriptionList
|
||||
type="column"
|
||||
listItems={detailsResultsLower}
|
||||
data-test-subj="endpointDetailsLowerList"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
EndpointDetails.displayName = 'EndpointDetails';
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
policyResponseError,
|
||||
policyResponseLoading,
|
||||
policyResponseTimestamp,
|
||||
policyVersionInfo,
|
||||
} from '../../store/selectors';
|
||||
import { EndpointDetails } from './endpoint_details';
|
||||
import { PolicyResponse } from './policy_response';
|
||||
|
@ -53,6 +54,7 @@ export const EndpointDetailsFlyout = memo(() => {
|
|||
...queryParamsWithoutSelectedEndpoint
|
||||
} = queryParams;
|
||||
const details = useEndpointSelector(detailsData);
|
||||
const policyInfo = useEndpointSelector(policyVersionInfo);
|
||||
const loading = useEndpointSelector(detailsLoading);
|
||||
const error = useEndpointSelector(detailsError);
|
||||
const show = useEndpointSelector(showView);
|
||||
|
@ -101,7 +103,7 @@ export const EndpointDetailsFlyout = memo(() => {
|
|||
{show === 'details' && (
|
||||
<>
|
||||
<EuiFlyoutBody data-test-subj="endpointDetailsFlyoutBody">
|
||||
<EndpointDetails details={details} />
|
||||
<EndpointDetails details={details} policyInfo={policyInfo} />
|
||||
</EuiFlyoutBody>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -228,15 +228,58 @@ describe('when on the list page', () => {
|
|||
|
||||
firstPolicyID = hostListData[0].metadata.Endpoint.policy.applied.id;
|
||||
|
||||
[HostStatus.ERROR, HostStatus.ONLINE, HostStatus.OFFLINE, HostStatus.UNENROLLING].forEach(
|
||||
(status, index) => {
|
||||
hostListData[index] = {
|
||||
metadata: hostListData[index].metadata,
|
||||
host_status: status,
|
||||
query_strategy_version: queryStrategyVersion,
|
||||
};
|
||||
}
|
||||
);
|
||||
// add ability to change (immutable) policy
|
||||
type DeepMutable<T> = { -readonly [P in keyof T]: DeepMutable<T[P]> };
|
||||
type Policy = DeepMutable<NonNullable<HostInfo['policy_info']>>;
|
||||
|
||||
const makePolicy = (
|
||||
applied: HostInfo['metadata']['Endpoint']['policy']['applied'],
|
||||
cb: (policy: Policy) => Policy
|
||||
): Policy => {
|
||||
return cb({
|
||||
agent: {
|
||||
applied: { id: 'xyz', revision: applied.version },
|
||||
configured: { id: 'xyz', revision: applied.version },
|
||||
},
|
||||
endpoint: { id: applied.id, revision: applied.endpoint_policy_version },
|
||||
});
|
||||
};
|
||||
|
||||
[
|
||||
{ status: HostStatus.ERROR, policy: (p: Policy) => p },
|
||||
{
|
||||
status: HostStatus.ONLINE,
|
||||
policy: (p: Policy) => {
|
||||
p.endpoint.id = 'xyz'; // represents change in endpoint policy assignment
|
||||
p.endpoint.revision = 1;
|
||||
return p;
|
||||
},
|
||||
},
|
||||
{
|
||||
status: HostStatus.OFFLINE,
|
||||
policy: (p: Policy) => {
|
||||
p.endpoint.revision += 1; // changes made to endpoint policy
|
||||
return p;
|
||||
},
|
||||
},
|
||||
{
|
||||
status: HostStatus.UNENROLLING,
|
||||
policy: (p: Policy) => {
|
||||
p.agent.configured.revision += 1; // agent policy change, not propagated to agent yet
|
||||
return p;
|
||||
},
|
||||
},
|
||||
].forEach((setup, index) => {
|
||||
hostListData[index] = {
|
||||
metadata: hostListData[index].metadata,
|
||||
host_status: setup.status,
|
||||
policy_info: makePolicy(
|
||||
hostListData[index].metadata.Endpoint.policy.applied,
|
||||
setup.policy
|
||||
),
|
||||
query_strategy_version: queryStrategyVersion,
|
||||
};
|
||||
});
|
||||
hostListData.forEach((item, index) => {
|
||||
generatedPolicyStatuses[index] = item.metadata.Endpoint.policy.applied.status;
|
||||
});
|
||||
|
@ -316,6 +359,20 @@ describe('when on the list page', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should display policy out-of-date warning when changes pending', async () => {
|
||||
const renderResult = render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointList');
|
||||
});
|
||||
const outOfDates = await renderResult.findAllByTestId('rowPolicyOutOfDate');
|
||||
expect(outOfDates).toHaveLength(3);
|
||||
|
||||
outOfDates.forEach((item, index) => {
|
||||
expect(item.textContent).toEqual('Out-of-date');
|
||||
expect(item.querySelector(`[data-euiicon-type][color=warning]`)).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display policy name as a link', async () => {
|
||||
const renderResult = render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
|
|
|
@ -35,6 +35,7 @@ import { NavigateToAppOptions } from 'kibana/public';
|
|||
import { EndpointDetailsFlyout } from './details';
|
||||
import * as selectors from '../store/selectors';
|
||||
import { useEndpointSelector } from './hooks';
|
||||
import { isPolicyOutOfDate } from '../utils';
|
||||
import {
|
||||
HOST_STATUS_TO_HEALTH_COLOR,
|
||||
POLICY_STATUS_TO_HEALTH_COLOR,
|
||||
|
@ -57,6 +58,7 @@ import { getEndpointListPath, getEndpointDetailsPath } from '../../../common/rou
|
|||
import { useFormatUrl } from '../../../../common/components/link_to';
|
||||
import { EndpointAction } from '../store/action';
|
||||
import { EndpointPolicyLink } from './components/endpoint_policy_link';
|
||||
import { OutOfDate } from './components/out_of_date';
|
||||
import { AdminSearchBar } from './components/search_bar';
|
||||
import { AdministrationListPage } from '../../../components/administration_list_page';
|
||||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
|
@ -322,17 +324,22 @@ export const EndpointList = () => {
|
|||
}),
|
||||
truncateText: true,
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied']) => {
|
||||
render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => {
|
||||
return (
|
||||
<EuiToolTip content={policy.name} anchorClassName="eui-textTruncate">
|
||||
<EndpointPolicyLink
|
||||
policyId={policy.id}
|
||||
className="eui-textTruncate"
|
||||
data-test-subj="policyNameCellLink"
|
||||
>
|
||||
{policy.name}
|
||||
</EndpointPolicyLink>
|
||||
</EuiToolTip>
|
||||
<>
|
||||
<EuiToolTip content={policy.name} anchorClassName="eui-textTruncate">
|
||||
<EndpointPolicyLink
|
||||
policyId={policy.id}
|
||||
className="eui-textTruncate"
|
||||
data-test-subj="policyNameCellLink"
|
||||
>
|
||||
{policy.name}
|
||||
</EndpointPolicyLink>
|
||||
</EuiToolTip>
|
||||
{isPolicyOutOfDate(policy, item.policy_info) && (
|
||||
<OutOfDate style={{ paddingLeft: '6px' }} data-test-subj="rowPolicyOutOfDate" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -10,7 +10,13 @@ import {
|
|||
SavedObjectsClientContract,
|
||||
} from 'src/core/server';
|
||||
import { SecurityPluginSetup } from '../../../security/server';
|
||||
import { AgentService, FleetStartContract, PackageService } from '../../../fleet/server';
|
||||
import {
|
||||
AgentService,
|
||||
FleetStartContract,
|
||||
PackageService,
|
||||
AgentPolicyServiceInterface,
|
||||
PackagePolicyServiceInterface,
|
||||
} from '../../../fleet/server';
|
||||
import { PluginStartContract as AlertsPluginStartContract } from '../../../alerts/server';
|
||||
import { getPackagePolicyCreateCallback } from './ingest_integration';
|
||||
import { ManifestManager } from './services/artifacts';
|
||||
|
@ -66,7 +72,10 @@ export const createMetadataService = (packageService: PackageService): MetadataS
|
|||
};
|
||||
|
||||
export type EndpointAppContextServiceStartContract = Partial<
|
||||
Pick<FleetStartContract, 'agentService' | 'packageService'>
|
||||
Pick<
|
||||
FleetStartContract,
|
||||
'agentService' | 'packageService' | 'packagePolicyService' | 'agentPolicyService'
|
||||
>
|
||||
> & {
|
||||
logger: Logger;
|
||||
manifestManager?: ManifestManager;
|
||||
|
@ -85,11 +94,15 @@ export type EndpointAppContextServiceStartContract = Partial<
|
|||
export class EndpointAppContextService {
|
||||
private agentService: AgentService | undefined;
|
||||
private manifestManager: ManifestManager | undefined;
|
||||
private packagePolicyService: PackagePolicyServiceInterface | undefined;
|
||||
private agentPolicyService: AgentPolicyServiceInterface | undefined;
|
||||
private savedObjectsStart: SavedObjectsServiceStart | undefined;
|
||||
private metadataService: MetadataService | undefined;
|
||||
|
||||
public start(dependencies: EndpointAppContextServiceStartContract) {
|
||||
this.agentService = dependencies.agentService;
|
||||
this.packagePolicyService = dependencies.packagePolicyService;
|
||||
this.agentPolicyService = dependencies.agentPolicyService;
|
||||
this.manifestManager = dependencies.manifestManager;
|
||||
this.savedObjectsStart = dependencies.savedObjectsStart;
|
||||
this.metadataService = createMetadataService(dependencies.packageService!);
|
||||
|
@ -115,6 +128,14 @@ export class EndpointAppContextService {
|
|||
return this.agentService;
|
||||
}
|
||||
|
||||
public getPackagePolicyService(): PackagePolicyServiceInterface | undefined {
|
||||
return this.packagePolicyService;
|
||||
}
|
||||
|
||||
public getAgentPolicyService(): AgentPolicyServiceInterface | undefined {
|
||||
return this.agentPolicyService;
|
||||
}
|
||||
|
||||
public getMetadataService(): MetadataService | undefined {
|
||||
return this.metadataService;
|
||||
}
|
||||
|
|
|
@ -9,13 +9,12 @@ import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mock
|
|||
import { securityMock } from '../../../security/server/mocks';
|
||||
import { alertsMock } from '../../../alerts/server/mocks';
|
||||
import { xpackMocks } from '../../../../mocks';
|
||||
import { FleetStartContract, ExternalCallback, PackageService } from '../../../fleet/server';
|
||||
import {
|
||||
AgentService,
|
||||
FleetStartContract,
|
||||
ExternalCallback,
|
||||
PackageService,
|
||||
} from '../../../fleet/server';
|
||||
import { createPackagePolicyServiceMock } from '../../../fleet/server/mocks';
|
||||
createPackagePolicyServiceMock,
|
||||
createMockAgentPolicyService,
|
||||
createMockAgentService,
|
||||
} from '../../../fleet/server/mocks';
|
||||
import { AppClientFactory } from '../client';
|
||||
import { createMockConfig } from '../lib/detection_engine/routes/__mocks__';
|
||||
import {
|
||||
|
@ -25,6 +24,7 @@ import {
|
|||
import { ManifestManager } from './services/artifacts/manifest_manager/manifest_manager';
|
||||
import { getManifestManagerMock } from './services/artifacts/manifest_manager/manifest_manager.mock';
|
||||
import { EndpointAppContext } from './types';
|
||||
import { MetadataRequestContext } from './routes/metadata/handlers';
|
||||
|
||||
/**
|
||||
* Creates a mocked EndpointAppContext.
|
||||
|
@ -49,6 +49,7 @@ export const createMockEndpointAppContextService = (
|
|||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
getAgentService: jest.fn(),
|
||||
getAgentPolicyService: jest.fn(),
|
||||
getManifestManager: jest.fn().mockReturnValue(mockManifestManager ?? jest.fn()),
|
||||
getScopedSavedObjectsClient: jest.fn(),
|
||||
} as unknown) as jest.Mocked<EndpointAppContextService>;
|
||||
|
@ -90,18 +91,6 @@ export const createMockPackageService = (): jest.Mocked<PackageService> => {
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock AgentService
|
||||
*/
|
||||
export const createMockAgentService = (): jest.Mocked<AgentService> => {
|
||||
return {
|
||||
getAgentStatusById: jest.fn(),
|
||||
authenticateAgentWithAccessToken: jest.fn(),
|
||||
getAgent: jest.fn(),
|
||||
listAgents: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock IndexPatternService for use in tests that need to interact with the Ingest Manager's
|
||||
* ESIndexPatternService.
|
||||
|
@ -116,11 +105,20 @@ export const createMockFleetStartContract = (indexPattern: string): FleetStartCo
|
|||
},
|
||||
agentService: createMockAgentService(),
|
||||
packageService: createMockPackageService(),
|
||||
agentPolicyService: createMockAgentPolicyService(),
|
||||
registerExternalCallback: jest.fn((...args: ExternalCallback) => {}),
|
||||
packagePolicyService: createPackagePolicyServiceMock(),
|
||||
};
|
||||
};
|
||||
|
||||
export const createMockMetadataRequestContext = (): jest.Mocked<MetadataRequestContext> => {
|
||||
return {
|
||||
endpointAppContextService: createMockEndpointAppContextService(),
|
||||
logger: loggingSystemMock.create().get('mock_endpoint_app_context'),
|
||||
requestHandlerContext: xpackMocks.createRequestHandlerContext(),
|
||||
};
|
||||
};
|
||||
|
||||
export function createRouteHandlerContext(
|
||||
dataClient: jest.Mocked<ILegacyScopedClusterClient>,
|
||||
savedObjectsClient: jest.Mocked<SavedObjectsClientContract>
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
/*
|
||||
* 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 { HostStatus, MetadataQueryStrategyVersions } from '../../../../common/endpoint/types';
|
||||
import { createMockMetadataRequestContext } from '../../mocks';
|
||||
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
|
||||
import { enrichHostMetadata, MetadataRequestContext } from './handlers';
|
||||
|
||||
describe('test document enrichment', () => {
|
||||
let metaReqCtx: jest.Mocked<MetadataRequestContext>;
|
||||
const docGen = new EndpointDocGenerator();
|
||||
|
||||
beforeEach(() => {
|
||||
metaReqCtx = createMockMetadataRequestContext();
|
||||
});
|
||||
|
||||
// verify query version passed through
|
||||
describe('metadata query strategy enrichment', () => {
|
||||
it('should match v1 strategy when directed', async () => {
|
||||
const enrichedHostList = await enrichHostMetadata(
|
||||
docGen.generateHostMetadata(),
|
||||
metaReqCtx,
|
||||
MetadataQueryStrategyVersions.VERSION_1
|
||||
);
|
||||
expect(enrichedHostList.query_strategy_version).toEqual(
|
||||
MetadataQueryStrategyVersions.VERSION_1
|
||||
);
|
||||
});
|
||||
it('should match v2 strategy when directed', async () => {
|
||||
const enrichedHostList = await enrichHostMetadata(
|
||||
docGen.generateHostMetadata(),
|
||||
metaReqCtx,
|
||||
MetadataQueryStrategyVersions.VERSION_2
|
||||
);
|
||||
expect(enrichedHostList.query_strategy_version).toEqual(
|
||||
MetadataQueryStrategyVersions.VERSION_2
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('host status enrichment', () => {
|
||||
let statusFn: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
statusFn = jest.fn();
|
||||
(metaReqCtx.endpointAppContextService.getAgentService as jest.Mock).mockImplementation(() => {
|
||||
return {
|
||||
getAgentStatusById: statusFn,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it('should return host online for online agent', async () => {
|
||||
statusFn.mockImplementation(() => 'online');
|
||||
|
||||
const enrichedHostList = await enrichHostMetadata(
|
||||
docGen.generateHostMetadata(),
|
||||
metaReqCtx,
|
||||
MetadataQueryStrategyVersions.VERSION_2
|
||||
);
|
||||
expect(enrichedHostList.host_status).toEqual(HostStatus.ONLINE);
|
||||
});
|
||||
|
||||
it('should return host offline for offline agent', async () => {
|
||||
statusFn.mockImplementation(() => 'offline');
|
||||
|
||||
const enrichedHostList = await enrichHostMetadata(
|
||||
docGen.generateHostMetadata(),
|
||||
metaReqCtx,
|
||||
MetadataQueryStrategyVersions.VERSION_2
|
||||
);
|
||||
expect(enrichedHostList.host_status).toEqual(HostStatus.OFFLINE);
|
||||
});
|
||||
|
||||
it('should return host unenrolling for unenrolling agent', async () => {
|
||||
statusFn.mockImplementation(() => 'unenrolling');
|
||||
|
||||
const enrichedHostList = await enrichHostMetadata(
|
||||
docGen.generateHostMetadata(),
|
||||
metaReqCtx,
|
||||
MetadataQueryStrategyVersions.VERSION_2
|
||||
);
|
||||
expect(enrichedHostList.host_status).toEqual(HostStatus.UNENROLLING);
|
||||
});
|
||||
|
||||
it('should return host error for degraded agent', async () => {
|
||||
statusFn.mockImplementation(() => 'degraded');
|
||||
|
||||
const enrichedHostList = await enrichHostMetadata(
|
||||
docGen.generateHostMetadata(),
|
||||
metaReqCtx,
|
||||
MetadataQueryStrategyVersions.VERSION_2
|
||||
);
|
||||
expect(enrichedHostList.host_status).toEqual(HostStatus.ERROR);
|
||||
});
|
||||
|
||||
it('should return host error for erroring agent', async () => {
|
||||
statusFn.mockImplementation(() => 'error');
|
||||
|
||||
const enrichedHostList = await enrichHostMetadata(
|
||||
docGen.generateHostMetadata(),
|
||||
metaReqCtx,
|
||||
MetadataQueryStrategyVersions.VERSION_2
|
||||
);
|
||||
expect(enrichedHostList.host_status).toEqual(HostStatus.ERROR);
|
||||
});
|
||||
|
||||
it('should return host error for warning agent', async () => {
|
||||
statusFn.mockImplementation(() => 'warning');
|
||||
|
||||
const enrichedHostList = await enrichHostMetadata(
|
||||
docGen.generateHostMetadata(),
|
||||
metaReqCtx,
|
||||
MetadataQueryStrategyVersions.VERSION_2
|
||||
);
|
||||
expect(enrichedHostList.host_status).toEqual(HostStatus.ERROR);
|
||||
});
|
||||
|
||||
it('should return host error for invalid agent', async () => {
|
||||
statusFn.mockImplementation(() => 'asliduasofb');
|
||||
|
||||
const enrichedHostList = await enrichHostMetadata(
|
||||
docGen.generateHostMetadata(),
|
||||
metaReqCtx,
|
||||
MetadataQueryStrategyVersions.VERSION_2
|
||||
);
|
||||
expect(enrichedHostList.host_status).toEqual(HostStatus.ERROR);
|
||||
});
|
||||
});
|
||||
|
||||
describe('policy info enrichment', () => {
|
||||
let agentMock: jest.Mock;
|
||||
let agentPolicyMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
agentMock = jest.fn();
|
||||
agentPolicyMock = jest.fn();
|
||||
(metaReqCtx.endpointAppContextService.getAgentService as jest.Mock).mockImplementation(() => {
|
||||
return {
|
||||
getAgent: agentMock,
|
||||
getAgentStatusById: jest.fn(),
|
||||
};
|
||||
});
|
||||
(metaReqCtx.endpointAppContextService.getAgentPolicyService as jest.Mock).mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
get: agentPolicyMock,
|
||||
};
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('reflects current applied agent info', async () => {
|
||||
const policyID = 'abc123';
|
||||
const policyRev = 9;
|
||||
agentMock.mockImplementation(() => {
|
||||
return {
|
||||
policy_id: policyID,
|
||||
policy_revision: policyRev,
|
||||
};
|
||||
});
|
||||
|
||||
const enrichedHostList = await enrichHostMetadata(
|
||||
docGen.generateHostMetadata(),
|
||||
metaReqCtx,
|
||||
MetadataQueryStrategyVersions.VERSION_2
|
||||
);
|
||||
expect(enrichedHostList.policy_info).toBeDefined();
|
||||
expect(enrichedHostList.policy_info!.agent.applied.id).toEqual(policyID);
|
||||
expect(enrichedHostList.policy_info!.agent.applied.revision).toEqual(policyRev);
|
||||
});
|
||||
|
||||
it('reflects current fleet agent info', async () => {
|
||||
const policyID = 'xyz456';
|
||||
const policyRev = 15;
|
||||
agentPolicyMock.mockImplementation(() => {
|
||||
return {
|
||||
id: policyID,
|
||||
revision: policyRev,
|
||||
};
|
||||
});
|
||||
|
||||
const enrichedHostList = await enrichHostMetadata(
|
||||
docGen.generateHostMetadata(),
|
||||
metaReqCtx,
|
||||
MetadataQueryStrategyVersions.VERSION_2
|
||||
);
|
||||
expect(enrichedHostList.policy_info).toBeDefined();
|
||||
expect(enrichedHostList.policy_info!.agent.configured.id).toEqual(policyID);
|
||||
expect(enrichedHostList.policy_info!.agent.configured.revision).toEqual(policyRev);
|
||||
});
|
||||
|
||||
it('reflects current endpoint policy info', async () => {
|
||||
const policyID = 'endpoint-b33f';
|
||||
const policyRev = 2;
|
||||
agentPolicyMock.mockImplementation(() => {
|
||||
return {
|
||||
package_policies: [
|
||||
{
|
||||
package: { name: 'endpoint' },
|
||||
id: policyID,
|
||||
revision: policyRev,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const enrichedHostList = await enrichHostMetadata(
|
||||
docGen.generateHostMetadata(),
|
||||
metaReqCtx,
|
||||
MetadataQueryStrategyVersions.VERSION_2
|
||||
);
|
||||
expect(enrichedHostList.policy_info).toBeDefined();
|
||||
expect(enrichedHostList.policy_info!.endpoint.id).toEqual(policyID);
|
||||
expect(enrichedHostList.policy_info!.endpoint.revision).toEqual(policyRev);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -15,7 +15,7 @@ import {
|
|||
MetadataQueryStrategyVersions,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders';
|
||||
import { Agent, AgentStatus } from '../../../../../fleet/common/types/models';
|
||||
import { Agent, AgentStatus, PackagePolicy } from '../../../../../fleet/common/types/models';
|
||||
import { EndpointAppContext, HostListQueryResult } from '../../types';
|
||||
import { GetMetadataListRequestSchema, GetMetadataRequestSchema } from './index';
|
||||
import { findAllUnenrolledAgentIds } from './support/unenroll';
|
||||
|
@ -245,7 +245,7 @@ export async function mapToHostResultList(
|
|||
}
|
||||
}
|
||||
|
||||
async function enrichHostMetadata(
|
||||
export async function enrichHostMetadata(
|
||||
hostMetadata: HostMetadata,
|
||||
metadataRequestContext: MetadataRequestContext,
|
||||
metadataQueryStrategyVersion: MetadataQueryStrategyVersions
|
||||
|
@ -282,9 +282,53 @@ async function enrichHostMetadata(
|
|||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
let policyInfo: HostInfo['policy_info'];
|
||||
try {
|
||||
const agent = await metadataRequestContext.endpointAppContextService
|
||||
?.getAgentService()
|
||||
?.getAgent(
|
||||
metadataRequestContext.requestHandlerContext.core.savedObjects.client,
|
||||
elasticAgentId
|
||||
);
|
||||
const agentPolicy = await metadataRequestContext.endpointAppContextService
|
||||
.getAgentPolicyService()
|
||||
?.get(
|
||||
metadataRequestContext.requestHandlerContext.core.savedObjects.client,
|
||||
agent?.policy_id!,
|
||||
true
|
||||
);
|
||||
const endpointPolicy = ((agentPolicy?.package_policies || []) as PackagePolicy[]).find(
|
||||
(policy: PackagePolicy) => policy.package?.name === 'endpoint'
|
||||
);
|
||||
|
||||
policyInfo = {
|
||||
agent: {
|
||||
applied: {
|
||||
revision: agent?.policy_revision || 0,
|
||||
id: agent?.policy_id || '',
|
||||
},
|
||||
configured: {
|
||||
revision: agentPolicy?.revision || 0,
|
||||
id: agentPolicy?.id || '',
|
||||
},
|
||||
},
|
||||
endpoint: {
|
||||
revision: endpointPolicy?.revision || 0,
|
||||
id: endpointPolicy?.id || '',
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
// this is a non-vital enrichment of expected policy revisions.
|
||||
// if we fail just fetching these, the rest of the endpoint
|
||||
// data should still be returned. log the error and move on
|
||||
log.error(e);
|
||||
}
|
||||
|
||||
return {
|
||||
metadata: hostMetadata,
|
||||
host_status: hostStatus,
|
||||
policy_info: policyInfo,
|
||||
query_strategy_version: metadataQueryStrategyVersion,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import { SavedObjectsClientContract } from 'kibana/server';
|
|||
import { findAgentIDsByStatus } from './agent_status';
|
||||
import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks';
|
||||
import { AgentService } from '../../../../../../fleet/server/services';
|
||||
import { createMockAgentService } from '../../../mocks';
|
||||
import { createMockAgentService } from '../../../../../../fleet/server/mocks';
|
||||
import { Agent } from '../../../../../../fleet/common/types/models';
|
||||
import { AgentStatusKueryHelper } from '../../../../../../fleet/common/services';
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import { SavedObjectsClientContract } from 'kibana/server';
|
|||
import { findAllUnenrolledAgentIds } from './unenroll';
|
||||
import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks';
|
||||
import { AgentService } from '../../../../../../fleet/server/services';
|
||||
import { createMockAgentService } from '../../../mocks';
|
||||
import { createMockAgentService } from '../../../../../../fleet/server/mocks';
|
||||
import { Agent } from '../../../../../../fleet/common/types/models';
|
||||
|
||||
describe('test find all unenrolled Agent id', () => {
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
*/
|
||||
import { EndpointAppContextService } from '../../endpoint_app_context_services';
|
||||
import {
|
||||
createMockAgentService,
|
||||
createMockEndpointAppContextServiceStartContract,
|
||||
createRouteHandlerContext,
|
||||
} from '../../mocks';
|
||||
import { createMockAgentService } from '../../../../../fleet/server/mocks';
|
||||
import { getHostPolicyResponseHandler, getAgentPolicySummaryHandler } from './handlers';
|
||||
import {
|
||||
ILegacyScopedClusterClient,
|
||||
|
|
|
@ -347,6 +347,8 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
this.endpointAppContextService.start({
|
||||
agentService: plugins.fleet?.agentService,
|
||||
packageService: plugins.fleet?.packageService,
|
||||
packagePolicyService: plugins.fleet?.packagePolicyService,
|
||||
agentPolicyService: plugins.fleet?.agentPolicyService,
|
||||
appClientFactory: this.appClientFactory,
|
||||
security: this.setupPlugins!.security!,
|
||||
alerts: plugins.alerts,
|
||||
|
|
Loading…
Reference in a new issue