[SECURITY_SOLUTION][ENDPOINT] Handle Host list/details policy links to non-existing policies (#73208)

* Make API call to check policies and save it to store
* change policy list and details to not show policy as a link if it does not exist
This commit is contained in:
Paul Tavares 2020-07-27 11:13:26 -04:00 committed by GitHub
parent 9aa5e1772d
commit 6d4bb9dc0d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 224 additions and 35 deletions

View file

@ -12,6 +12,7 @@ import {
import { ServerApiError } from '../../../../common/types';
import { GetPolicyListResponse } from '../../policy/types';
import { GetPackagesResponse } from '../../../../../../ingest_manager/common';
import { HostState } from '../types';
interface ServerReturnedHostList {
type: 'serverReturnedHostList';
@ -75,6 +76,11 @@ interface ServerReturnedEndpointPackageInfo {
payload: GetPackagesResponse['response'][0];
}
interface ServerReturnedHostNonExistingPolicies {
type: 'serverReturnedHostNonExistingPolicies';
payload: HostState['nonExistingPolicies'];
}
export type HostAction =
| ServerReturnedHostList
| ServerFailedToReturnHostList
@ -87,4 +93,5 @@ export type HostAction =
| UserSelectedEndpointPolicy
| ServerCancelledHostListLoading
| ServerCancelledPolicyItemsLoading
| ServerReturnedEndpointPackageInfo;
| ServerReturnedEndpointPackageInfo
| ServerReturnedHostNonExistingPolicies;

View file

@ -50,6 +50,7 @@ describe('HostList store concerns', () => {
selectedPolicyId: undefined,
policyItemsLoading: false,
endpointPackageInfo: undefined,
nonExistingPolicies: {},
});
});

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HostResultList } from '../../../../../common/endpoint/types';
import { HttpSetup } from 'kibana/public';
import { HostInfo, HostResultList } from '../../../../../common/endpoint/types';
import { GetPolicyListResponse } from '../../policy/types';
import { ImmutableMiddlewareFactory } from '../../../../common/store';
import {
@ -13,12 +14,15 @@ import {
uiQueryParams,
listData,
endpointPackageInfo,
nonExistingPolicies,
} from './selectors';
import { HostState } from '../types';
import {
sendGetEndpointSpecificPackageConfigs,
sendGetEndpointSecurityPackage,
sendGetAgentConfigList,
} from '../../policy/store/policy_list/services/ingest';
import { AGENT_CONFIG_SAVED_OBJECT_TYPE } from '../../../../../../ingest_manager/common';
export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostState> = (coreStart) => {
return ({ getState, dispatch }) => (next) => async (action) => {
@ -58,6 +62,23 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostState> = (cor
type: 'serverReturnedHostList',
payload: hostResponse,
});
getNonExistingPoliciesForHostsList(
coreStart.http,
hostResponse.hosts,
nonExistingPolicies(state)
)
.then((missingPolicies) => {
if (missingPolicies !== undefined) {
dispatch({
type: 'serverReturnedHostNonExistingPolicies',
payload: missingPolicies,
});
}
})
// Ignore Errors, since this should not hinder the user's ability to use the UI
// eslint-disable-next-line no-console
.catch((error) => console.error(error));
} catch (error) {
dispatch({
type: 'serverFailedToReturnHostList',
@ -117,6 +138,23 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostState> = (cor
type: 'serverReturnedHostList',
payload: response,
});
getNonExistingPoliciesForHostsList(
coreStart.http,
response.hosts,
nonExistingPolicies(state)
)
.then((missingPolicies) => {
if (missingPolicies !== undefined) {
dispatch({
type: 'serverReturnedHostNonExistingPolicies',
payload: missingPolicies,
});
}
})
// Ignore Errors, since this should not hinder the user's ability to use the UI
// eslint-disable-next-line no-console
.catch((error) => console.error(error));
} catch (error) {
dispatch({
type: 'serverFailedToReturnHostList',
@ -133,11 +171,25 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostState> = (cor
// call the host details api
const { selected_host: selectedHost } = uiQueryParams(state);
try {
const response = await coreStart.http.get(`/api/endpoint/metadata/${selectedHost}`);
const response = await coreStart.http.get<HostInfo>(
`/api/endpoint/metadata/${selectedHost}`
);
dispatch({
type: 'serverReturnedHostDetails',
payload: response,
});
getNonExistingPoliciesForHostsList(coreStart.http, [response], nonExistingPolicies(state))
.then((missingPolicies) => {
if (missingPolicies !== undefined) {
dispatch({
type: 'serverReturnedHostNonExistingPolicies',
payload: missingPolicies,
});
}
})
// Ignore Errors, since this should not hinder the user's ability to use the UI
// eslint-disable-next-line no-console
.catch((error) => console.error(error));
} catch (error) {
dispatch({
type: 'serverFailedToReturnHostDetails',
@ -163,3 +215,62 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostState> = (cor
}
};
};
const getNonExistingPoliciesForHostsList = async (
http: HttpSetup,
hosts: HostResultList['hosts'],
currentNonExistingPolicies: HostState['nonExistingPolicies']
): Promise<HostState['nonExistingPolicies'] | undefined> => {
if (hosts.length === 0) {
return;
}
// Create an array of unique policy IDs that are not yet known to be non-existing.
const policyIdsToCheck = Array.from(
new Set(
hosts
.filter((host) => !currentNonExistingPolicies[host.metadata.Endpoint.policy.applied.id])
.map((host) => host.metadata.Endpoint.policy.applied.id)
)
);
if (policyIdsToCheck.length === 0) {
return;
}
// We use the Agent Config API here, instead of the Package Config, because we can't use
// filter by ID of the Saved Object. Agent Config, however, keeps a reference (array) of
// Package Ids that it uses, thus if a reference exists there, then the package config (policy)
// exists.
const policiesFound = (
await sendGetAgentConfigList(http, {
query: {
kuery: `${AGENT_CONFIG_SAVED_OBJECT_TYPE}.package_configs: (${policyIdsToCheck.join(
' or '
)})`,
},
})
).items.reduce<HostState['nonExistingPolicies']>((list, agentConfig) => {
(agentConfig.package_configs as string[]).forEach((packageConfig) => {
list[packageConfig as string] = true;
});
return list;
}, {});
const nonExisting = policyIdsToCheck.reduce<HostState['nonExistingPolicies']>(
(list, policyId) => {
if (policiesFound[policyId]) {
return list;
}
list[policyId] = true;
return list;
},
{}
);
if (Object.keys(nonExisting).length === 0) {
return;
}
return nonExisting;
};

View file

@ -28,6 +28,7 @@ export const initialHostListState: Immutable<HostState> = {
selectedPolicyId: undefined,
policyItemsLoading: false,
endpointPackageInfo: undefined,
nonExistingPolicies: {},
};
/* eslint-disable-next-line complexity */
@ -57,6 +58,14 @@ export const hostListReducer: ImmutableReducer<HostState, AppAction> = (
error: action.payload,
loading: false,
};
} else if (action.type === 'serverReturnedHostNonExistingPolicies') {
return {
...state,
nonExistingPolicies: {
...state.nonExistingPolicies,
...action.payload,
},
};
} else if (action.type === 'serverReturnedHostDetails') {
return {
...state,

View file

@ -195,3 +195,11 @@ export const policyResponseStatus: (state: Immutable<HostState>) => string = cre
return (policyResponse && policyResponse?.Endpoint?.policy?.applied?.status) || '';
}
);
/**
* returns the list of known non-existing polices that may have been in the Host API response.
* @param state
*/
export const nonExistingPolicies: (
state: Immutable<HostState>
) => Immutable<HostState['nonExistingPolicies']> = (state) => state.nonExistingPolicies;

View file

@ -50,6 +50,8 @@ export interface HostState {
selectedPolicyId?: string;
/** Endpoint package info */
endpointPackageInfo?: GetPackagesResponse['response'][0];
/** tracks the list of policies IDs used in Host metadata that may no longer exist */
nonExistingPolicies: Record<string, boolean>;
}
/**

View file

@ -0,0 +1,53 @@
/*
* 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, { memo, useMemo } from 'react';
import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui';
import { useHostSelector } from '../hooks';
import { nonExistingPolicies } from '../../store/selectors';
import { getPolicyDetailPath } from '../../../../common/routing';
import { useFormatUrl } from '../../../../../common/components/link_to';
import { SecurityPageName } from '../../../../../../common/constants';
import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
/**
* A policy link (to details) that first checks to see if the policy id exists against
* the `nonExistingPolicies` value in the store. If it does not exist, then regular
* text is returned.
*/
export const HostPolicyLink = memo<
Omit<EuiLinkAnchorProps, 'href'> & {
policyId: string;
}
>(({ policyId, children, onClick, ...otherProps }) => {
const missingPolicies = useHostSelector(nonExistingPolicies);
const { formatUrl } = useFormatUrl(SecurityPageName.administration);
const { toRoutePath, toRouteUrl } = useMemo(() => {
const toPath = getPolicyDetailPath(policyId);
return {
toRoutePath: toPath,
toRouteUrl: formatUrl(toPath),
};
}, [formatUrl, policyId]);
const clickHandler = useNavigateByRouterEventHandler(toRoutePath, onClick);
if (missingPolicies[policyId]) {
return (
<span className={otherProps.className} data-test-subj={otherProps['data-test-subj']}>
{children}
</span>
);
}
return (
// eslint-disable-next-line @elastic/eui/href-or-on-click
<EuiLink href={toRouteUrl} onClick={clickHandler} {...otherProps}>
{children}
</EuiLink>
);
});
HostPolicyLink.displayName = 'HostPolicyLink';

View file

@ -26,10 +26,11 @@ import { POLICY_STATUS_TO_HEALTH_COLOR } from '../host_constants';
import { FormattedDateAndTime } from '../../../../../common/components/endpoint/formatted_date_time';
import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app';
import { getHostDetailsPath, getPolicyDetailPath } from '../../../../common/routing';
import { getHostDetailsPath } from '../../../../common/routing';
import { SecurityPageName } from '../../../../../app/types';
import { useFormatUrl } from '../../../../../common/components/link_to';
import { AgentDetailsReassignConfigAction } from '../../../../../../../ingest_manager/public';
import { HostPolicyLink } from '../components/host_policy_link';
const HostIds = styled(EuiListGroupItem)`
margin-top: 0;
@ -116,15 +117,6 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath);
const [policyDetailsRoutePath, policyDetailsRouteUrl] = useMemo(() => {
return [
getPolicyDetailPath(details.Endpoint.policy.applied.id),
formatUrl(getPolicyDetailPath(details.Endpoint.policy.applied.id)),
];
}, [details.Endpoint.policy.applied.id, formatUrl]);
const policyDetailsClickHandler = useNavigateByRouterEventHandler(policyDetailsRoutePath);
const detailsResultsPolicy = useMemo(() => {
return [
{
@ -133,14 +125,12 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
}),
description: (
<>
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
<EuiLink
<HostPolicyLink
policyId={details.Endpoint.policy.applied.id}
data-test-subj="policyDetailsValue"
href={policyDetailsRouteUrl}
onClick={policyDetailsClickHandler}
>
{details.Endpoint.policy.applied.name}
</EuiLink>
</HostPolicyLink>
</>
),
},
@ -171,14 +161,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
),
},
];
}, [
details,
policyResponseUri,
policyStatus,
policyStatusClickHandler,
policyDetailsRouteUrl,
policyDetailsClickHandler,
]);
}, [details, policyResponseUri, policyStatus, policyStatusClickHandler]);
const detailsResultsLower = useMemo(() => {
return [
{

View file

@ -46,9 +46,10 @@ import {
AgentConfigDetailsDeployAgentAction,
} from '../../../../../../ingest_manager/public';
import { SecurityPageName } from '../../../../app/types';
import { getHostListPath, getHostDetailsPath, getPolicyDetailPath } from '../../../common/routing';
import { getHostListPath, getHostDetailsPath } from '../../../common/routing';
import { useFormatUrl } from '../../../../common/components/link_to';
import { HostAction } from '../store/action';
import { HostPolicyLink } from './components/host_policy_link';
const HostListNavLink = memo<{
name: string;
@ -241,15 +242,14 @@ export const HostList = () => {
truncateText: true,
// eslint-disable-next-line react/display-name
render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied']) => {
const toRoutePath = getPolicyDetailPath(policy.id);
const toRouteUrl = formatUrl(toRoutePath);
return (
<HostListNavLink
name={policy.name}
href={toRouteUrl}
route={toRoutePath}
dataTestSubj="policyNameCellLink"
/>
<HostPolicyLink
policyId={policy.id}
className="eui-textTruncate"
data-test-subj="policyNameCellLink"
>
{policy.name}
</HostPolicyLink>
);
},
},

View file

@ -12,12 +12,15 @@ import {
DeletePackageConfigsRequest,
PACKAGE_CONFIG_SAVED_OBJECT_TYPE,
GetPackagesResponse,
GetAgentConfigsRequest,
GetAgentConfigsResponse,
} from '../../../../../../../../ingest_manager/common';
import { GetPolicyListResponse, GetPolicyResponse, UpdatePolicyResponse } from '../../../types';
import { NewPolicyData } from '../../../../../../../common/endpoint/types';
const INGEST_API_ROOT = `/api/ingest_manager`;
export const INGEST_API_PACKAGE_CONFIGS = `${INGEST_API_ROOT}/package_configs`;
const INGEST_API_AGENT_CONFIGS = `${INGEST_API_ROOT}/agent_configs`;
const INGEST_API_FLEET = `${INGEST_API_ROOT}/fleet`;
const INGEST_API_FLEET_AGENT_STATUS = `${INGEST_API_FLEET}/agent-status`;
export const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`;
@ -75,6 +78,18 @@ export const sendDeletePackageConfig = (
});
};
/**
* Retrieve a list of Agent Configurations
* @param http
* @param options
*/
export const sendGetAgentConfigList = (
http: HttpStart,
options: HttpFetchOptions & GetAgentConfigsRequest
) => {
return http.get<GetAgentConfigsResponse>(INGEST_API_AGENT_CONFIGS, options);
};
/**
* Updates a package config
*