mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
9aa5e1772d
commit
6d4bb9dc0d
10 changed files with 224 additions and 35 deletions
|
@ -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;
|
||||
|
|
|
@ -50,6 +50,7 @@ describe('HostList store concerns', () => {
|
|||
selectedPolicyId: undefined,
|
||||
policyItemsLoading: false,
|
||||
endpointPackageInfo: undefined,
|
||||
nonExistingPolicies: {},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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';
|
|
@ -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 [
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue