Skip to content

Commit

Permalink
[SECURITY_SOLUTION][ENDPOINT] Handle Host list/details policy links t…
Browse files Browse the repository at this point in the history
…o 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
  • Loading branch information
paul-tavares authored Jul 27, 2020
1 parent 9aa5e17 commit 6d4bb9d
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -75,6 +76,11 @@ interface ServerReturnedEndpointPackageInfo {
payload: GetPackagesResponse['response'][0];
}

interface ServerReturnedHostNonExistingPolicies {
type: 'serverReturnedHostNonExistingPolicies';
payload: HostState['nonExistingPolicies'];
}

export type HostAction =
| ServerReturnedHostList
| ServerFailedToReturnHostList
Expand All @@ -87,4 +93,5 @@ export type HostAction =
| UserSelectedEndpointPolicy
| ServerCancelledHostListLoading
| ServerCancelledPolicyItemsLoading
| ServerReturnedEndpointPackageInfo;
| ServerReturnedEndpointPackageInfo
| ServerReturnedHostNonExistingPolicies;
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe('HostList store concerns', () => {
selectedPolicyId: undefined,
policyItemsLoading: false,
endpointPackageInfo: undefined,
nonExistingPolicies: {},
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) => {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const initialHostListState: Immutable<HostState> = {
selectedPolicyId: undefined,
policyItemsLoading: false,
endpointPackageInfo: undefined,
nonExistingPolicies: {},
};

/* eslint-disable-next-line complexity */
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 [
{
Expand All @@ -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>
</>
),
},
Expand Down Expand Up @@ -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 [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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>
);
},
},
Expand Down
Loading

0 comments on commit 6d4bb9d

Please sign in to comment.