mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution][Endpoint] Add validation to artifact create/update APIs for management of ownerSpaceId
(#211325)
## Summary #### Changes in support of space awareness > currently behind feature flag: `endpointManagementSpaceAwarenessEnabled` - Add logic to the server-side Lists plugin extension points for endpoint artifacts to ensure that only a user with the new Global Artifact Management privilege can update/change/add `ownerSpaceId` tags on an artifact - Added validation to all endpoint artifacts (Trusted Apps, Event Filters, Blocklists, Host Isolation Exceptions and Endpoint Exceptions) #### Other changes: - Fix UI bug that failed to display artifact submit API failures. API errors are now displayed in the artifact's respective edit/create forms if encountered - Fixed a bug where "unknown" artifact `tags` were being dropped whenever the artifact assignment (global, per-policy) was updated in the UI ## Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
0e02a32892
commit
1ee97c3c8f
34 changed files with 1465 additions and 770 deletions
|
@ -108,6 +108,12 @@ export const ENDPOINT_PRIVILEGES: Record<string, PrivilegeMapObject> = deepFreez
|
|||
privilegeType: 'api',
|
||||
privilegeName: 'readEventFilters',
|
||||
},
|
||||
writeGlobalArtifacts: {
|
||||
appId: DEFAULT_APP_CATEGORIES.security.id,
|
||||
privilegeSplit: '-',
|
||||
privilegeType: 'api',
|
||||
privilegeName: 'writeGlobalArtifacts',
|
||||
},
|
||||
writePolicyManagement: {
|
||||
appId: DEFAULT_APP_CATEGORIES.security.id,
|
||||
privilegeSplit: '-',
|
||||
|
|
|
@ -44,9 +44,21 @@ export const getPolicyIdsFromArtifact = (item: Pick<ExceptionListItemSchema, 'ta
|
|||
return policyIds;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an Artifact tag value, utility will return a boolean indicating if that tag is
|
||||
* tracking artifact assignment (global/per-policy)
|
||||
*/
|
||||
export const isPolicySelectionTag: TagFilter = (tag) =>
|
||||
tag.startsWith(BY_POLICY_ARTIFACT_TAG_PREFIX) || tag === GLOBAL_ARTIFACT_TAG;
|
||||
|
||||
/**
|
||||
* Builds the per-policy tag that should be stored in the artifact's `tags` array
|
||||
* @param policyId
|
||||
*/
|
||||
export const buildPerPolicyTag = (policyId: string): string => {
|
||||
return `${BY_POLICY_ARTIFACT_TAG_PREFIX}${policyId}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a list of artifact policy tags based on a current
|
||||
* selection by the EffectedPolicySelection component.
|
||||
|
@ -57,7 +69,7 @@ export const getArtifactTagsByPolicySelection = (selection: EffectedPolicySelect
|
|||
}
|
||||
|
||||
return selection.selected.map((policy) => {
|
||||
return `${BY_POLICY_ARTIFACT_TAG_PREFIX}${policy.id}`;
|
||||
return buildPerPolicyTag(policy.id);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -79,13 +91,16 @@ export const getEffectedPolicySelectionByTags = (
|
|||
};
|
||||
}
|
||||
const selected: PolicyData[] = tags.reduce((acc, tag) => {
|
||||
// edge case: a left over tag with a non-existed policy
|
||||
// will be removed by verifying the policy exists
|
||||
const id = tag.split(':')[1];
|
||||
const foundPolicy = policies.find((policy) => policy.id === id);
|
||||
if (foundPolicy !== undefined) {
|
||||
acc.push(foundPolicy);
|
||||
if (tag.startsWith(BY_POLICY_ARTIFACT_TAG_PREFIX)) {
|
||||
const id = tag.split(':')[1];
|
||||
const foundPolicy = policies.find((policy) => policy.id === id);
|
||||
|
||||
// edge case: a left over tag with a non-existed policy will be removed by verifying the policy exists
|
||||
if (foundPolicy !== undefined) {
|
||||
acc.push(foundPolicy);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as PolicyData[]);
|
||||
|
||||
|
@ -120,6 +135,14 @@ export const createExceptionListItemForCreate = (listId: string): CreateExceptio
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks the provided `tag` string to see if it is an owner apace ID tag
|
||||
* @param tag
|
||||
*/
|
||||
export const isOwnerSpaceIdTag = (tag: string): boolean => {
|
||||
return tag.startsWith(OWNER_SPACE_ID_TAG_PREFIX);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array with all owner space IDs for the artifact
|
||||
*/
|
||||
|
@ -127,7 +150,7 @@ export const getArtifactOwnerSpaceIds = (
|
|||
item: Partial<Pick<ExceptionListItemSchema, 'tags'>>
|
||||
): string[] => {
|
||||
return (item.tags ?? []).reduce((acc, tag) => {
|
||||
if (tag.startsWith(OWNER_SPACE_ID_TAG_PREFIX)) {
|
||||
if (isOwnerSpaceIdTag(tag)) {
|
||||
acc.push(tag.substring(OWNER_SPACE_ID_TAG_PREFIX.length));
|
||||
}
|
||||
|
||||
|
@ -176,5 +199,5 @@ export const setArtifactOwnerSpaceId = (
|
|||
export const hasArtifactOwnerSpaceId = (
|
||||
item: Partial<Pick<ExceptionListItemSchema, 'tags'>>
|
||||
): boolean => {
|
||||
return (item.tags ?? []).some((tag) => tag.startsWith(OWNER_SPACE_ID_TAG_PREFIX));
|
||||
return (item.tags ?? []).some((tag) => isOwnerSpaceIdTag(tag));
|
||||
};
|
||||
|
|
|
@ -179,6 +179,7 @@ describe('Endpoint Authz service', () => {
|
|||
['canReadEventFilters', 'readEventFilters'],
|
||||
['canReadWorkflowInsights', 'readWorkflowInsights'],
|
||||
['canWriteWorkflowInsights', 'writeWorkflowInsights'],
|
||||
['canManageGlobalArtifacts', 'writeGlobalArtifacts'],
|
||||
])('%s should be true if `packagePrivilege.%s` is `true`', (auth) => {
|
||||
const authz = calculateEndpointAuthz(licenseService, fleetAuthz, userRoles);
|
||||
expect(authz[auth]).toBe(true);
|
||||
|
@ -220,6 +221,7 @@ describe('Endpoint Authz service', () => {
|
|||
['canReadEventFilters', ['readEventFilters']],
|
||||
['canWriteWorkflowInsights', ['writeWorkflowInsights']],
|
||||
['canReadWorkflowInsights', ['readWorkflowInsights']],
|
||||
['canManageGlobalArtifacts', ['writeGlobalArtifacts']],
|
||||
// all dependent privileges are false and so it should be false
|
||||
['canAccessResponseConsole', responseConsolePrivileges],
|
||||
])('%s should be false if `packagePrivilege.%s` is `false`', (auth, privileges) => {
|
||||
|
@ -271,6 +273,7 @@ describe('Endpoint Authz service', () => {
|
|||
['canReadEventFilters', ['readEventFilters']],
|
||||
['canWriteWorkflowInsights', ['writeWorkflowInsights']],
|
||||
['canReadWorkflowInsights', ['readWorkflowInsights']],
|
||||
['canManageGlobalArtifacts', ['writeGlobalArtifacts']],
|
||||
// all dependent privileges are false and so it should be false
|
||||
['canAccessResponseConsole', responseConsolePrivileges],
|
||||
])(
|
||||
|
@ -339,6 +342,7 @@ describe('Endpoint Authz service', () => {
|
|||
canWriteExecuteOperations: false,
|
||||
canWriteScanOperations: false,
|
||||
canWriteFileOperations: false,
|
||||
canManageGlobalArtifacts: false,
|
||||
canWriteTrustedApplications: false,
|
||||
canWriteWorkflowInsights: false,
|
||||
canReadTrustedApplications: false,
|
||||
|
|
|
@ -97,6 +97,8 @@ export const calculateEndpointAuthz = (
|
|||
const canReadEndpointExceptions = hasAuth('showEndpointExceptions');
|
||||
const canWriteEndpointExceptions = hasAuth('crudEndpointExceptions');
|
||||
|
||||
const canManageGlobalArtifacts = hasAuth('writeGlobalArtifacts');
|
||||
|
||||
const canReadWorkflowInsights = hasAuth('readWorkflowInsights');
|
||||
const canWriteWorkflowInsights = hasAuth('writeWorkflowInsights');
|
||||
|
||||
|
@ -156,6 +158,7 @@ export const calculateEndpointAuthz = (
|
|||
canReadEventFilters,
|
||||
canReadEndpointExceptions,
|
||||
canWriteEndpointExceptions,
|
||||
canManageGlobalArtifacts,
|
||||
};
|
||||
|
||||
// Response console is only accessible when license is Enterprise and user has access to any
|
||||
|
@ -212,6 +215,7 @@ export const getEndpointAuthzInitialState = (): EndpointAuthz => {
|
|||
canReadEventFilters: false,
|
||||
canReadEndpointExceptions: false,
|
||||
canWriteEndpointExceptions: false,
|
||||
canManageGlobalArtifacts: false,
|
||||
canReadWorkflowInsights: false,
|
||||
canWriteWorkflowInsights: false,
|
||||
};
|
||||
|
|
|
@ -93,6 +93,9 @@ export interface EndpointAuthz {
|
|||
canReadEndpointExceptions: boolean;
|
||||
/** if the user has read permissions for endpoint exceptions */
|
||||
canWriteEndpointExceptions: boolean;
|
||||
/** If user is allowed to manage global artifacts. Introduced support for spaces feature */
|
||||
canManageGlobalArtifacts: boolean;
|
||||
|
||||
/** if the user has write permissions for workflow insights */
|
||||
canWriteWorkflowInsights: boolean;
|
||||
/** if the user has read permissions for workflow insights */
|
||||
|
|
|
@ -18,7 +18,7 @@ export const ObjectContent = memo<ObjectContentProps>(({ data }) => {
|
|||
<EuiText size="s">
|
||||
{Object.entries(data).map(([key, value]) => {
|
||||
return (
|
||||
<div key={key} className="eui-textBreakAll">
|
||||
<div key={key} className="eui-textBreakWord">
|
||||
<strong>{key}</strong>
|
||||
{': '}
|
||||
{value}
|
||||
|
|
|
@ -142,7 +142,7 @@ describe('useGetUpdatedTags hook', () => {
|
|||
// add first
|
||||
rerender({ exception: { tags }, filters: getFiltersInOrder() });
|
||||
tags = result.current.getTagsUpdatedBy('first', ['first:brie']);
|
||||
expect(tags).toStrictEqual(['first:brie', 'special_second', 'third:spaghetti']);
|
||||
expect(tags).toStrictEqual(['special_second', 'third:spaghetti', 'first:brie']);
|
||||
});
|
||||
|
||||
it('should update category order on any change if filter is changed (although it should not)', () => {
|
||||
|
@ -155,11 +155,9 @@ describe('useGetUpdatedTags hook', () => {
|
|||
expect(tags).toStrictEqual([
|
||||
'first:mozzarella',
|
||||
'first:roquefort',
|
||||
|
||||
'second:shiraz',
|
||||
|
||||
'third:tagliatelle',
|
||||
'third:penne',
|
||||
'second:shiraz',
|
||||
]);
|
||||
|
||||
const newFilterOrder = {
|
||||
|
@ -172,12 +170,10 @@ describe('useGetUpdatedTags hook', () => {
|
|||
tags = result.current.getTagsUpdatedBy('third', ['third:spaghetti']);
|
||||
|
||||
expect(tags).toStrictEqual([
|
||||
'third:spaghetti',
|
||||
|
||||
'first:mozzarella',
|
||||
'first:roquefort',
|
||||
|
||||
'second:shiraz',
|
||||
'third:spaghetti',
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
@ -8,20 +8,31 @@
|
|||
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { useCallback } from 'react';
|
||||
import type { TagFilter } from '../../../../common/endpoint/service/artifacts/utils';
|
||||
import {
|
||||
isOwnerSpaceIdTag,
|
||||
isFilterProcessDescendantsTag,
|
||||
isPolicySelectionTag,
|
||||
} from '../../../../common/endpoint/service/artifacts/utils';
|
||||
|
||||
type TagFiltersType = {
|
||||
[tagCategory in string]: TagFilter;
|
||||
};
|
||||
interface TagFiltersType {
|
||||
[tagCategory: string]: TagFilter;
|
||||
}
|
||||
|
||||
type GetTagsUpdatedBy<TagFilters> = (tagsToUpdate: keyof TagFilters, newTags: string[]) => string[];
|
||||
type GetTagsUpdatedBy<TagFilters> = (tagType: keyof TagFilters, newTags: string[]) => string[];
|
||||
|
||||
const DEFAULT_FILTERS = Object.freeze({
|
||||
policySelection: isPolicySelectionTag,
|
||||
processDescendantsFiltering: isFilterProcessDescendantsTag,
|
||||
ownerSpaceId: isOwnerSpaceIdTag,
|
||||
} as const);
|
||||
|
||||
/**
|
||||
* A hook to be used to generate a new `tags` array that contains multiple 'categories' of tags,
|
||||
* e.g. policy assignment, some special settings, in a desired order.
|
||||
* A hook that returns a callback for using in updating the complete list of `tags` on an artifact.
|
||||
* The callback will replace a given type of tag with new set of values - example: update the list
|
||||
* of tags on an artifact with a new list of policy assignment tags.
|
||||
*
|
||||
* The hook excepts a `filter` object that contain a simple filter function for every tag
|
||||
* category. The `filter` object should contain the filters in the same ORDER as the categories
|
||||
* should appear in the `tags` array.
|
||||
* The hook uses a `filter` object (can be overwritten on input) that contain a simple filter
|
||||
* function that is used to identify tags for that category.
|
||||
*
|
||||
* ```
|
||||
* const FILTERS_IN_ORDER = { // preferably defined out of the component
|
||||
|
@ -42,25 +53,21 @@ type GetTagsUpdatedBy<TagFilters> = (tagsToUpdate: keyof TagFilters, newTags: st
|
|||
* @param filters
|
||||
* @returns `getTagsUpdatedBy(tagCategory, ['new', 'tags'])`
|
||||
*/
|
||||
export const useGetUpdatedTags = <TagFilters extends TagFiltersType>(
|
||||
export const useGetUpdatedTags = <TagFilters extends TagFiltersType = typeof DEFAULT_FILTERS>(
|
||||
exception: Partial<Pick<ExceptionListItemSchema, 'tags'>>,
|
||||
filters: TagFilters
|
||||
): {
|
||||
filters: TagFilters = DEFAULT_FILTERS as unknown as TagFilters
|
||||
): Readonly<{
|
||||
getTagsUpdatedBy: GetTagsUpdatedBy<TagFilters>;
|
||||
} => {
|
||||
}> => {
|
||||
const getTagsUpdatedBy: GetTagsUpdatedBy<TagFilters> = useCallback(
|
||||
(tagsToUpdate, newTags) => {
|
||||
const tagCategories = Object.keys(filters);
|
||||
(tagType, newTags) => {
|
||||
if (!filters[tagType]) {
|
||||
throw new Error(
|
||||
`getTagsUpdateBy() was called with an unknown tag type: ${String(tagType)}`
|
||||
);
|
||||
}
|
||||
|
||||
const arrayOfTagArrays: string[][] = tagCategories.map((category) => {
|
||||
if (tagsToUpdate === category) {
|
||||
return newTags;
|
||||
}
|
||||
|
||||
return (exception.tags ?? []).filter(filters[category]);
|
||||
});
|
||||
|
||||
return arrayOfTagArrays.flat();
|
||||
return (exception.tags ?? []).filter((tag) => !filters[tagType](tag)).concat(...newTags);
|
||||
},
|
||||
[exception, filters]
|
||||
);
|
||||
|
|
|
@ -26,6 +26,7 @@ import type { PolicyData } from '../../../../../../common/endpoint/types';
|
|||
import { GLOBAL_ARTIFACT_TAG } from '../../../../../../common/endpoint/service/artifacts';
|
||||
import { ListOperatorEnum, ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants';
|
||||
import type { IHttpFetchError } from '@kbn/core/public';
|
||||
|
||||
jest.mock('../../../../../common/hooks/use_license', () => {
|
||||
const licenseServiceInstance = {
|
||||
|
@ -491,7 +492,7 @@ describe('blocklist form', () => {
|
|||
expect(screen.getByTestId('blocklist-form-effectedPolicies-global')).toBeEnabled();
|
||||
});
|
||||
|
||||
it('should correctly edit policies', async () => {
|
||||
it('should correctly edit policies and retain all other tags', async () => {
|
||||
const policies: PolicyData[] = [
|
||||
{
|
||||
id: 'policy-id-123',
|
||||
|
@ -502,7 +503,7 @@ describe('blocklist form', () => {
|
|||
name: 'some-policy-456',
|
||||
},
|
||||
] as PolicyData[];
|
||||
render(createProps({ policies }));
|
||||
render(createProps({ policies, item: createItem({ tags: ['some:random_tag'] }) }));
|
||||
const byPolicyButton = screen.getByTestId('blocklist-form-effectedPolicies-perPolicy');
|
||||
await user.click(byPolicyButton);
|
||||
expect(byPolicyButton).toBeEnabled();
|
||||
|
@ -510,10 +511,10 @@ describe('blocklist form', () => {
|
|||
await user.click(screen.getByText(policies[1].name));
|
||||
const expected = createOnChangeArgs({
|
||||
item: createItem({
|
||||
tags: [`policy:${policies[1].id}`],
|
||||
tags: ['some:random_tag', `policy:${policies[1].id}`],
|
||||
}),
|
||||
});
|
||||
expect(onChangeSpy).toHaveBeenCalledWith(expected);
|
||||
expect(onChangeSpy).toHaveBeenLastCalledWith(expected);
|
||||
});
|
||||
|
||||
it('should correctly retain selected policies when toggling between global/by policy', async () => {
|
||||
|
@ -567,4 +568,11 @@ describe('blocklist form', () => {
|
|||
});
|
||||
expect(onChangeSpy).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
|
||||
it('should display submit errors', async () => {
|
||||
const message = 'foo - something went wrong';
|
||||
const { getByTestId } = render(createProps({ error: new Error(message) as IHttpFetchError }));
|
||||
|
||||
expect(getByTestId('blocklist-form-submitError').textContent).toMatch(message);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,6 +29,7 @@ import { isOneOfOperator, isOperator } from '@kbn/securitysolution-list-utils';
|
|||
import { uniq } from 'lodash';
|
||||
|
||||
import { ListOperatorEnum, ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { FormattedError } from '../../../../components/formatted_error';
|
||||
import { OS_TITLES } from '../../../../common/translations';
|
||||
import type {
|
||||
ArtifactFormComponentOnChangeCallbackProps,
|
||||
|
@ -62,6 +63,7 @@ import {
|
|||
} from '../../../../../../common/endpoint/service/artifacts';
|
||||
import type { PolicyData } from '../../../../../../common/endpoint/types';
|
||||
import { useTestIdGenerator } from '../../../../hooks/use_test_id_generator';
|
||||
import { useGetUpdatedTags } from '../../../../hooks/artifacts';
|
||||
|
||||
const testIdPrefix = 'blocklist-form';
|
||||
|
||||
|
@ -113,7 +115,7 @@ function isValid(itemValidation: ItemValidation): boolean {
|
|||
|
||||
// eslint-disable-next-line react/display-name
|
||||
export const BlockListForm = memo<ArtifactFormComponentProps>(
|
||||
({ item, policies, policiesIsLoading, onChange, mode }) => {
|
||||
({ item, policies, policiesIsLoading, onChange, mode, error: submitError }) => {
|
||||
const [nameVisited, setNameVisited] = useState(false);
|
||||
const [valueVisited, setValueVisited] = useState({ value: false }); // Use object to trigger re-render
|
||||
const warningsRef = useRef<ItemValidation>({ name: {}, value: {} });
|
||||
|
@ -123,6 +125,7 @@ export const BlockListForm = memo<ArtifactFormComponentProps>(
|
|||
const isGlobal = useMemo(() => isArtifactGlobal(item), [item]);
|
||||
const [wasByPolicy, setWasByPolicy] = useState(!isArtifactGlobal(item));
|
||||
const [hasFormChanged, setHasFormChanged] = useState(false);
|
||||
const { getTagsUpdatedBy } = useGetUpdatedTags(item);
|
||||
|
||||
const showAssignmentSection = useMemo(() => {
|
||||
return (
|
||||
|
@ -546,8 +549,7 @@ export const BlockListForm = memo<ArtifactFormComponentProps>(
|
|||
|
||||
const handleOnPolicyChange = useCallback(
|
||||
(change: EffectedPolicySelection) => {
|
||||
const tags = getArtifactTagsByPolicySelection(change);
|
||||
|
||||
const tags = getTagsUpdatedBy('policySelection', getArtifactTagsByPolicySelection(change));
|
||||
const nextItem = { ...item, tags };
|
||||
|
||||
// Preserve old selected policies when switching to global
|
||||
|
@ -561,11 +563,19 @@ export const BlockListForm = memo<ArtifactFormComponentProps>(
|
|||
});
|
||||
setHasFormChanged(true);
|
||||
},
|
||||
[validateValues, onChange, item]
|
||||
[getTagsUpdatedBy, item, validateValues, onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiForm component="div">
|
||||
<EuiForm
|
||||
component="div"
|
||||
error={
|
||||
submitError ? (
|
||||
<FormattedError error={submitError} data-test-subj={getTestId('submitError')} />
|
||||
) : undefined
|
||||
}
|
||||
isInvalid={!!submitError}
|
||||
>
|
||||
<EuiTitle size="xs">
|
||||
<h3>{DETAILS_HEADER}</h3>
|
||||
</EuiTitle>
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
FILTER_PROCESS_DESCENDANTS_TAG,
|
||||
GLOBAL_ARTIFACT_TAG,
|
||||
} from '../../../../../../common/endpoint/service/artifacts/constants';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
|
||||
jest.mock('../../../../../common/lib/kibana');
|
||||
jest.mock('../../../../../common/containers/source');
|
||||
|
@ -365,6 +366,25 @@ describe('Event filter form', () => {
|
|||
// 'true'
|
||||
// );
|
||||
});
|
||||
|
||||
it('should preserve other tags when updating artifact assignment', async () => {
|
||||
formProps.item.tags = ['some:random_tag'];
|
||||
render();
|
||||
const policyId = formProps.policies[0].id;
|
||||
// move to per-policy and select the first
|
||||
await userEvent.click(
|
||||
renderResult.getByTestId('eventFilters-form-effectedPolicies-perPolicy')
|
||||
);
|
||||
await userEvent.click(renderResult.getByTestId(`policy-${policyId}`));
|
||||
|
||||
expect(formProps.onChange).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
item: expect.objectContaining({
|
||||
tags: ['some:random_tag', 'policy:id-0'],
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Policy section with downgraded license', () => {
|
||||
|
@ -520,8 +540,8 @@ describe('Event filter form', () => {
|
|||
rerenderWithLatestProps();
|
||||
await userEvent.click(renderResult.getByTestId(`${formPrefix}-effectedPolicies-global`));
|
||||
expect(latestUpdatedItem.tags).toStrictEqual([
|
||||
GLOBAL_ARTIFACT_TAG,
|
||||
FILTER_PROCESS_DESCENDANTS_TAG,
|
||||
GLOBAL_ARTIFACT_TAG,
|
||||
]);
|
||||
|
||||
rerenderWithLatestProps();
|
||||
|
@ -542,8 +562,8 @@ describe('Event filter form', () => {
|
|||
renderResult.getByTestId('eventFilters-form-effectedPolicies-perPolicy')
|
||||
);
|
||||
expect(latestUpdatedItem.tags).toStrictEqual([
|
||||
...perPolicyTags,
|
||||
FILTER_PROCESS_DESCENDANTS_TAG,
|
||||
...perPolicyTags,
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -884,5 +904,13 @@ describe('Event filter form', () => {
|
|||
)
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should display form submission errors', () => {
|
||||
const message = 'oh oh - error';
|
||||
formProps.error = new Error(message) as IHttpFetchError;
|
||||
const { getByTestId } = render();
|
||||
|
||||
expect(getByTestId('eventFilters-form-submitError').textContent).toMatch(message);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -41,6 +41,7 @@ import {
|
|||
} from './translations';
|
||||
import type { ArtifactFormComponentProps } from '../../../../components/artifact_list_page';
|
||||
import { FormattedError } from '../../../../components/formatted_error';
|
||||
import { useGetUpdatedTags } from '../../../../hooks/artifacts';
|
||||
|
||||
export const testIdPrefix = 'hostIsolationExceptions-form';
|
||||
|
||||
|
@ -66,8 +67,8 @@ export const HostIsolationExceptionsForm = memo<ArtifactFormComponentProps>(
|
|||
const [hasBeenInputIpVisited, setHasBeenInputIpVisited] = useState(false);
|
||||
const [hasNameError, setHasNameError] = useState(!exception.name);
|
||||
const [hasIpError, setHasIpError] = useState(!ipEntry.value);
|
||||
|
||||
const getTestId = useTestIdGenerator(testIdPrefix);
|
||||
const { getTagsUpdatedBy } = useGetUpdatedTags(exception);
|
||||
|
||||
const [selectedPolicies, setSelectedPolicies] = useState<EffectedPolicySelection>({
|
||||
isGlobal: isArtifactGlobal(exception),
|
||||
|
@ -137,11 +138,14 @@ export const HostIsolationExceptionsForm = memo<ArtifactFormComponentProps>(
|
|||
setSelectedPolicies(selection);
|
||||
}
|
||||
|
||||
notifyOfChange({
|
||||
tags: getArtifactTagsByPolicySelection(selection),
|
||||
});
|
||||
const tags = getTagsUpdatedBy(
|
||||
'policySelection',
|
||||
getArtifactTagsByPolicySelection(selection)
|
||||
);
|
||||
|
||||
notifyOfChange({ tags });
|
||||
},
|
||||
[notifyOfChange]
|
||||
[getTagsUpdatedBy, notifyOfChange]
|
||||
);
|
||||
|
||||
const handleOnDescriptionChange = useCallback(
|
||||
|
@ -254,7 +258,14 @@ export const HostIsolationExceptionsForm = memo<ArtifactFormComponentProps>(
|
|||
return (
|
||||
<EuiForm
|
||||
component="div"
|
||||
error={error && <FormattedError error={error} />}
|
||||
error={
|
||||
error && (
|
||||
<FormattedError
|
||||
error={error}
|
||||
data-test-subj={'hostIsolationExceptions-form-submitError'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
isInvalid={!!error}
|
||||
data-test-subj="hostIsolationExceptions-form"
|
||||
>
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
isEffectedPolicySelected,
|
||||
} from '../../../../../components/effected_policy_select/test_utils';
|
||||
import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../../../../../../../common/endpoint/service/artifacts';
|
||||
import type { HttpFetchOptionsWithPath } from '@kbn/core/public';
|
||||
import type { HttpFetchOptionsWithPath, IHttpFetchError } from '@kbn/core/public';
|
||||
import { testIdPrefix } from '../form';
|
||||
|
||||
jest.mock('../../../../../../common/components/user_privileges');
|
||||
|
@ -295,5 +295,25 @@ describe('When on the host isolation exceptions entry form', () => {
|
|||
renderResult.queryByTestId(`${testIdPrefix}-effectedPolicies-policiesSelectable`)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
// FIXME:PT not sure why this test is not working but I have spent several hours now on it and can't
|
||||
// figure it out. Skipping for now and will try to come back to it.
|
||||
it.skip('should display form submission errors', async () => {
|
||||
const error = new Error('oh oh - error') as IHttpFetchError;
|
||||
exceptionsApiMock.responseProvider.exceptionUpdate.mockImplementation(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
const { getByTestId } = await render();
|
||||
await userEvent.click(getByTestId('hostIsolationExceptionsListPage-flyout-submitButton'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(exceptionsApiMock.responseProvider.exceptionUpdate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(getByTestId('hostIsolationExceptions-form-submitError').textContent).toMatch(
|
||||
'oh oh - error'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -26,6 +26,7 @@ import { forceHTMLElementOffsetWidth } from '../../../../components/effected_pol
|
|||
import type { PolicyData, TrustedAppConditionEntry } from '../../../../../../common/endpoint/types';
|
||||
|
||||
import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
|
||||
jest.mock('../../../../../common/hooks/use_license', () => {
|
||||
const licenseServiceInstance = {
|
||||
|
@ -194,6 +195,14 @@ describe('Trusted apps form', () => {
|
|||
cleanup();
|
||||
});
|
||||
|
||||
it('should display form submission errors', () => {
|
||||
const message = 'oh oh - failed';
|
||||
formProps.error = new Error(message) as IHttpFetchError;
|
||||
render();
|
||||
|
||||
expect(renderResult.getByTestId(`${formPrefix}-submitError`).textContent).toMatch(message);
|
||||
});
|
||||
|
||||
describe('Details and Conditions', () => {
|
||||
beforeEach(() => render());
|
||||
|
||||
|
@ -409,6 +418,22 @@ describe('Trusted apps form', () => {
|
|||
render();
|
||||
expect(renderResult.queryByTestId('loading-spinner')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should preserve other tags when policies are updated', async () => {
|
||||
formProps.item.tags = ['some:unknown_tag'];
|
||||
const policyId = formProps.policies[0].id;
|
||||
render();
|
||||
await userEvent.click(renderResult.getByTestId(`${formPrefix}-effectedPolicies-perPolicy`));
|
||||
await userEvent.click(renderResult.getByTestId(`policy-${policyId}`));
|
||||
|
||||
expect(formProps.onChange).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
item: expect.objectContaining({
|
||||
tags: ['some:unknown_tag', `policy:${policyId}`],
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the Policy Selection area when the license downgrades to gold or below', () => {
|
||||
|
|
|
@ -29,6 +29,8 @@ import {
|
|||
OperatingSystem,
|
||||
} from '@kbn/securitysolution-utils';
|
||||
import { WildCardWithWrongOperatorCallout } from '@kbn/securitysolution-exception-list-components';
|
||||
import { useGetUpdatedTags } from '../../../../hooks/artifacts';
|
||||
import { FormattedError } from '../../../../components/formatted_error';
|
||||
import type {
|
||||
TrustedAppConditionEntry,
|
||||
NewTrustedApp,
|
||||
|
@ -235,7 +237,7 @@ const defaultConditionEntry = (): TrustedAppConditionEntry<ConditionEntryField.H
|
|||
});
|
||||
|
||||
export const TrustedAppsForm = memo<ArtifactFormComponentProps>(
|
||||
({ item, policies, policiesIsLoading, onChange, mode }) => {
|
||||
({ item, policies, policiesIsLoading, onChange, mode, error: submitError }) => {
|
||||
const getTestId = useTestIdGenerator('trustedApps-form');
|
||||
const [visited, setVisited] = useState<
|
||||
Partial<{
|
||||
|
@ -248,6 +250,7 @@ export const TrustedAppsForm = memo<ArtifactFormComponentProps>(
|
|||
const isGlobal = useMemo(() => isArtifactGlobal(item), [item]);
|
||||
const [wasByPolicy, setWasByPolicy] = useState(!isArtifactGlobal(item));
|
||||
const [hasFormChanged, setHasFormChanged] = useState(false);
|
||||
const { getTagsUpdatedBy } = useGetUpdatedTags(item);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasFormChanged && item.tags) {
|
||||
|
@ -301,9 +304,9 @@ export const TrustedAppsForm = memo<ArtifactFormComponentProps>(
|
|||
|
||||
const handleOnPolicyChange = useCallback(
|
||||
(change: EffectedPolicySelection) => {
|
||||
const tags = getArtifactTagsByPolicySelection(change);
|
||||
|
||||
const tags = getTagsUpdatedBy('policySelection', getArtifactTagsByPolicySelection(change));
|
||||
const nextItem = { ...item, tags };
|
||||
|
||||
// Preserve old selected policies when switching to global
|
||||
if (!change.isGlobal) {
|
||||
setSelectedPolicies(change.selected);
|
||||
|
@ -311,7 +314,7 @@ export const TrustedAppsForm = memo<ArtifactFormComponentProps>(
|
|||
processChanged(nextItem);
|
||||
setHasFormChanged(true);
|
||||
},
|
||||
[item, processChanged]
|
||||
[getTagsUpdatedBy, item, processChanged]
|
||||
);
|
||||
|
||||
const handleOnNameOrDescriptionChange = useCallback<
|
||||
|
@ -480,7 +483,16 @@ export const TrustedAppsForm = memo<ArtifactFormComponentProps>(
|
|||
}, [item]);
|
||||
|
||||
return (
|
||||
<EuiForm component="div" data-test-subj={getTestId('')}>
|
||||
<EuiForm
|
||||
component="div"
|
||||
data-test-subj={getTestId('')}
|
||||
error={
|
||||
submitError ? (
|
||||
<FormattedError error={submitError} data-test-subj={getTestId('submitError')} />
|
||||
) : undefined
|
||||
}
|
||||
isInvalid={!!submitError}
|
||||
>
|
||||
<EuiTitle size="xs">
|
||||
<h3>{DETAILS_HEADER}</h3>
|
||||
</EuiTitle>
|
||||
|
|
|
@ -12,6 +12,8 @@ import type { Role } from '@kbn/security-plugin/common';
|
|||
import type { ToolingLog } from '@kbn/tooling-log';
|
||||
import { inspect } from 'util';
|
||||
import type { AxiosError } from 'axios';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { dump } from './utils';
|
||||
import type { EndpointSecurityRoleDefinitions } from './roles_users';
|
||||
import { getAllEndpointSecurityRoles } from './roles_users';
|
||||
import { catchAxiosErrorFormatAndThrow } from '../../../common/endpoint/format_axios_error';
|
||||
|
@ -71,7 +73,7 @@ export class RoleAndUserLoader<R extends Record<string, Role> = Record<string, R
|
|||
};
|
||||
}
|
||||
|
||||
async load(name: keyof R): Promise<LoadedRoleAndUser> {
|
||||
public async load(name: keyof R): Promise<LoadedRoleAndUser> {
|
||||
const role = this.roles[name];
|
||||
|
||||
if (!role) {
|
||||
|
@ -85,7 +87,7 @@ export class RoleAndUserLoader<R extends Record<string, Role> = Record<string, R
|
|||
return this.create(role);
|
||||
}
|
||||
|
||||
async loadAll(): Promise<Record<keyof R, LoadedRoleAndUser>> {
|
||||
public async loadAll(): Promise<Record<keyof R, LoadedRoleAndUser>> {
|
||||
const response = {} as Record<keyof R, LoadedRoleAndUser>;
|
||||
|
||||
for (const [name, role] of Object.entries(this.roles)) {
|
||||
|
@ -108,10 +110,42 @@ export class RoleAndUserLoader<R extends Record<string, Role> = Record<string, R
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes both the role and user by a given name.
|
||||
* @param roleAndUserName
|
||||
*/
|
||||
public async delete(roleAndUserName: string): Promise<void> {
|
||||
const roleDeleteResponse = await this.kbnClient.request({
|
||||
method: 'DELETE',
|
||||
path: `/api/security/role/${roleAndUserName}`,
|
||||
headers: { ...COMMON_API_HEADERS },
|
||||
});
|
||||
|
||||
this.logger.info(`Deleted role ${roleAndUserName}`);
|
||||
this.logger.verbose(dump(roleDeleteResponse));
|
||||
|
||||
const userDeleteResponse = await this.kbnClient.request({
|
||||
method: 'DELETE',
|
||||
path: `/internal/security/users/${roleAndUserName}`,
|
||||
headers: { ...COMMON_API_HEADERS },
|
||||
});
|
||||
|
||||
this.logger.info(`Deleted user ${roleAndUserName}`);
|
||||
this.logger.verbose(dump(userDeleteResponse));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a copy of a predefined Role definition
|
||||
* @param name
|
||||
*/
|
||||
public getPreDefinedRole(name: keyof R): Role {
|
||||
return cloneDeep(this.roles[name]);
|
||||
}
|
||||
|
||||
protected async createRole(role: Role): Promise<void> {
|
||||
const { name: roleName, ...roleDefinition } = role;
|
||||
|
||||
this.logger.debug(`creating role:`, roleDefinition);
|
||||
this.logger.debug(`creating role [${roleName}]:`, dump(roleDefinition, 10));
|
||||
|
||||
await this.kbnClient
|
||||
.request({
|
||||
|
@ -144,7 +178,7 @@ export class RoleAndUserLoader<R extends Record<string, Role> = Record<string, R
|
|||
email: '',
|
||||
};
|
||||
|
||||
this.logger.debug(`creating user:`, user);
|
||||
this.logger.debug(`creating user:`, dump(user, 10));
|
||||
|
||||
await this.kbnClient
|
||||
.request({
|
||||
|
|
|
@ -7,8 +7,14 @@
|
|||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EndpointError } from '../../common/endpoint/errors';
|
||||
|
||||
export const ENDPOINT_AUTHZ_ERROR_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.errors.noEndpointAuthzApiErrorMessage',
|
||||
{ defaultMessage: 'Endpoint authorization failure' }
|
||||
);
|
||||
|
||||
export class NotFoundError extends EndpointError {}
|
||||
|
||||
export class EndpointAppContentServicesNotSetUpError extends EndpointError {
|
||||
|
@ -25,6 +31,6 @@ export class EndpointAppContentServicesNotStartedError extends EndpointError {
|
|||
|
||||
export class EndpointAuthorizationError extends EndpointError {
|
||||
constructor(meta?: unknown) {
|
||||
super('Endpoint authorization failure', meta);
|
||||
super(ENDPOINT_AUTHZ_ERROR_MESSAGE, meta);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -150,7 +150,11 @@ export const createMockEndpointAppContextService = (
|
|||
savedObjects: createSavedObjectsClientFactoryMock({ savedObjectsServiceStart }).service,
|
||||
isServerless: jest.fn().mockReturnValue(false),
|
||||
getInternalEsClient: jest.fn().mockReturnValue(esClient),
|
||||
getActiveSpace: jest.fn(async () => DEFAULT_SPACE_ID),
|
||||
getActiveSpace: jest.fn(async () => ({
|
||||
id: DEFAULT_SPACE_ID,
|
||||
name: 'default',
|
||||
disabledFeatures: [],
|
||||
})),
|
||||
} as unknown as jest.Mocked<EndpointAppContextService>;
|
||||
};
|
||||
|
||||
|
|
|
@ -503,6 +503,8 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient
|
|||
: {}),
|
||||
};
|
||||
|
||||
this.log.debug(() => `creating action request document:\n${stringify(doc)}`);
|
||||
|
||||
try {
|
||||
const logsEndpointActionsResult = await this.options.esClient.index<LogsEndpointAction>(
|
||||
{
|
||||
|
@ -526,6 +528,9 @@ export abstract class ResponseActionsClientImpl implements ResponseActionsClient
|
|||
return doc;
|
||||
} catch (err) {
|
||||
this.sendActionCreationErrorTelemetry(actionRequest.command, err);
|
||||
this.log.debug(
|
||||
() => `attempt to index document into ${ENDPOINT_ACTIONS_INDEX} failed:\n${stringify(err)}`
|
||||
);
|
||||
|
||||
if (!(err instanceof ResponseActionsClientError)) {
|
||||
throw new ResponseActionsClientError(
|
||||
|
|
|
@ -76,7 +76,10 @@ export const getExceptionsPreUpdateItemHandler = (
|
|||
endpointAppContextService,
|
||||
request
|
||||
);
|
||||
const validatedItem = await hostIsolationExceptionValidator.validatePreUpdateItem(data);
|
||||
const validatedItem = await hostIsolationExceptionValidator.validatePreUpdateItem(
|
||||
data,
|
||||
currentSavedItem
|
||||
);
|
||||
hostIsolationExceptionValidator.notifyFeatureUsage(
|
||||
data as ExceptionItemLikeOptions,
|
||||
'HOST_ISOLATION_EXCEPTION_BY_POLICY'
|
||||
|
@ -105,7 +108,10 @@ export const getExceptionsPreUpdateItemHandler = (
|
|||
endpointAppContextService,
|
||||
request
|
||||
);
|
||||
const validatedItem = await endpointExceptionValidator.validatePreUpdateItem(data);
|
||||
const validatedItem = await endpointExceptionValidator.validatePreUpdateItem(
|
||||
data,
|
||||
currentSavedItem
|
||||
);
|
||||
endpointExceptionValidator.notifyFeatureUsage(
|
||||
data as ExceptionItemLikeOptions,
|
||||
'ENDPOINT_EXCEPTIONS'
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
|
||||
import {
|
||||
createMockEndpointAppContextService,
|
||||
createMockEndpointAppContextServiceSetupContract,
|
||||
createMockEndpointAppContextServiceStartContract,
|
||||
} from '../../../endpoint/mocks';
|
||||
|
@ -22,6 +23,10 @@ import {
|
|||
GLOBAL_ARTIFACT_TAG,
|
||||
} from '../../../../common/endpoint/service/artifacts';
|
||||
import { securityMock } from '@kbn/security-plugin/server/mocks';
|
||||
import { setArtifactOwnerSpaceId } from '../../../../common/endpoint/service/artifacts/utils';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
|
||||
import { getEndpointAuthzInitialStateMock } from '../../../../common/endpoint/service/authz/mocks';
|
||||
import type { EndpointAuthz } from '../../../../common/endpoint/types/authz';
|
||||
|
||||
describe('When using Artifacts Exceptions BaseValidator', () => {
|
||||
let endpointAppContextServices: EndpointAppContextService;
|
||||
|
@ -66,6 +71,11 @@ describe('When using Artifacts Exceptions BaseValidator', () => {
|
|||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// @ts-expect-error setting variable to undefined
|
||||
validator = undefined;
|
||||
});
|
||||
|
||||
it('should use default endpoint authz (no access) when `request` is not provided', async () => {
|
||||
const baseValidator = new BaseValidatorMock(endpointAppContextServices);
|
||||
|
||||
|
@ -186,4 +196,100 @@ describe('When using Artifacts Exceptions BaseValidator', () => {
|
|||
false
|
||||
);
|
||||
});
|
||||
|
||||
describe('with space awareness', () => {
|
||||
const noGlobalArtifactManagementAuthzMessage =
|
||||
'EndpointArtifactError: Endpoint authorization failure. Management of "ownerSpaceId" tag requires global artifact management privilege';
|
||||
let authzMock: EndpointAuthz;
|
||||
|
||||
beforeEach(() => {
|
||||
authzMock = getEndpointAuthzInitialStateMock();
|
||||
endpointAppContextServices = createMockEndpointAppContextService();
|
||||
// @ts-expect-error updating a readonly field
|
||||
endpointAppContextServices.experimentalFeatures.endpointManagementSpaceAwarenessEnabled =
|
||||
true;
|
||||
(endpointAppContextServices.getEndpointAuthz as jest.Mock).mockResolvedValue(authzMock);
|
||||
setArtifactOwnerSpaceId(exceptionLikeItem, DEFAULT_SPACE_ID);
|
||||
validator = new BaseValidatorMock(endpointAppContextServices, kibanaRequest);
|
||||
});
|
||||
|
||||
describe('#validateCreateOnwerSpaceIds()', () => {
|
||||
it('should error if adding an spaceOwnerId but has no global artifact management authz', async () => {
|
||||
setArtifactOwnerSpaceId(exceptionLikeItem, 'foo');
|
||||
authzMock.canManageGlobalArtifacts = false;
|
||||
|
||||
await expect(validator._validateCreateOwnerSpaceIds(exceptionLikeItem)).rejects.toThrow(
|
||||
noGlobalArtifactManagementAuthzMessage
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow spaceOwnerId tag matching current space even if no global artifact management authz', async () => {
|
||||
authzMock.canManageGlobalArtifacts = false;
|
||||
|
||||
await expect(
|
||||
validator._validateCreateOwnerSpaceIds(exceptionLikeItem)
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow additional spaceOwnerId tags if user has global artifact management authz', async () => {
|
||||
setArtifactOwnerSpaceId(exceptionLikeItem, 'foo');
|
||||
|
||||
await expect(
|
||||
validator._validateCreateOwnerSpaceIds(exceptionLikeItem)
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not error if feature flag is disabled', async () => {
|
||||
// @ts-expect-error updating a readonly field
|
||||
endpointAppContextServices.experimentalFeatures.endpointManagementSpaceAwarenessEnabled =
|
||||
false;
|
||||
authzMock.canManageGlobalArtifacts = false;
|
||||
setArtifactOwnerSpaceId(exceptionLikeItem, 'foo');
|
||||
setArtifactOwnerSpaceId(exceptionLikeItem, 'bar');
|
||||
|
||||
await expect(
|
||||
validator._validateCreateOwnerSpaceIds(exceptionLikeItem)
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#validateUpdateOnwerSpaceIds()', () => {
|
||||
let savedExceptionLikeItem: ExceptionItemLikeOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
savedExceptionLikeItem = createExceptionItemLikeOptionsMock();
|
||||
setArtifactOwnerSpaceId(exceptionLikeItem, DEFAULT_SPACE_ID);
|
||||
});
|
||||
|
||||
it('should error if changing spaceOwnerId but has no global artifact management authz', async () => {
|
||||
authzMock.canManageGlobalArtifacts = false;
|
||||
setArtifactOwnerSpaceId(exceptionLikeItem, 'foo');
|
||||
|
||||
await expect(
|
||||
validator._validateUpdateOwnerSpaceIds(exceptionLikeItem, savedExceptionLikeItem)
|
||||
).rejects.toThrow(noGlobalArtifactManagementAuthzMessage);
|
||||
});
|
||||
|
||||
it('should allow changes to spaceOwnerId tags if user has global artifact management authz', async () => {
|
||||
setArtifactOwnerSpaceId(exceptionLikeItem, 'foo');
|
||||
|
||||
await expect(
|
||||
validator._validateUpdateOwnerSpaceIds(exceptionLikeItem, savedExceptionLikeItem)
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not error if feature flag is disabled', async () => {
|
||||
// @ts-expect-error updating a readonly field
|
||||
endpointAppContextServices.experimentalFeatures.endpointManagementSpaceAwarenessEnabled =
|
||||
false;
|
||||
authzMock.canManageGlobalArtifacts = false;
|
||||
setArtifactOwnerSpaceId(exceptionLikeItem, 'foo');
|
||||
setArtifactOwnerSpaceId(exceptionLikeItem, 'bar');
|
||||
|
||||
await expect(
|
||||
validator._validateUpdateOwnerSpaceIds(exceptionLikeItem, savedExceptionLikeItem)
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,7 +11,12 @@ import { isEqual } from 'lodash/fp';
|
|||
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { OperatingSystem } from '@kbn/securitysolution-utils';
|
||||
|
||||
import { setArtifactOwnerSpaceId } from '../../../../common/endpoint/service/artifacts/utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ENDPOINT_AUTHZ_ERROR_MESSAGE } from '../../../endpoint/errors';
|
||||
import {
|
||||
getArtifactOwnerSpaceIds,
|
||||
setArtifactOwnerSpaceId,
|
||||
} from '../../../../common/endpoint/service/artifacts/utils';
|
||||
import type { FeatureKeys } from '../../../endpoint/services';
|
||||
import type { EndpointAuthz } from '../../../../common/endpoint/types/authz';
|
||||
import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
|
||||
|
@ -24,6 +29,14 @@ import {
|
|||
import { EndpointArtifactExceptionValidationError } from './errors';
|
||||
import { EndpointExceptionsValidationError } from './endpoint_exception_errors';
|
||||
|
||||
const NO_GLOBAL_ARTIFACT_AUTHZ_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.baseValidator.noGlobalArtifactAuthzApiMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'Management of "ownerSpaceId" tag requires global artifact management privilege',
|
||||
}
|
||||
);
|
||||
|
||||
export const BasicEndpointExceptionDataSchema = schema.object(
|
||||
{
|
||||
// must have a name
|
||||
|
@ -87,7 +100,7 @@ export class BaseValidator {
|
|||
|
||||
protected async validateHasPrivilege(privilege: keyof EndpointAuthz): Promise<void> {
|
||||
if (!(await this.endpointAuthzPromise)[privilege]) {
|
||||
throw new EndpointArtifactExceptionValidationError('Endpoint authorization failure', 403);
|
||||
throw new EndpointArtifactExceptionValidationError(ENDPOINT_AUTHZ_ERROR_MESSAGE, 403);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,13 +114,13 @@ export class BaseValidator {
|
|||
|
||||
protected async validateCanManageEndpointArtifacts(): Promise<void> {
|
||||
if (!(await this.endpointAuthzPromise).canAccessEndpointManagement) {
|
||||
throw new EndpointArtifactExceptionValidationError('Endpoint authorization failure', 403);
|
||||
throw new EndpointArtifactExceptionValidationError(ENDPOINT_AUTHZ_ERROR_MESSAGE, 403);
|
||||
}
|
||||
}
|
||||
|
||||
protected async validateCanIsolateHosts(): Promise<void> {
|
||||
if (!(await this.endpointAuthzPromise).canIsolateHost) {
|
||||
throw new EndpointArtifactExceptionValidationError('Endpoint authorization failure', 403);
|
||||
throw new EndpointArtifactExceptionValidationError(ENDPOINT_AUTHZ_ERROR_MESSAGE, 403);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -198,6 +211,65 @@ export class BaseValidator {
|
|||
return false;
|
||||
}
|
||||
|
||||
protected async validateUpdateOwnerSpaceIds(
|
||||
updatedItem: Partial<Pick<ExceptionListItemSchema, 'tags'>>,
|
||||
currentItem: Pick<ExceptionListItemSchema, 'tags'>
|
||||
): Promise<void> {
|
||||
if (
|
||||
this.endpointAppContext.experimentalFeatures.endpointManagementSpaceAwarenessEnabled &&
|
||||
this.wasOwnerSpaceIdTagsChanged(updatedItem, currentItem) &&
|
||||
!(await this.endpointAuthzPromise).canManageGlobalArtifacts
|
||||
) {
|
||||
throw new EndpointArtifactExceptionValidationError(
|
||||
`Endpoint authorization failure. ${NO_GLOBAL_ARTIFACT_AUTHZ_MESSAGE}`,
|
||||
403
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected async validateCreateOwnerSpaceIds(item: ExceptionItemLikeOptions): Promise<void> {
|
||||
if (
|
||||
this.endpointAppContext.experimentalFeatures.endpointManagementSpaceAwarenessEnabled &&
|
||||
item.tags &&
|
||||
item.tags.length > 0
|
||||
) {
|
||||
if ((await this.endpointAuthzPromise).canManageGlobalArtifacts) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ownerSpaceIds = getArtifactOwnerSpaceIds(item);
|
||||
const activeSpaceId = await this.getActiveSpaceId();
|
||||
|
||||
if (
|
||||
ownerSpaceIds.length > 1 ||
|
||||
(ownerSpaceIds.length === 1 && ownerSpaceIds[0] !== activeSpaceId)
|
||||
) {
|
||||
throw new EndpointArtifactExceptionValidationError(
|
||||
`Endpoint authorization failure. ${NO_GLOBAL_ARTIFACT_AUTHZ_MESSAGE}`,
|
||||
403
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected wasOwnerSpaceIdTagsChanged(
|
||||
updatedItem: Partial<Pick<ExceptionListItemSchema, 'tags'>>,
|
||||
currentItem: Pick<ExceptionListItemSchema, 'tags'>
|
||||
): boolean {
|
||||
return !isEqual(getArtifactOwnerSpaceIds(updatedItem), getArtifactOwnerSpaceIds(currentItem));
|
||||
}
|
||||
|
||||
protected async getActiveSpaceId(): Promise<string> {
|
||||
if (!this.request) {
|
||||
throw new EndpointArtifactExceptionValidationError(
|
||||
'Unable to determine space id. Missing HTTP Request object',
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
return (await this.endpointAppContext.getActiveSpace(this.request)).id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the artifact item (if necessary) with a `ownerSpaceId` tag using the HTTP request's active space
|
||||
* @param item
|
||||
|
@ -207,15 +279,7 @@ export class BaseValidator {
|
|||
item: Partial<Pick<ExceptionListItemSchema, 'tags'>>
|
||||
): Promise<void> {
|
||||
if (this.endpointAppContext.experimentalFeatures.endpointManagementSpaceAwarenessEnabled) {
|
||||
if (!this.request) {
|
||||
throw new EndpointArtifactExceptionValidationError(
|
||||
'Unable to determine space id. Missing HTTP Request object',
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
const spaceId = (await this.endpointAppContext.getActiveSpace(this.request)).id;
|
||||
setArtifactOwnerSpaceId(item, spaceId);
|
||||
setArtifactOwnerSpaceId(item, await this.getActiveSpaceId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -243,6 +243,7 @@ export class BlocklistValidator extends BaseValidator {
|
|||
await this.validateBlocklistData(item);
|
||||
await this.validateCanCreateByPolicyArtifacts(item);
|
||||
await this.validateByPolicyItem(item);
|
||||
await this.validateCreateOwnerSpaceIds(item);
|
||||
|
||||
await this.setOwnerSpaceId(item);
|
||||
|
||||
|
@ -299,6 +300,7 @@ export class BlocklistValidator extends BaseValidator {
|
|||
}
|
||||
|
||||
await this.validateByPolicyItem(updatedItem);
|
||||
await this.validateUpdateOwnerSpaceIds(updatedItem, currentItem);
|
||||
|
||||
if (!hasArtifactOwnerSpaceId(_updatedItem)) {
|
||||
await this.setOwnerSpaceId(_updatedItem);
|
||||
|
|
|
@ -10,6 +10,7 @@ import type {
|
|||
UpdateExceptionListItemOptions,
|
||||
} from '@kbn/lists-plugin/server';
|
||||
import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants';
|
||||
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { hasArtifactOwnerSpaceId } from '../../../../common/endpoint/service/artifacts/utils';
|
||||
import { BaseValidator } from './base_validator';
|
||||
|
||||
|
@ -28,14 +29,19 @@ export class EndpointExceptionsValidator extends BaseValidator {
|
|||
|
||||
async validatePreCreateItem(item: CreateExceptionListItemOptions) {
|
||||
await this.validateHasWritePrivilege();
|
||||
await this.validateCreateOwnerSpaceIds(item);
|
||||
|
||||
await this.setOwnerSpaceId(item);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
async validatePreUpdateItem(item: UpdateExceptionListItemOptions) {
|
||||
async validatePreUpdateItem(
|
||||
item: UpdateExceptionListItemOptions,
|
||||
currentItem: ExceptionListItemSchema
|
||||
) {
|
||||
await this.validateHasWritePrivilege();
|
||||
await this.validateUpdateOwnerSpaceIds(item, currentItem);
|
||||
|
||||
if (!hasArtifactOwnerSpaceId(item)) {
|
||||
await this.setOwnerSpaceId(item);
|
||||
|
|
|
@ -60,6 +60,8 @@ export class EventFilterValidator extends BaseValidator {
|
|||
await this.validateByPolicyItem(item);
|
||||
}
|
||||
|
||||
await this.validateCreateOwnerSpaceIds(item);
|
||||
|
||||
await this.setOwnerSpaceId(item);
|
||||
|
||||
return item;
|
||||
|
@ -86,6 +88,7 @@ export class EventFilterValidator extends BaseValidator {
|
|||
}
|
||||
|
||||
await this.validateByPolicyItem(updatedItem);
|
||||
await this.validateUpdateOwnerSpaceIds(_updatedItem, currentItem);
|
||||
|
||||
if (!hasArtifactOwnerSpaceId(_updatedItem)) {
|
||||
await this.setOwnerSpaceId(_updatedItem);
|
||||
|
|
|
@ -12,6 +12,7 @@ import type {
|
|||
CreateExceptionListItemOptions,
|
||||
UpdateExceptionListItemOptions,
|
||||
} from '@kbn/lists-plugin/server';
|
||||
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { hasArtifactOwnerSpaceId } from '../../../../common/endpoint/service/artifacts/utils';
|
||||
import { BaseValidator, BasicEndpointExceptionDataSchema } from './base_validator';
|
||||
import { EndpointArtifactExceptionValidationError } from './errors';
|
||||
|
@ -79,6 +80,7 @@ export class HostIsolationExceptionsValidator extends BaseValidator {
|
|||
await this.validateHasWritePrivilege();
|
||||
await this.validateHostIsolationData(item);
|
||||
await this.validateByPolicyItem(item);
|
||||
await this.validateCreateOwnerSpaceIds(item);
|
||||
|
||||
await this.setOwnerSpaceId(item);
|
||||
|
||||
|
@ -86,13 +88,15 @@ export class HostIsolationExceptionsValidator extends BaseValidator {
|
|||
}
|
||||
|
||||
async validatePreUpdateItem(
|
||||
_updatedItem: UpdateExceptionListItemOptions
|
||||
_updatedItem: UpdateExceptionListItemOptions,
|
||||
currentItem: ExceptionListItemSchema
|
||||
): Promise<UpdateExceptionListItemOptions> {
|
||||
const updatedItem = _updatedItem as ExceptionItemLikeOptions;
|
||||
|
||||
await this.validateHasWritePrivilege();
|
||||
await this.validateHostIsolationData(updatedItem);
|
||||
await this.validateByPolicyItem(updatedItem);
|
||||
await this.validateUpdateOwnerSpaceIds(_updatedItem, currentItem);
|
||||
|
||||
if (!hasArtifactOwnerSpaceId(_updatedItem)) {
|
||||
await this.setOwnerSpaceId(_updatedItem);
|
||||
|
|
|
@ -45,6 +45,17 @@ export class BaseValidatorMock extends BaseValidator {
|
|||
): boolean {
|
||||
return this.wasByPolicyEffectScopeChanged(updatedItem, currentItem);
|
||||
}
|
||||
|
||||
_validateCreateOwnerSpaceIds(item: ExceptionItemLikeOptions): Promise<void> {
|
||||
return this.validateCreateOwnerSpaceIds(item);
|
||||
}
|
||||
|
||||
_validateUpdateOwnerSpaceIds(
|
||||
updatedItem: Partial<Pick<ExceptionListItemSchema, 'tags'>>,
|
||||
currentItem: Pick<ExceptionListItemSchema, 'tags'>
|
||||
): Promise<void> {
|
||||
return this.validateUpdateOwnerSpaceIds(updatedItem, currentItem);
|
||||
}
|
||||
}
|
||||
|
||||
export const createExceptionItemLikeOptionsMock = (
|
||||
|
|
|
@ -207,6 +207,7 @@ export class TrustedAppValidator extends BaseValidator {
|
|||
await this.validateTrustedAppData(item);
|
||||
await this.validateCanCreateByPolicyArtifacts(item);
|
||||
await this.validateByPolicyItem(item);
|
||||
await this.validateCreateOwnerSpaceIds(item);
|
||||
|
||||
await this.setOwnerSpaceId(item);
|
||||
|
||||
|
@ -258,6 +259,7 @@ export class TrustedAppValidator extends BaseValidator {
|
|||
}
|
||||
|
||||
await this.validateByPolicyItem(updatedItem);
|
||||
await this.validateUpdateOwnerSpaceIds(_updatedItem, currentItem);
|
||||
|
||||
if (!hasArtifactOwnerSpaceId(_updatedItem)) {
|
||||
await this.setOwnerSpaceId(_updatedItem);
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
getAllEndpointSecurityRoles,
|
||||
} from '@kbn/security-solution-plugin/scripts/endpoint/common/roles_users';
|
||||
|
||||
import { EndpointSecurityTestRolesLoader } from '@kbn/security-solution-plugin/scripts/endpoint/common/role_and_user_loader';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context_edr_workflows';
|
||||
|
||||
export const ROLE = ENDPOINT_SECURITY_ROLE_NAMES;
|
||||
|
@ -20,10 +21,17 @@ const rolesMapping = getAllEndpointSecurityRoles();
|
|||
|
||||
export function RolesUsersProvider({ getService }: FtrProviderContext) {
|
||||
const security = getService('security');
|
||||
const kbnServer = getService('kibanaServer');
|
||||
const log = getService('log');
|
||||
|
||||
return {
|
||||
/** Endpoint security test roles loader */
|
||||
loader: new EndpointSecurityTestRolesLoader(kbnServer, log),
|
||||
|
||||
/**
|
||||
* Creates an user with specific values
|
||||
* @param user
|
||||
* @deprecated use `.loader.*` methods instead
|
||||
*/
|
||||
async createUser(user: { name: string; roles: string[]; password?: string }): Promise<void> {
|
||||
const { name, roles, password } = user;
|
||||
|
@ -33,6 +41,7 @@ export function RolesUsersProvider({ getService }: FtrProviderContext) {
|
|||
/**
|
||||
* Deletes specified users by username
|
||||
* @param names[]
|
||||
* @deprecated use `.loader.*` methods instead
|
||||
*/
|
||||
async deleteUsers(names: string[]): Promise<void> {
|
||||
for (const name of names) {
|
||||
|
@ -43,6 +52,7 @@ export function RolesUsersProvider({ getService }: FtrProviderContext) {
|
|||
/**
|
||||
* Creates a role using predefined role config if defined or a custom one. It also allows define extra privileges.
|
||||
* @param options
|
||||
* @deprecated use `.loader.*` methods instead
|
||||
*/
|
||||
async createRole(options: {
|
||||
predefinedRole?: EndpointSecurityRoleNames;
|
||||
|
@ -87,6 +97,7 @@ export function RolesUsersProvider({ getService }: FtrProviderContext) {
|
|||
/**
|
||||
* Deletes specified roles by name
|
||||
* @param roles[]
|
||||
* @deprecated use `.loader.*` methods instead
|
||||
*/
|
||||
async deleteRoles(roles: string[]): Promise<void> {
|
||||
for (const role of roles) {
|
||||
|
|
|
@ -0,0 +1,339 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import TestAgent from 'supertest/lib/agent';
|
||||
import { ensureSpaceIdExists } from '@kbn/security-solution-plugin/scripts/endpoint/common/spaces';
|
||||
import {
|
||||
ENDPOINT_ARTIFACT_LISTS,
|
||||
EXCEPTION_LIST_ITEM_URL,
|
||||
} from '@kbn/securitysolution-list-constants';
|
||||
import expect from '@kbn/expect';
|
||||
import {
|
||||
buildPerPolicyTag,
|
||||
buildSpaceOwnerIdTag,
|
||||
} from '@kbn/security-solution-plugin/common/endpoint/service/artifacts/utils';
|
||||
import { addSpaceIdToPath } from '@kbn/spaces-plugin/common';
|
||||
import { exceptionItemToCreateExceptionItem } from '@kbn/security-solution-plugin/common/endpoint/data_generators/exceptions_list_item_generator';
|
||||
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { Role } from '@kbn/security-plugin-types-common';
|
||||
import { GLOBAL_ARTIFACT_TAG } from '@kbn/security-solution-plugin/common/endpoint/service/artifacts';
|
||||
import { PolicyTestResourceInfo } from '../../../../../security_solution_endpoint/services/endpoint_policy';
|
||||
import { createSupertestErrorLogger } from '../../utils';
|
||||
import { ArtifactTestData } from '../../../../../security_solution_endpoint/services/endpoint_artifacts';
|
||||
import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const utils = getService('securitySolutionUtils');
|
||||
const rolesUsersProvider = getService('rolesUsersProvider');
|
||||
const endpointArtifactTestResources = getService('endpointArtifactTestResources');
|
||||
const policyTestResources = getService('endpointPolicyTestResources');
|
||||
const kbnServer = getService('kibanaServer');
|
||||
const log = getService('log');
|
||||
|
||||
// @skipInServerless: due to the fact that the serverless builtin roles are not yet updated with new privilege
|
||||
// and tests below are currently creating a new role/user
|
||||
describe('@ess @skipInServerless, @skipInServerlessMKI Endpoint Artifacts space awareness support', function () {
|
||||
const spaceOneId = 'space_one';
|
||||
const spaceTwoId = 'space_two';
|
||||
|
||||
let artifactManagerRole: Role;
|
||||
let globalArtifactManagerRole: Role;
|
||||
let supertestArtifactManager: TestAgent;
|
||||
let supertestGlobalArtifactManager: TestAgent;
|
||||
let spaceOnePolicy: PolicyTestResourceInfo;
|
||||
|
||||
before(async () => {
|
||||
// For testing, we're using the `t3_analyst` role which already has All privileges
|
||||
// to all artifacts and manipulating that role definition to create two new roles/users
|
||||
artifactManagerRole = Object.assign(
|
||||
rolesUsersProvider.loader.getPreDefinedRole('t3_analyst'),
|
||||
{ name: 'artifactManager' }
|
||||
);
|
||||
|
||||
if (artifactManagerRole.kibana[0].feature.siemV2.includes('global_artifact_management_all')) {
|
||||
artifactManagerRole.kibana[0].feature.siemV2 =
|
||||
artifactManagerRole.kibana[0].feature.siemV2.filter(
|
||||
(privilege) => privilege !== 'global_artifact_management_all'
|
||||
);
|
||||
}
|
||||
|
||||
globalArtifactManagerRole = Object.assign(
|
||||
rolesUsersProvider.loader.getPreDefinedRole('t3_analyst'),
|
||||
{ name: 'globalArtifactManager' }
|
||||
);
|
||||
|
||||
if (
|
||||
!globalArtifactManagerRole.kibana[0].feature.siemV2.includes(
|
||||
'global_artifact_management_all'
|
||||
)
|
||||
) {
|
||||
globalArtifactManagerRole.kibana[0].feature.siemV2.push('global_artifact_management_all');
|
||||
}
|
||||
|
||||
const [artifactManagerUser, globalArtifactManagerUser] = await Promise.all([
|
||||
rolesUsersProvider.loader.create(artifactManagerRole),
|
||||
rolesUsersProvider.loader.create(globalArtifactManagerRole),
|
||||
]);
|
||||
|
||||
supertestArtifactManager = await utils.createSuperTest(
|
||||
artifactManagerUser.username,
|
||||
artifactManagerUser.password
|
||||
);
|
||||
supertestGlobalArtifactManager = await utils.createSuperTest(
|
||||
globalArtifactManagerUser.username,
|
||||
globalArtifactManagerUser.password
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
ensureSpaceIdExists(kbnServer, spaceOneId, { log }),
|
||||
ensureSpaceIdExists(kbnServer, spaceTwoId, { log }),
|
||||
]);
|
||||
|
||||
spaceOnePolicy = await policyTestResources.createPolicy();
|
||||
});
|
||||
|
||||
// the endpoint uses data streams and es archiver does not support deleting them at the moment so we need
|
||||
// to do it manually
|
||||
after(async () => {
|
||||
if (artifactManagerRole) {
|
||||
await rolesUsersProvider.loader.delete(artifactManagerRole.name);
|
||||
// @ts-expect-error
|
||||
artifactManagerRole = undefined;
|
||||
}
|
||||
|
||||
if (globalArtifactManagerRole) {
|
||||
await rolesUsersProvider.loader.delete(globalArtifactManagerRole.name);
|
||||
// @ts-expect-error
|
||||
globalArtifactManagerRole = undefined;
|
||||
}
|
||||
|
||||
if (spaceOnePolicy) {
|
||||
await spaceOnePolicy.cleanup();
|
||||
// @ts-expect-error
|
||||
spaceOnePolicy = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const artifactLists = Object.keys(ENDPOINT_ARTIFACT_LISTS);
|
||||
|
||||
for (const artifactList of artifactLists) {
|
||||
const listInfo =
|
||||
ENDPOINT_ARTIFACT_LISTS[artifactList as keyof typeof ENDPOINT_ARTIFACT_LISTS];
|
||||
|
||||
describe(`for ${listInfo.name}`, () => {
|
||||
let spaceOnePerPolicyArtifact: ArtifactTestData;
|
||||
let spaceOneGlobalArtifact: ArtifactTestData;
|
||||
|
||||
beforeEach(async () => {
|
||||
spaceOnePerPolicyArtifact = await endpointArtifactTestResources.createArtifact(
|
||||
listInfo.id,
|
||||
{ tags: [buildPerPolicyTag(spaceOnePolicy.packagePolicy.id)] },
|
||||
{ supertest: supertestArtifactManager, spaceId: spaceOneId }
|
||||
);
|
||||
|
||||
spaceOneGlobalArtifact = await endpointArtifactTestResources.createArtifact(
|
||||
listInfo.id,
|
||||
{ tags: [GLOBAL_ARTIFACT_TAG] },
|
||||
{ supertest: supertestGlobalArtifactManager, spaceId: spaceOneId }
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (spaceOnePerPolicyArtifact) {
|
||||
await spaceOnePerPolicyArtifact.cleanup();
|
||||
// @ts-expect-error assigning `undefined`
|
||||
spaceOnePerPolicyArtifact = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
it('should add owner space id when item is created', async () => {
|
||||
expect(spaceOnePerPolicyArtifact.artifact.tags).to.include.string(
|
||||
buildSpaceOwnerIdTag(spaceOneId)
|
||||
);
|
||||
});
|
||||
|
||||
it('should not add owner space id during artifact update if one is already present', async () => {
|
||||
const { body } = await supertestArtifactManager
|
||||
.put(addSpaceIdToPath('/', spaceOneId, EXCEPTION_LIST_ITEM_URL))
|
||||
.set('elastic-api-version', '2023-10-31')
|
||||
.set('x-elastic-internal-origin', 'kibana')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.on('error', createSupertestErrorLogger(log))
|
||||
.send(
|
||||
exceptionItemToCreateExceptionItem({
|
||||
...spaceOnePerPolicyArtifact.artifact,
|
||||
description: 'item was updated',
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
expect((body as ExceptionListItemSchema).tags).to.eql(
|
||||
spaceOnePerPolicyArtifact.artifact.tags
|
||||
);
|
||||
});
|
||||
|
||||
describe('and user does NOT have global artifact management privilege', () => {
|
||||
it('should error when attempting to create artifact with additional owner space id tags', async () => {
|
||||
await supertestArtifactManager
|
||||
.post(addSpaceIdToPath('/', spaceOneId, EXCEPTION_LIST_ITEM_URL))
|
||||
.set('elastic-api-version', '2023-10-31')
|
||||
.set('x-elastic-internal-origin', 'kibana')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.on('error', createSupertestErrorLogger(log).ignoreCodes([403]))
|
||||
.send(
|
||||
Object.assign(
|
||||
exceptionItemToCreateExceptionItem({
|
||||
...spaceOnePerPolicyArtifact.artifact,
|
||||
tags: [buildSpaceOwnerIdTag('foo')],
|
||||
}),
|
||||
{ item_id: undefined }
|
||||
)
|
||||
)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it('should error when attempting to update artifact with different owner space id tags', async () => {
|
||||
await supertestArtifactManager
|
||||
.put(addSpaceIdToPath('/', spaceOneId, EXCEPTION_LIST_ITEM_URL))
|
||||
.set('elastic-api-version', '2023-10-31')
|
||||
.set('x-elastic-internal-origin', 'kibana')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.on('error', createSupertestErrorLogger(log).ignoreCodes([403]))
|
||||
.send(
|
||||
exceptionItemToCreateExceptionItem({
|
||||
...spaceOnePerPolicyArtifact.artifact,
|
||||
tags: [buildSpaceOwnerIdTag('foo')],
|
||||
})
|
||||
)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
// TODO:PT Un-skip in next PR. I got a little ahead of myself and added a test for the change that wil come with the next PR.
|
||||
it.skip('should error if attempting to update a global artifact', async () => {
|
||||
await supertestArtifactManager
|
||||
.put(addSpaceIdToPath('/', spaceOneId, EXCEPTION_LIST_ITEM_URL))
|
||||
.set('elastic-api-version', '2023-10-31')
|
||||
.set('x-elastic-internal-origin', 'kibana')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.on('error', createSupertestErrorLogger(log).ignoreCodes([403]))
|
||||
.send(
|
||||
exceptionItemToCreateExceptionItem({
|
||||
...spaceOneGlobalArtifact.artifact,
|
||||
description: 'updating a global here',
|
||||
})
|
||||
)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
// TODO:PT Un-skip in next PR. I got a little ahead of myself and added a test for the change that wil come with the next PR.
|
||||
it.skip('should error when attempting to change a global artifact to per-policy', async () => {
|
||||
await supertestArtifactManager
|
||||
.put(addSpaceIdToPath('/', spaceOneId, EXCEPTION_LIST_ITEM_URL))
|
||||
.set('elastic-api-version', '2023-10-31')
|
||||
.set('x-elastic-internal-origin', 'kibana')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.on('error', createSupertestErrorLogger(log).ignoreCodes([403]))
|
||||
.send(
|
||||
exceptionItemToCreateExceptionItem({
|
||||
...spaceOneGlobalArtifact.artifact,
|
||||
tags: spaceOneGlobalArtifact.artifact.tags
|
||||
.filter((tag) => tag !== GLOBAL_ARTIFACT_TAG)
|
||||
.concat(buildPerPolicyTag(spaceOnePolicy.packagePolicy.id)),
|
||||
})
|
||||
)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and user has privilege to manage global artifacts', () => {
|
||||
it('should allow creating artifact with additional owner space id tags', async () => {
|
||||
const { body } = await supertestGlobalArtifactManager
|
||||
.post(addSpaceIdToPath('/', spaceOneId, EXCEPTION_LIST_ITEM_URL))
|
||||
.set('elastic-api-version', '2023-10-31')
|
||||
.set('x-elastic-internal-origin', 'kibana')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.on('error', createSupertestErrorLogger(log))
|
||||
.send(
|
||||
Object.assign(
|
||||
exceptionItemToCreateExceptionItem({
|
||||
...spaceOnePerPolicyArtifact.artifact,
|
||||
tags: [buildSpaceOwnerIdTag('foo')],
|
||||
}),
|
||||
{ item_id: undefined }
|
||||
)
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
expect((body as ExceptionListItemSchema).tags).to.eql([
|
||||
buildSpaceOwnerIdTag('foo'),
|
||||
buildSpaceOwnerIdTag(spaceOneId),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should add owner space id when item is updated without having an owner tag', async () => {
|
||||
const { body } = await supertestGlobalArtifactManager
|
||||
.put(addSpaceIdToPath('/', spaceOneId, EXCEPTION_LIST_ITEM_URL))
|
||||
.set('elastic-api-version', '2023-10-31')
|
||||
.set('x-elastic-internal-origin', 'kibana')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.on('error', createSupertestErrorLogger(log))
|
||||
.send(
|
||||
exceptionItemToCreateExceptionItem({
|
||||
...spaceOnePerPolicyArtifact.artifact,
|
||||
tags: [],
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
expect((body as ExceptionListItemSchema).tags).to.eql([
|
||||
buildSpaceOwnerIdTag(spaceOneId),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should allow creation of global artifacts', async () => {
|
||||
// test is already covered by the fact that we created a global artifact for testing
|
||||
expect(spaceOneGlobalArtifact.artifact).to.not.equal(undefined);
|
||||
});
|
||||
|
||||
it('should allow updating of global artifacts', async () => {
|
||||
await supertestGlobalArtifactManager
|
||||
.put(addSpaceIdToPath('/', spaceOneId, EXCEPTION_LIST_ITEM_URL))
|
||||
.set('elastic-api-version', '2023-10-31')
|
||||
.set('x-elastic-internal-origin', 'kibana')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.on('error', createSupertestErrorLogger(log))
|
||||
.send(
|
||||
exceptionItemToCreateExceptionItem({
|
||||
...spaceOneGlobalArtifact.artifact,
|
||||
description: 'updating of global artifacts',
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should allow converting global artifact to per-policy', async () => {
|
||||
await supertestGlobalArtifactManager
|
||||
.put(addSpaceIdToPath('/', spaceOneId, EXCEPTION_LIST_ITEM_URL))
|
||||
.set('elastic-api-version', '2023-10-31')
|
||||
.set('x-elastic-internal-origin', 'kibana')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.on('error', createSupertestErrorLogger(log))
|
||||
.send(
|
||||
exceptionItemToCreateExceptionItem({
|
||||
...spaceOneGlobalArtifact.artifact,
|
||||
tags: spaceOneGlobalArtifact.artifact.tags
|
||||
.filter((tag) => tag !== GLOBAL_ARTIFACT_TAG)
|
||||
.concat(buildPerPolicyTag(spaceOnePolicy.packagePolicy.id)),
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
|
@ -57,5 +57,6 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider
|
|||
});
|
||||
|
||||
loadTestFile(require.resolve('./space_awareness'));
|
||||
loadTestFile(require.resolve('./artifacts'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -15,21 +15,12 @@ import {
|
|||
HOST_METADATA_GET_ROUTE,
|
||||
HOST_METADATA_LIST_ROUTE,
|
||||
} from '@kbn/security-solution-plugin/common/endpoint/constants';
|
||||
import {
|
||||
ENDPOINT_ARTIFACT_LISTS,
|
||||
EXCEPTION_LIST_ITEM_URL,
|
||||
} from '@kbn/securitysolution-list-constants';
|
||||
import { buildSpaceOwnerIdTag } from '@kbn/security-solution-plugin/common/endpoint/service/artifacts/utils';
|
||||
import { exceptionItemToCreateExceptionItem } from '@kbn/security-solution-plugin/common/endpoint/data_generators/exceptions_list_item_generator';
|
||||
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { ArtifactTestData } from '../../../../../security_solution_endpoint/services/endpoint_artifacts';
|
||||
import { createSupertestErrorLogger } from '../../utils';
|
||||
import { FtrProviderContext } from '../../../../ftr_provider_context_edr_workflows';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const utils = getService('securitySolutionUtils');
|
||||
const endpointTestresources = getService('endpointTestResources');
|
||||
const endpointArtifactTestResources = getService('endpointArtifactTestResources');
|
||||
const kbnServer = getService('kibanaServer');
|
||||
const log = getService('log');
|
||||
|
||||
|
@ -195,78 +186,5 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(body.data[dataSpaceB.hosts[0].agent.id].found).to.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`Artifact management (via Lists plugin)`, () => {
|
||||
const artifactLists = Object.keys(ENDPOINT_ARTIFACT_LISTS);
|
||||
|
||||
for (const artifactList of artifactLists) {
|
||||
const listInfo =
|
||||
ENDPOINT_ARTIFACT_LISTS[artifactList as keyof typeof ENDPOINT_ARTIFACT_LISTS];
|
||||
|
||||
describe(`for ${listInfo.name}`, () => {
|
||||
let itemDataSpaceA: ArtifactTestData;
|
||||
|
||||
beforeEach(async () => {
|
||||
itemDataSpaceA = await endpointArtifactTestResources.createArtifact(
|
||||
listInfo.id,
|
||||
{ tags: [] },
|
||||
{ supertest: adminSupertest, spaceId: dataSpaceA.spaceId }
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (itemDataSpaceA) {
|
||||
await itemDataSpaceA.cleanup();
|
||||
// @ts-expect-error assigning `undefined`
|
||||
itemDataSpaceA = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
it('should add owner space id when item is created', async () => {
|
||||
expect(itemDataSpaceA.artifact.tags).to.include.string(
|
||||
buildSpaceOwnerIdTag(dataSpaceA.spaceId)
|
||||
);
|
||||
});
|
||||
|
||||
it('should not add owner space id during artifact update if one is already present', async () => {
|
||||
const { body } = await adminSupertest
|
||||
.put(addSpaceIdToPath('/', dataSpaceA.spaceId, EXCEPTION_LIST_ITEM_URL))
|
||||
.set('elastic-api-version', '2023-10-31')
|
||||
.set('x-elastic-internal-origin', 'kibana')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.on('error', createSupertestErrorLogger(log))
|
||||
.send(
|
||||
exceptionItemToCreateExceptionItem({
|
||||
...itemDataSpaceA.artifact,
|
||||
description: 'item was updated',
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
expect((body as ExceptionListItemSchema).tags).to.eql(itemDataSpaceA.artifact.tags);
|
||||
});
|
||||
|
||||
it('should add owner space id when item is updated, if one is not present', async () => {
|
||||
const { body } = await adminSupertest
|
||||
.put(addSpaceIdToPath('/', dataSpaceA.spaceId, EXCEPTION_LIST_ITEM_URL))
|
||||
.set('elastic-api-version', '2023-10-31')
|
||||
.set('x-elastic-internal-origin', 'kibana')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.on('error', createSupertestErrorLogger(log))
|
||||
.send(
|
||||
exceptionItemToCreateExceptionItem({
|
||||
...itemDataSpaceA.artifact,
|
||||
tags: [],
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
|
||||
expect((body as ExceptionListItemSchema).tags).to.eql([
|
||||
buildSpaceOwnerIdTag(dataSpaceA.spaceId),
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -55,5 +55,6 @@
|
|||
"@kbn/test-suites-src",
|
||||
"@kbn/openapi-common",
|
||||
"@kbn/scout-info",
|
||||
"@kbn/security-plugin-types-common",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue