Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: I don't like that these then().catch() blocks are repeated below. Will look to improve in the future (maybe by passing along the dispatch method to the getNonexistingPoliciesForHostList() as an argument.

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