mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] Put Artifacts by Policy feature behind a feature flag (#95284)
* Added sync_master file for tracking/triggering PRs for merging master into feature branch * removed unnecessary (temporary) markdown file * Trusted apps by policy api (#88025) * Initial version of API for trusted apps per policy. * Fixed compilation errors because of missing new property. * Mapping from tags to policies and back. (No testing) * Fixed compilation error after pulling in main. * Fixed failing tests. * Separated out the prefix in tag for policy reference into constant. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [SECURITY_SOLUTION][ENDPOINT] Ability to create a Trusted App as either Global or Policy Specific (#88707) * Create form supports selecting policies or making Trusted app global * New component `EffectedPolicySelect` - for selecting policies * Enhanced `waitForAction()` test utility to provide a `validate()` option * [SECURITY SOLUTION][ENDPOINT] UI for editing Trusted Application items (#89479) * Add Edit button to TA card UI * Support additional url params (`show`, `id`) * Refactor TrustedAppForm to support Editing of an existing entry * [SECURITY SOLUTION][ENDPOINT] API (`PUT`) for Trusted Apps Edit flow (#90333) * New API route for Update (`PUT`) * Connect UI to Update (PUT) API * Add `version` to TrustedApp type and return it on the API responses * Refactor - moved some public/server shared modules to top-level `common/*` * [SECURITY SOLUTION][ENDPOINT] Trusted Apps API to retrieve a single Trusted App item (#90842) * Get One Trusted App API - route, service, handler * Adjust UI to call GET api to retrieve trusted app for edit * Deleted ununsed trusted app types file * Add UI handling of non-existing TA for edit or when id is missing in url * [Security Solution][Endpoint] Multiple misc. updates/fixes for Edit Trusted Apps (#91656) * correct trusted app schema to ensure `version` is not exposed on TS type for POST * Added updated_by, updated_on properties to TrustedApp * Refactored TA List view to fix bug where card was not updated on a successful edit * Test cases for card interaction from the TA List view * Change title of policy selection to `Assignment` * Selectable Policy CSS adjustments based on UX feedback * Fix failing server tests * [Security Solution][Endpoint] Trusted Apps list API KQL filtering support (#92611) * Fix bad merge from master * Fix trusted apps generator * Add `kuery` to the GET (list) Trusted Apps api * Refactor schema with Put method after merging changes with master * WIP: allow effectScope only when feature flag is enabled * Fixes errors with non declared logger * Uses experimental features module to allow or not effectScope on create/update trusted app schema * Set default value for effectScope when feature flag is disabled * Adds experimentals into redux store. Also creates hook to retrieve a feature flag value from state * Hides effectPolicy when feature flag is not enabled * Fixes unit test mocking hook and adds new test case * Changes file extension for custom hook * Adds new unit test for custom hook * Hides horizontal bar with feature flag * Compress text area depending on feature flag * Fixes failing test because feature flag * Fixes wrong import and unit test * Thwrows error if invalid feature flag check * Adds snapshoot checks with feature flag enabled/disabled * Test snapshots * Changes type name * Add experimentalFeatures in app context * Fixes type checks due AppContext changes * Fixes test due changes on custom hook Co-authored-by: Paul Tavares <paul.tavares@elastic.co> Co-authored-by: Bohdan Tsymbala <bohdan.tsymbala@elastic.co> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
This commit is contained in:
parent
02a8f11ec8
commit
2af094a63d
73 changed files with 9572 additions and 692 deletions
|
@ -12,7 +12,10 @@ import { ListPlugin } from './plugin';
|
|||
|
||||
// exporting these since its required at top level in siem plugin
|
||||
export { ListClient } from './services/lists/list_client';
|
||||
export { CreateExceptionListItemOptions } from './services/exception_lists/exception_list_client_types';
|
||||
export {
|
||||
CreateExceptionListItemOptions,
|
||||
UpdateExceptionListItemOptions,
|
||||
} from './services/exception_lists/exception_list_client_types';
|
||||
export { ExceptionListClient } from './services/exception_lists/exception_list_client';
|
||||
export type { ListPluginSetup, ListsApiRequestHandlerContext } from './types';
|
||||
|
||||
|
|
|
@ -15,8 +15,10 @@ export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*';
|
|||
export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurrency';
|
||||
export const LIMITED_CONCURRENCY_ENDPOINT_COUNT = 100;
|
||||
|
||||
export const TRUSTED_APPS_GET_API = '/api/endpoint/trusted_apps/{id}';
|
||||
export const TRUSTED_APPS_LIST_API = '/api/endpoint/trusted_apps';
|
||||
export const TRUSTED_APPS_CREATE_API = '/api/endpoint/trusted_apps';
|
||||
export const TRUSTED_APPS_UPDATE_API = '/api/endpoint/trusted_apps/{id}';
|
||||
export const TRUSTED_APPS_DELETE_API = '/api/endpoint/trusted_apps/{id}';
|
||||
export const TRUSTED_APPS_SUMMARY_API = '/api/endpoint/trusted_apps/summary';
|
||||
|
||||
|
|
|
@ -5,8 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema } from './trusted_apps';
|
||||
import { ConditionEntryField, OperatingSystem } from '../types';
|
||||
import {
|
||||
GetTrustedAppsRequestSchema,
|
||||
PostTrustedAppCreateRequestSchema,
|
||||
PutTrustedAppUpdateRequestSchema,
|
||||
} from './trusted_apps';
|
||||
import {
|
||||
ConditionEntry,
|
||||
ConditionEntryField,
|
||||
NewTrustedApp,
|
||||
OperatingSystem,
|
||||
PutTrustedAppsRequestParams,
|
||||
} from '../types';
|
||||
|
||||
describe('When invoking Trusted Apps Schema', () => {
|
||||
describe('for GET List', () => {
|
||||
|
@ -72,17 +82,18 @@ describe('When invoking Trusted Apps Schema', () => {
|
|||
});
|
||||
|
||||
describe('for POST Create', () => {
|
||||
const createConditionEntry = <T>(data?: T) => ({
|
||||
const createConditionEntry = <T>(data?: T): ConditionEntry => ({
|
||||
field: ConditionEntryField.PATH,
|
||||
type: 'match',
|
||||
operator: 'included',
|
||||
value: 'c:/programs files/Anti-Virus',
|
||||
...(data || {}),
|
||||
});
|
||||
const createNewTrustedApp = <T>(data?: T) => ({
|
||||
const createNewTrustedApp = <T>(data?: T): NewTrustedApp => ({
|
||||
name: 'Some Anti-Virus App',
|
||||
description: 'this one is ok',
|
||||
os: 'windows',
|
||||
os: OperatingSystem.WINDOWS,
|
||||
effectScope: { type: 'global' },
|
||||
entries: [createConditionEntry()],
|
||||
...(data || {}),
|
||||
});
|
||||
|
@ -329,4 +340,55 @@ describe('When invoking Trusted Apps Schema', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('for PUT Update', () => {
|
||||
const createConditionEntry = <T>(data?: T): ConditionEntry => ({
|
||||
field: ConditionEntryField.PATH,
|
||||
type: 'match',
|
||||
operator: 'included',
|
||||
value: 'c:/programs files/Anti-Virus',
|
||||
...(data || {}),
|
||||
});
|
||||
const createNewTrustedApp = <T>(data?: T): NewTrustedApp => ({
|
||||
name: 'Some Anti-Virus App',
|
||||
description: 'this one is ok',
|
||||
os: OperatingSystem.WINDOWS,
|
||||
effectScope: { type: 'global' },
|
||||
entries: [createConditionEntry()],
|
||||
...(data || {}),
|
||||
});
|
||||
|
||||
const updateParams = <T>(data?: T): PutTrustedAppsRequestParams => ({
|
||||
id: 'validId',
|
||||
...(data || {}),
|
||||
});
|
||||
|
||||
const body = PutTrustedAppUpdateRequestSchema.body;
|
||||
const params = PutTrustedAppUpdateRequestSchema.params;
|
||||
|
||||
it('should not error on a valid message', () => {
|
||||
const bodyMsg = createNewTrustedApp();
|
||||
const paramsMsg = updateParams();
|
||||
expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg);
|
||||
expect(params.validate(paramsMsg)).toStrictEqual(paramsMsg);
|
||||
});
|
||||
|
||||
it('should validate `id` params is required', () => {
|
||||
expect(() => params.validate(updateParams({ id: undefined }))).toThrow();
|
||||
});
|
||||
|
||||
it('should validate `id` params to be string', () => {
|
||||
expect(() => params.validate(updateParams({ id: 1 }))).toThrow();
|
||||
});
|
||||
|
||||
it('should validate `version`', () => {
|
||||
const bodyMsg = createNewTrustedApp({ version: 'v1' });
|
||||
expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg);
|
||||
});
|
||||
|
||||
it('should validate `version` must be string', () => {
|
||||
const bodyMsg = createNewTrustedApp({ version: 1 });
|
||||
expect(() => body.validate(bodyMsg)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { ConditionEntryField, OperatingSystem } from '../types';
|
||||
import { getDuplicateFields, isValidHash } from '../validation/trusted_apps';
|
||||
import { ConditionEntry, ConditionEntryField, OperatingSystem } from '../types';
|
||||
import { getDuplicateFields, isValidHash } from '../service/trusted_apps/validations';
|
||||
|
||||
export const DeleteTrustedAppsRequestSchema = {
|
||||
params: schema.object({
|
||||
|
@ -15,10 +15,17 @@ export const DeleteTrustedAppsRequestSchema = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const GetOneTrustedAppRequestSchema = {
|
||||
params: schema.object({
|
||||
id: schema.string(),
|
||||
}),
|
||||
};
|
||||
|
||||
export const GetTrustedAppsRequestSchema = {
|
||||
query: schema.object({
|
||||
page: schema.maybe(schema.number({ defaultValue: 1, min: 1 })),
|
||||
per_page: schema.maybe(schema.number({ defaultValue: 20, min: 1 })),
|
||||
kuery: schema.maybe(schema.string()),
|
||||
}),
|
||||
};
|
||||
|
||||
|
@ -40,18 +47,18 @@ const CommonEntrySchema = {
|
|||
schema.siblingRef('field'),
|
||||
ConditionEntryField.HASH,
|
||||
schema.string({
|
||||
validate: (hash) =>
|
||||
validate: (hash: string) =>
|
||||
isValidHash(hash) ? undefined : `invalidField.${ConditionEntryField.HASH}`,
|
||||
}),
|
||||
schema.conditional(
|
||||
schema.siblingRef('field'),
|
||||
ConditionEntryField.PATH,
|
||||
schema.string({
|
||||
validate: (field) =>
|
||||
validate: (field: string) =>
|
||||
field.length > 0 ? undefined : `invalidField.${ConditionEntryField.PATH}`,
|
||||
}),
|
||||
schema.string({
|
||||
validate: (field) =>
|
||||
validate: (field: string) =>
|
||||
field.length > 0 ? undefined : `invalidField.${ConditionEntryField.SIGNER}`,
|
||||
})
|
||||
)
|
||||
|
@ -99,7 +106,7 @@ const EntrySchemaDependingOnOS = schema.conditional(
|
|||
*/
|
||||
const EntriesSchema = schema.arrayOf(EntrySchemaDependingOnOS, {
|
||||
minSize: 1,
|
||||
validate(entries) {
|
||||
validate(entries: ConditionEntry[]) {
|
||||
return (
|
||||
getDuplicateFields(entries)
|
||||
.map((field) => `duplicatedEntry.${field}`)
|
||||
|
@ -108,8 +115,8 @@ const EntriesSchema = schema.arrayOf(EntrySchemaDependingOnOS, {
|
|||
},
|
||||
});
|
||||
|
||||
export const PostTrustedAppCreateRequestSchema = {
|
||||
body: schema.object({
|
||||
const getTrustedAppForOsScheme = (forUpdateFlow: boolean = false) =>
|
||||
schema.object({
|
||||
name: schema.string({ minLength: 1, maxLength: 256 }),
|
||||
description: schema.maybe(schema.string({ minLength: 0, maxLength: 256, defaultValue: '' })),
|
||||
os: schema.oneOf([
|
||||
|
@ -117,6 +124,26 @@ export const PostTrustedAppCreateRequestSchema = {
|
|||
schema.literal(OperatingSystem.LINUX),
|
||||
schema.literal(OperatingSystem.MAC),
|
||||
]),
|
||||
effectScope: schema.oneOf([
|
||||
schema.object({
|
||||
type: schema.literal('global'),
|
||||
}),
|
||||
schema.object({
|
||||
type: schema.literal('policy'),
|
||||
policies: schema.arrayOf(schema.string({ minLength: 1 })),
|
||||
}),
|
||||
]),
|
||||
entries: EntriesSchema,
|
||||
}),
|
||||
...(forUpdateFlow ? { version: schema.maybe(schema.string()) } : {}),
|
||||
});
|
||||
|
||||
export const PostTrustedAppCreateRequestSchema = {
|
||||
body: getTrustedAppForOsScheme(),
|
||||
};
|
||||
|
||||
export const PutTrustedAppUpdateRequestSchema = {
|
||||
params: schema.object({
|
||||
id: schema.string(),
|
||||
}),
|
||||
body: getTrustedAppForOsScheme(true),
|
||||
};
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { MaybeImmutable, NewTrustedApp, UpdateTrustedApp } from '../../types';
|
||||
|
||||
const NEW_TRUSTED_APP_KEYS: Array<keyof UpdateTrustedApp> = [
|
||||
'name',
|
||||
'effectScope',
|
||||
'entries',
|
||||
'description',
|
||||
'os',
|
||||
'version',
|
||||
];
|
||||
|
||||
export const toUpdateTrustedApp = <T extends NewTrustedApp>(
|
||||
trustedApp: MaybeImmutable<T>
|
||||
): UpdateTrustedApp => {
|
||||
const trustedAppForUpdate: UpdateTrustedApp = {} as UpdateTrustedApp;
|
||||
|
||||
for (const key of NEW_TRUSTED_APP_KEYS) {
|
||||
// This should be safe. Its needed due to the inter-dependency on property values (`os` <=> `entries`)
|
||||
// @ts-expect-error
|
||||
trustedAppForUpdate[key] = trustedApp[key];
|
||||
}
|
||||
return trustedAppForUpdate;
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConditionEntry, ConditionEntryField } from '../types';
|
||||
import { ConditionEntry, ConditionEntryField } from '../../types';
|
||||
|
||||
const HASH_LENGTHS: readonly number[] = [
|
||||
32, // MD5
|
|
@ -62,6 +62,11 @@ type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
|
|||
type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
|
||||
type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> };
|
||||
|
||||
/**
|
||||
* Utility type that will return back a union of the given [T]ype and an Immutable version of it
|
||||
*/
|
||||
export type MaybeImmutable<T> = T | Immutable<T>;
|
||||
|
||||
/**
|
||||
* Stats for related events for a particular node in a resolver graph.
|
||||
*/
|
||||
|
|
|
@ -9,14 +9,22 @@ import { TypeOf } from '@kbn/config-schema';
|
|||
import { ApplicationStart } from 'kibana/public';
|
||||
import {
|
||||
DeleteTrustedAppsRequestSchema,
|
||||
GetOneTrustedAppRequestSchema,
|
||||
GetTrustedAppsRequestSchema,
|
||||
PostTrustedAppCreateRequestSchema,
|
||||
PutTrustedAppUpdateRequestSchema,
|
||||
} from '../schema/trusted_apps';
|
||||
import { OperatingSystem } from './os';
|
||||
|
||||
/** API request params for deleting Trusted App entry */
|
||||
export type DeleteTrustedAppsRequestParams = TypeOf<typeof DeleteTrustedAppsRequestSchema.params>;
|
||||
|
||||
export type GetOneTrustedAppRequestParams = TypeOf<typeof GetOneTrustedAppRequestSchema.params>;
|
||||
|
||||
export interface GetOneTrustedAppResponse {
|
||||
data: TrustedApp;
|
||||
}
|
||||
|
||||
/** API request params for retrieving a list of Trusted Apps */
|
||||
export type GetTrustedAppsListRequest = TypeOf<typeof GetTrustedAppsRequestSchema.query>;
|
||||
|
||||
|
@ -39,6 +47,15 @@ export interface PostTrustedAppCreateResponse {
|
|||
data: TrustedApp;
|
||||
}
|
||||
|
||||
/** API request params for updating a Trusted App */
|
||||
export type PutTrustedAppsRequestParams = TypeOf<typeof PutTrustedAppUpdateRequestSchema.params>;
|
||||
|
||||
/** API Request body for Updating a new Trusted App entry */
|
||||
export type PutTrustedAppUpdateRequest = TypeOf<typeof PutTrustedAppUpdateRequestSchema.body> &
|
||||
(MacosLinuxConditionEntries | WindowsConditionEntries);
|
||||
|
||||
export type PutTrustedAppUpdateResponse = PostTrustedAppCreateResponse;
|
||||
|
||||
export interface GetTrustedAppsSummaryResponse {
|
||||
total: number;
|
||||
windows: number;
|
||||
|
@ -76,17 +93,38 @@ export interface WindowsConditionEntries {
|
|||
entries: WindowsConditionEntry[];
|
||||
}
|
||||
|
||||
export interface GlobalEffectScope {
|
||||
type: 'global';
|
||||
}
|
||||
|
||||
export interface PolicyEffectScope {
|
||||
type: 'policy';
|
||||
/** An array of Endpoint Integration Policy UUIDs */
|
||||
policies: string[];
|
||||
}
|
||||
|
||||
export type EffectScope = GlobalEffectScope | PolicyEffectScope;
|
||||
|
||||
/** Type for a new Trusted App Entry */
|
||||
export type NewTrustedApp = {
|
||||
name: string;
|
||||
description?: string;
|
||||
effectScope: EffectScope;
|
||||
} & (MacosLinuxConditionEntries | WindowsConditionEntries);
|
||||
|
||||
/** An Update to a Trusted App Entry */
|
||||
export type UpdateTrustedApp = NewTrustedApp & {
|
||||
version?: string;
|
||||
};
|
||||
|
||||
/** A trusted app entry */
|
||||
export type TrustedApp = NewTrustedApp & {
|
||||
version: string;
|
||||
id: string;
|
||||
created_at: string;
|
||||
created_by: string;
|
||||
updated_at: string;
|
||||
updated_by: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,6 +13,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues;
|
|||
*/
|
||||
const allowedExperimentalValues = Object.freeze({
|
||||
fleetServerEnabled: false,
|
||||
trustedAppsByPolicyEnabled: false,
|
||||
});
|
||||
|
||||
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
|
|
|
@ -5,7 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, isValidElement, memo, ReactElement, ReactNode, useMemo } from 'react';
|
||||
import React, {
|
||||
FC,
|
||||
isValidElement,
|
||||
memo,
|
||||
PropsWithChildren,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
EuiPanel,
|
||||
|
@ -92,41 +100,46 @@ export const ItemDetailsAction: FC<PropsForButton<EuiButtonProps>> = memo(
|
|||
|
||||
ItemDetailsAction.displayName = 'ItemDetailsAction';
|
||||
|
||||
export const ItemDetailsCard: FC = memo(({ children }) => {
|
||||
const childElements = useMemo(
|
||||
() => groupChildrenByType(children, [ItemDetailsPropertySummary, ItemDetailsAction]),
|
||||
[children]
|
||||
);
|
||||
export type ItemDetailsCardProps = PropsWithChildren<{
|
||||
'data-test-subj'?: string;
|
||||
}>;
|
||||
export const ItemDetailsCard = memo<ItemDetailsCardProps>(
|
||||
({ children, 'data-test-subj': dataTestSubj }) => {
|
||||
const childElements = useMemo(
|
||||
() => groupChildrenByType(children, [ItemDetailsPropertySummary, ItemDetailsAction]),
|
||||
[children]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel paddingSize="none">
|
||||
<EuiFlexGroup direction="row">
|
||||
<SummarySection grow={2}>
|
||||
<EuiDescriptionList compressed type="column">
|
||||
{childElements.get(ItemDetailsPropertySummary)}
|
||||
</EuiDescriptionList>
|
||||
</SummarySection>
|
||||
<DetailsSection grow={5}>
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem grow={1}>
|
||||
<div>{childElements.get(OTHER_NODES)}</div>
|
||||
</EuiFlexItem>
|
||||
{childElements.has(ItemDetailsAction) && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd">
|
||||
{childElements.get(ItemDetailsAction)?.map((action, index) => (
|
||||
<EuiFlexItem grow={false} key={index}>
|
||||
{action}
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
return (
|
||||
<EuiPanel paddingSize="none" data-test-subj={dataTestSubj}>
|
||||
<EuiFlexGroup direction="row">
|
||||
<SummarySection grow={2}>
|
||||
<EuiDescriptionList compressed type="column">
|
||||
{childElements.get(ItemDetailsPropertySummary)}
|
||||
</EuiDescriptionList>
|
||||
</SummarySection>
|
||||
<DetailsSection grow={5}>
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem grow={1}>
|
||||
<div>{childElements.get(OTHER_NODES)}</div>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</DetailsSection>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
});
|
||||
{childElements.has(ItemDetailsAction) && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd">
|
||||
{childElements.get(ItemDetailsAction)?.map((action, index) => (
|
||||
<EuiFlexItem grow={false} key={index}>
|
||||
{action}
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</DetailsSection>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ItemDetailsCard.displayName = 'ItemDetailsCard';
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { useSelector } from 'react-redux';
|
||||
import { ExperimentalFeatures } from '../../../common/experimental_features';
|
||||
import { useIsExperimentalFeatureEnabled } from './use_experimental_features';
|
||||
|
||||
jest.mock('react-redux');
|
||||
const useSelectorMock = useSelector as jest.Mock;
|
||||
const mockAppState = {
|
||||
app: {
|
||||
enableExperimental: {
|
||||
featureA: true,
|
||||
featureB: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('useExperimentalFeatures', () => {
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockImplementation((cb) => {
|
||||
return cb(mockAppState);
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
});
|
||||
it('throws an error when unexisting feature', async () => {
|
||||
expect(() =>
|
||||
useIsExperimentalFeatureEnabled('unexistingFeature' as keyof ExperimentalFeatures)
|
||||
).toThrowError();
|
||||
});
|
||||
it('returns true when existing feature and is enabled', async () => {
|
||||
const result = useIsExperimentalFeatureEnabled('featureA' as keyof ExperimentalFeatures);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
it('returns false when existing feature and is disabled', async () => {
|
||||
const result = useIsExperimentalFeatureEnabled('featureB' as keyof ExperimentalFeatures);
|
||||
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { useSelector } from 'react-redux';
|
||||
import { State } from '../../common/store';
|
||||
import {
|
||||
ExperimentalFeatures,
|
||||
getExperimentalAllowedValues,
|
||||
} from '../../../common/experimental_features';
|
||||
|
||||
const allowedExperimentalValues = getExperimentalAllowedValues();
|
||||
|
||||
export const useIsExperimentalFeatureEnabled = (feature: keyof ExperimentalFeatures): boolean => {
|
||||
return useSelector(({ app: { enableExperimental } }: State) => {
|
||||
if (!enableExperimental || !(feature in enableExperimental)) {
|
||||
throw new Error(
|
||||
`Invalid enable value ${feature}. Allowed values are: ${allowedExperimentalValues.join(
|
||||
', '
|
||||
)}`
|
||||
);
|
||||
}
|
||||
return enableExperimental[feature];
|
||||
});
|
||||
};
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ExperimentalFeatures } from '../../../../common/experimental_features';
|
||||
import { Note } from '../../lib/note';
|
||||
|
||||
export type ErrorState = ErrorModel;
|
||||
|
@ -24,4 +25,5 @@ export type ErrorModel = Error[];
|
|||
export interface AppModel {
|
||||
notesById: NotesById;
|
||||
errors: ErrorState;
|
||||
enableExperimental?: ExperimentalFeatures;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { parseExperimentalConfigValue } from '../../..//common/experimental_features';
|
||||
import { createInitialState } from './reducer';
|
||||
|
||||
jest.mock('../lib/kibana', () => ({
|
||||
|
@ -22,6 +23,7 @@ describe('createInitialState', () => {
|
|||
kibanaIndexPatterns: [{ id: '1234567890987654321', title: 'mock-kibana' }],
|
||||
configIndexPatterns: ['auditbeat-*', 'filebeat'],
|
||||
signalIndexName: 'siem-signals-default',
|
||||
enableExperimental: parseExperimentalConfigValue([]),
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -35,6 +37,7 @@ describe('createInitialState', () => {
|
|||
kibanaIndexPatterns: [{ id: '1234567890987654321', title: 'mock-kibana' }],
|
||||
configIndexPatterns: [],
|
||||
signalIndexName: 'siem-signals-default',
|
||||
enableExperimental: parseExperimentalConfigValue([]),
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import { ManagementPluginReducer } from '../../management';
|
|||
import { State } from './types';
|
||||
import { AppAction } from './actions';
|
||||
import { KibanaIndexPatterns } from './sourcerer/model';
|
||||
import { ExperimentalFeatures } from '../../../common/experimental_features';
|
||||
|
||||
export type SubPluginsInitReducer = HostsPluginReducer &
|
||||
NetworkPluginReducer &
|
||||
|
@ -36,14 +37,16 @@ export const createInitialState = (
|
|||
kibanaIndexPatterns,
|
||||
configIndexPatterns,
|
||||
signalIndexName,
|
||||
enableExperimental,
|
||||
}: {
|
||||
kibanaIndexPatterns: KibanaIndexPatterns;
|
||||
configIndexPatterns: string[];
|
||||
signalIndexName: string | null;
|
||||
enableExperimental: ExperimentalFeatures;
|
||||
}
|
||||
): PreloadedState<State> => {
|
||||
const preloadedState: PreloadedState<State> = {
|
||||
app: initialAppState,
|
||||
app: { ...initialAppState, enableExperimental },
|
||||
dragAndDrop: initialDragAndDropState,
|
||||
...pluginsInitState,
|
||||
inputs: createInitialInputsState(),
|
||||
|
|
|
@ -9,6 +9,10 @@ import { Dispatch } from 'redux';
|
|||
import { State, ImmutableMiddlewareFactory } from './types';
|
||||
import { AppAction } from './actions';
|
||||
|
||||
interface WaitForActionOptions<T extends A['type'], A extends AppAction = AppAction> {
|
||||
validate?: (action: A extends { type: T } ? A : never) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilities for testing Redux middleware
|
||||
*/
|
||||
|
@ -21,7 +25,10 @@ export interface MiddlewareActionSpyHelper<S = State, A extends AppAction = AppA
|
|||
*
|
||||
* @param actionType
|
||||
*/
|
||||
waitForAction: <T extends A['type']>(actionType: T) => Promise<A extends { type: T } ? A : never>;
|
||||
waitForAction: <T extends A['type']>(
|
||||
actionType: T,
|
||||
options?: WaitForActionOptions<T, A>
|
||||
) => Promise<A extends { type: T } ? A : never>;
|
||||
/**
|
||||
* A property holding the information around the calls that were processed by the internal
|
||||
* `actionSpyMiddelware`. This property holds the information typically found in Jets's mocked
|
||||
|
@ -78,7 +85,7 @@ export const createSpyMiddleware = <
|
|||
let spyDispatch: jest.Mock<Dispatch<A>>;
|
||||
|
||||
return {
|
||||
waitForAction: async (actionType) => {
|
||||
waitForAction: async (actionType, options = {}) => {
|
||||
type ResolvedAction = A extends { type: typeof actionType } ? A : never;
|
||||
|
||||
// Error is defined here so that we get a better stack trace that points to the test from where it was used
|
||||
|
@ -87,6 +94,10 @@ export const createSpyMiddleware = <
|
|||
return new Promise<ResolvedAction>((resolve, reject) => {
|
||||
const watch: ActionWatcher = (action) => {
|
||||
if (action.type === actionType) {
|
||||
if (options.validate && !options.validate(action as ResolvedAction)) {
|
||||
return;
|
||||
}
|
||||
|
||||
watchers.delete(watch);
|
||||
clearTimeout(timeout);
|
||||
resolve(action as ResolvedAction);
|
||||
|
|
|
@ -10,3 +10,7 @@ export interface ServerApiError {
|
|||
error: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface SecuritySolutionUiConfigType {
|
||||
enableExperimental: string[];
|
||||
}
|
||||
|
|
|
@ -108,6 +108,7 @@ const normalizeTrustedAppsPageLocation = (
|
|||
: {}),
|
||||
...(!isDefaultOrMissing(location.view_type, 'grid') ? { view_type: location.view_type } : {}),
|
||||
...(!isDefaultOrMissing(location.show, undefined) ? { show: location.show } : {}),
|
||||
...(!isDefaultOrMissing(location.id, undefined) ? { id: location.id } : {}),
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
|
@ -147,11 +148,20 @@ export const extractListPaginationParams = (query: querystring.ParsedUrlQuery) =
|
|||
|
||||
export const extractTrustedAppsListPageLocation = (
|
||||
query: querystring.ParsedUrlQuery
|
||||
): TrustedAppsListPageLocation => ({
|
||||
...extractListPaginationParams(query),
|
||||
view_type: extractFirstParamValue(query, 'view_type') === 'list' ? 'list' : 'grid',
|
||||
show: extractFirstParamValue(query, 'show') === 'create' ? 'create' : undefined,
|
||||
});
|
||||
): TrustedAppsListPageLocation => {
|
||||
const showParamValue = extractFirstParamValue(
|
||||
query,
|
||||
'show'
|
||||
) as TrustedAppsListPageLocation['show'];
|
||||
|
||||
return {
|
||||
...extractListPaginationParams(query),
|
||||
view_type: extractFirstParamValue(query, 'view_type') === 'list' ? 'list' : 'grid',
|
||||
show:
|
||||
showParamValue && ['edit', 'create'].includes(showParamValue) ? showParamValue : undefined,
|
||||
id: extractFirstParamValue(query, 'id'),
|
||||
};
|
||||
};
|
||||
|
||||
export const getTrustedAppsListPath = (location?: Partial<TrustedAppsListPageLocation>): string => {
|
||||
const path = generatePath(MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, {
|
||||
|
|
|
@ -10,7 +10,9 @@ import { HttpStart } from 'kibana/public';
|
|||
import {
|
||||
TRUSTED_APPS_CREATE_API,
|
||||
TRUSTED_APPS_DELETE_API,
|
||||
TRUSTED_APPS_GET_API,
|
||||
TRUSTED_APPS_LIST_API,
|
||||
TRUSTED_APPS_UPDATE_API,
|
||||
TRUSTED_APPS_SUMMARY_API,
|
||||
} from '../../../../../common/endpoint/constants';
|
||||
|
||||
|
@ -21,19 +23,39 @@ import {
|
|||
PostTrustedAppCreateRequest,
|
||||
PostTrustedAppCreateResponse,
|
||||
GetTrustedAppsSummaryResponse,
|
||||
PutTrustedAppUpdateRequest,
|
||||
PutTrustedAppUpdateResponse,
|
||||
PutTrustedAppsRequestParams,
|
||||
GetOneTrustedAppRequestParams,
|
||||
GetOneTrustedAppResponse,
|
||||
} from '../../../../../common/endpoint/types/trusted_apps';
|
||||
|
||||
import { resolvePathVariables } from './utils';
|
||||
import { sendGetEndpointSpecificPackagePolicies } from '../../policy/store/services/ingest';
|
||||
|
||||
export interface TrustedAppsService {
|
||||
getTrustedApp(params: GetOneTrustedAppRequestParams): Promise<GetOneTrustedAppResponse>;
|
||||
getTrustedAppsList(request: GetTrustedAppsListRequest): Promise<GetTrustedListAppsResponse>;
|
||||
deleteTrustedApp(request: DeleteTrustedAppsRequestParams): Promise<void>;
|
||||
createTrustedApp(request: PostTrustedAppCreateRequest): Promise<PostTrustedAppCreateResponse>;
|
||||
updateTrustedApp(
|
||||
params: PutTrustedAppsRequestParams,
|
||||
request: PutTrustedAppUpdateRequest
|
||||
): Promise<PutTrustedAppUpdateResponse>;
|
||||
getPolicyList(
|
||||
options?: Parameters<typeof sendGetEndpointSpecificPackagePolicies>[1]
|
||||
): ReturnType<typeof sendGetEndpointSpecificPackagePolicies>;
|
||||
}
|
||||
|
||||
export class TrustedAppsHttpService implements TrustedAppsService {
|
||||
constructor(private http: HttpStart) {}
|
||||
|
||||
async getTrustedApp(params: GetOneTrustedAppRequestParams) {
|
||||
return this.http.get<GetOneTrustedAppResponse>(
|
||||
resolvePathVariables(TRUSTED_APPS_GET_API, params)
|
||||
);
|
||||
}
|
||||
|
||||
async getTrustedAppsList(request: GetTrustedAppsListRequest) {
|
||||
return this.http.get<GetTrustedListAppsResponse>(TRUSTED_APPS_LIST_API, {
|
||||
query: request,
|
||||
|
@ -50,7 +72,21 @@ export class TrustedAppsHttpService implements TrustedAppsService {
|
|||
});
|
||||
}
|
||||
|
||||
async updateTrustedApp(
|
||||
params: PutTrustedAppsRequestParams,
|
||||
updatedTrustedApp: PutTrustedAppUpdateRequest
|
||||
) {
|
||||
return this.http.put<PutTrustedAppUpdateResponse>(
|
||||
resolvePathVariables(TRUSTED_APPS_UPDATE_API, params),
|
||||
{ body: JSON.stringify(updatedTrustedApp) }
|
||||
);
|
||||
}
|
||||
|
||||
async getTrustedAppsSummary() {
|
||||
return this.http.get<GetTrustedAppsSummaryResponse>(TRUSTED_APPS_SUMMARY_API);
|
||||
}
|
||||
|
||||
getPolicyList(options?: Parameters<typeof sendGetEndpointSpecificPackagePolicies>[1]) {
|
||||
return sendGetEndpointSpecificPackagePolicies(this.http, options);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types/trusted_apps';
|
||||
import { AsyncResourceState } from '.';
|
||||
import { GetPolicyListResponse } from '../../policy/types';
|
||||
|
||||
export interface Pagination {
|
||||
pageIndex: number;
|
||||
|
@ -29,7 +30,9 @@ export interface TrustedAppsListPageLocation {
|
|||
page_index: number;
|
||||
page_size: number;
|
||||
view_type: ViewType;
|
||||
show?: 'create';
|
||||
show?: 'create' | 'edit';
|
||||
/** Used for editing. The ID of the selected trusted app */
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface TrustedAppsListPageState {
|
||||
|
@ -51,9 +54,13 @@ export interface TrustedAppsListPageState {
|
|||
entry: NewTrustedApp;
|
||||
isValid: boolean;
|
||||
};
|
||||
/** The trusted app to be edited (when in edit mode) */
|
||||
editItem?: AsyncResourceState<TrustedApp>;
|
||||
confirmed: boolean;
|
||||
submissionResourceState: AsyncResourceState<TrustedApp>;
|
||||
};
|
||||
/** A list of all available polices for use in associating TA to policies */
|
||||
policies: AsyncResourceState<GetPolicyListResponse>;
|
||||
location: TrustedAppsListPageLocation;
|
||||
active: boolean;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,11 @@
|
|||
import {
|
||||
ConditionEntry,
|
||||
ConditionEntryField,
|
||||
EffectScope,
|
||||
GlobalEffectScope,
|
||||
MacosLinuxConditionEntry,
|
||||
MaybeImmutable,
|
||||
PolicyEffectScope,
|
||||
WindowsConditionEntry,
|
||||
} from '../../../../../common/endpoint/types';
|
||||
|
||||
|
@ -23,3 +27,15 @@ export const isMacosLinuxTrustedAppCondition = (
|
|||
): condition is MacosLinuxConditionEntry => {
|
||||
return condition.field !== ConditionEntryField.SIGNER;
|
||||
};
|
||||
|
||||
export const isGlobalEffectScope = (
|
||||
effectedScope: MaybeImmutable<EffectScope>
|
||||
): effectedScope is GlobalEffectScope => {
|
||||
return effectedScope.type === 'global';
|
||||
};
|
||||
|
||||
export const isPolicyEffectScope = (
|
||||
effectedScope: MaybeImmutable<EffectScope>
|
||||
): effectedScope is PolicyEffectScope => {
|
||||
return effectedScope.type === 'policy';
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Action } from 'redux';
|
|||
|
||||
import { NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types';
|
||||
import { AsyncResourceState, TrustedAppsListData } from '../state';
|
||||
import { GetPolicyListResponse } from '../../policy/types';
|
||||
|
||||
export type TrustedAppsListDataOutdated = Action<'trustedAppsListDataOutdated'>;
|
||||
|
||||
|
@ -51,6 +52,10 @@ export type TrustedAppCreationDialogFormStateUpdated = Action<'trustedAppCreatio
|
|||
};
|
||||
};
|
||||
|
||||
export type TrustedAppCreationEditItemStateChanged = Action<'trustedAppCreationEditItemStateChanged'> & {
|
||||
payload: AsyncResourceState<TrustedApp>;
|
||||
};
|
||||
|
||||
export type TrustedAppCreationDialogConfirmed = Action<'trustedAppCreationDialogConfirmed'>;
|
||||
|
||||
export type TrustedAppCreationDialogClosed = Action<'trustedAppCreationDialogClosed'>;
|
||||
|
@ -59,6 +64,10 @@ export type TrustedAppsExistResponse = Action<'trustedAppsExistStateChanged'> &
|
|||
payload: AsyncResourceState<boolean>;
|
||||
};
|
||||
|
||||
export type TrustedAppsPoliciesStateChanged = Action<'trustedAppsPoliciesStateChanged'> & {
|
||||
payload: AsyncResourceState<GetPolicyListResponse>;
|
||||
};
|
||||
|
||||
export type TrustedAppsPageAction =
|
||||
| TrustedAppsListDataOutdated
|
||||
| TrustedAppsListResourceStateChanged
|
||||
|
@ -67,8 +76,10 @@ export type TrustedAppsPageAction =
|
|||
| TrustedAppDeletionDialogConfirmed
|
||||
| TrustedAppDeletionDialogClosed
|
||||
| TrustedAppCreationSubmissionResourceStateChanged
|
||||
| TrustedAppCreationEditItemStateChanged
|
||||
| TrustedAppCreationDialogStarted
|
||||
| TrustedAppCreationDialogFormStateUpdated
|
||||
| TrustedAppCreationDialogConfirmed
|
||||
| TrustedAppsExistResponse
|
||||
| TrustedAppsPoliciesStateChanged
|
||||
| TrustedAppCreationDialogClosed;
|
||||
|
|
|
@ -28,6 +28,7 @@ export const defaultNewTrustedApp = (): NewTrustedApp => ({
|
|||
os: OperatingSystem.WINDOWS,
|
||||
entries: [defaultConditionEntry()],
|
||||
description: '',
|
||||
effectScope: { type: 'global' },
|
||||
});
|
||||
|
||||
export const initialDeletionDialogState = (): TrustedAppsListPageState['deletionDialog'] => ({
|
||||
|
@ -48,10 +49,12 @@ export const initialTrustedAppsPageState = (): TrustedAppsListPageState => ({
|
|||
},
|
||||
deletionDialog: initialDeletionDialogState(),
|
||||
creationDialog: initialCreationDialogState(),
|
||||
policies: { type: 'UninitialisedResourceState' },
|
||||
location: {
|
||||
page_index: MANAGEMENT_DEFAULT_PAGE,
|
||||
page_size: MANAGEMENT_DEFAULT_PAGE_SIZE,
|
||||
show: undefined,
|
||||
id: undefined,
|
||||
view_type: 'grid',
|
||||
},
|
||||
active: false,
|
||||
|
|
|
@ -21,10 +21,11 @@ import {
|
|||
} from '../test_utils';
|
||||
|
||||
import { TrustedAppsService } from '../service';
|
||||
import { Pagination, TrustedAppsListPageState } from '../state';
|
||||
import { Pagination, TrustedAppsListPageLocation, TrustedAppsListPageState } from '../state';
|
||||
import { initialTrustedAppsPageState } from './builders';
|
||||
import { trustedAppsPageReducer } from './reducer';
|
||||
import { createTrustedAppsPageMiddleware } from './middleware';
|
||||
import { Immutable } from '../../../../../common/endpoint/types';
|
||||
|
||||
const initialNow = 111111;
|
||||
const dateNowMock = jest.fn();
|
||||
|
@ -32,7 +33,7 @@ dateNowMock.mockReturnValue(initialNow);
|
|||
|
||||
Date.now = dateNowMock;
|
||||
|
||||
const initialState = initialTrustedAppsPageState();
|
||||
const initialState: Immutable<TrustedAppsListPageState> = initialTrustedAppsPageState();
|
||||
|
||||
const createGetTrustedListAppsResponse = (pagination: Partial<Pagination>) => {
|
||||
const fullPagination = { ...createDefaultPagination(), ...pagination };
|
||||
|
@ -49,6 +50,9 @@ const createTrustedAppsServiceMock = (): jest.Mocked<TrustedAppsService> => ({
|
|||
getTrustedAppsList: jest.fn(),
|
||||
deleteTrustedApp: jest.fn(),
|
||||
createTrustedApp: jest.fn(),
|
||||
getPolicyList: jest.fn(),
|
||||
updateTrustedApp: jest.fn(),
|
||||
getTrustedApp: jest.fn(),
|
||||
});
|
||||
|
||||
const createStoreSetup = (trustedAppsService: TrustedAppsService) => {
|
||||
|
@ -87,6 +91,15 @@ describe('middleware', () => {
|
|||
};
|
||||
};
|
||||
|
||||
const createLocationState = (
|
||||
params?: Partial<TrustedAppsListPageLocation>
|
||||
): TrustedAppsListPageLocation => {
|
||||
return {
|
||||
...initialState.location,
|
||||
...(params ?? {}),
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
dateNowMock.mockReturnValue(initialNow);
|
||||
});
|
||||
|
@ -102,7 +115,10 @@ describe('middleware', () => {
|
|||
describe('refreshing list resource state', () => {
|
||||
it('refreshes the list when location changes and data gets outdated', async () => {
|
||||
const pagination = { pageIndex: 2, pageSize: 50 };
|
||||
const location = { page_index: 2, page_size: 50, show: undefined, view_type: 'grid' };
|
||||
const location = createLocationState({
|
||||
page_index: 2,
|
||||
page_size: 50,
|
||||
});
|
||||
const service = createTrustedAppsServiceMock();
|
||||
const { store, spyMiddleware } = createStoreSetup(service);
|
||||
|
||||
|
@ -136,7 +152,10 @@ describe('middleware', () => {
|
|||
|
||||
it('does not refresh the list when location changes and data does not get outdated', async () => {
|
||||
const pagination = { pageIndex: 2, pageSize: 50 };
|
||||
const location = { page_index: 2, page_size: 50, show: undefined, view_type: 'grid' };
|
||||
const location = createLocationState({
|
||||
page_index: 2,
|
||||
page_size: 50,
|
||||
});
|
||||
const service = createTrustedAppsServiceMock();
|
||||
const { store, spyMiddleware } = createStoreSetup(service);
|
||||
|
||||
|
@ -161,7 +180,7 @@ describe('middleware', () => {
|
|||
it('refreshes the list when data gets outdated with and outdate action', async () => {
|
||||
const newNow = 222222;
|
||||
const pagination = { pageIndex: 0, pageSize: 10 };
|
||||
const location = { page_index: 0, page_size: 10, show: undefined, view_type: 'grid' };
|
||||
const location = createLocationState();
|
||||
const service = createTrustedAppsServiceMock();
|
||||
const { store, spyMiddleware } = createStoreSetup(service);
|
||||
|
||||
|
@ -224,7 +243,10 @@ describe('middleware', () => {
|
|||
freshDataTimestamp: initialNow,
|
||||
},
|
||||
active: true,
|
||||
location: { page_index: 2, page_size: 50, show: undefined, view_type: 'grid' },
|
||||
location: createLocationState({
|
||||
page_index: 2,
|
||||
page_size: 50,
|
||||
}),
|
||||
});
|
||||
|
||||
const infiniteLoopTest = async () => {
|
||||
|
@ -240,7 +262,7 @@ describe('middleware', () => {
|
|||
const entry = createSampleTrustedApp(3);
|
||||
const notFoundError = createServerApiError('Not Found');
|
||||
const pagination = { pageIndex: 0, pageSize: 10 };
|
||||
const location = { page_index: 0, page_size: 10, show: undefined, view_type: 'grid' };
|
||||
const location = createLocationState();
|
||||
const getTrustedAppsListResponse = createGetTrustedListAppsResponse(pagination);
|
||||
const listView = createLoadedListViewWithPagination(initialNow, pagination);
|
||||
const listViewNew = createLoadedListViewWithPagination(newNow, pagination);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
Immutable,
|
||||
PostTrustedAppCreateRequest,
|
||||
|
@ -54,7 +55,15 @@ import {
|
|||
getListTotalItemsCount,
|
||||
trustedAppsListPageActive,
|
||||
entriesExistState,
|
||||
policiesState,
|
||||
isEdit,
|
||||
isFetchingEditTrustedAppItem,
|
||||
editItemId,
|
||||
editingTrustedApp,
|
||||
getListItems,
|
||||
editItemState,
|
||||
} from './selectors';
|
||||
import { toUpdateTrustedApp } from '../../../../../common/endpoint/service/trusted_apps/to_update_trusted_app';
|
||||
|
||||
const createTrustedAppsListResourceStateChangedAction = (
|
||||
newState: Immutable<AsyncResourceState<TrustedAppsListData>>
|
||||
|
@ -139,9 +148,11 @@ const submitCreationIfNeeded = async (
|
|||
store: ImmutableMiddlewareAPI<TrustedAppsListPageState, AppAction>,
|
||||
trustedAppsService: TrustedAppsService
|
||||
) => {
|
||||
const submissionResourceState = getCreationSubmissionResourceState(store.getState());
|
||||
const isValid = isCreationDialogFormValid(store.getState());
|
||||
const entry = getCreationDialogFormEntry(store.getState());
|
||||
const currentState = store.getState();
|
||||
const submissionResourceState = getCreationSubmissionResourceState(currentState);
|
||||
const isValid = isCreationDialogFormValid(currentState);
|
||||
const entry = getCreationDialogFormEntry(currentState);
|
||||
const editMode = isEdit(currentState);
|
||||
|
||||
if (isStaleResourceState(submissionResourceState) && entry !== undefined && isValid) {
|
||||
store.dispatch(
|
||||
|
@ -152,12 +163,27 @@ const submitCreationIfNeeded = async (
|
|||
);
|
||||
|
||||
try {
|
||||
let responseTrustedApp: TrustedApp;
|
||||
|
||||
if (editMode) {
|
||||
responseTrustedApp = (
|
||||
await trustedAppsService.updateTrustedApp(
|
||||
{ id: editItemId(currentState)! },
|
||||
// TODO: try to remove the cast
|
||||
entry as PostTrustedAppCreateRequest
|
||||
)
|
||||
).data;
|
||||
} else {
|
||||
// TODO: try to remove the cast
|
||||
responseTrustedApp = (
|
||||
await trustedAppsService.createTrustedApp(entry as PostTrustedAppCreateRequest)
|
||||
).data;
|
||||
}
|
||||
|
||||
store.dispatch(
|
||||
createTrustedAppCreationSubmissionResourceStateChanged({
|
||||
type: 'LoadedResourceState',
|
||||
// TODO: try to remove the cast
|
||||
data: (await trustedAppsService.createTrustedApp(entry as PostTrustedAppCreateRequest))
|
||||
.data,
|
||||
data: responseTrustedApp,
|
||||
})
|
||||
);
|
||||
store.dispatch({
|
||||
|
@ -268,6 +294,139 @@ const checkTrustedAppsExistIfNeeded = async (
|
|||
}
|
||||
};
|
||||
|
||||
export const retrieveListOfPoliciesIfNeeded = async (
|
||||
{ getState, dispatch }: ImmutableMiddlewareAPI<TrustedAppsListPageState, AppAction>,
|
||||
trustedAppsService: TrustedAppsService
|
||||
) => {
|
||||
const currentState = getState();
|
||||
const currentPoliciesState = policiesState(currentState);
|
||||
const isLoading = isLoadingResourceState(currentPoliciesState);
|
||||
const isPageActive = trustedAppsListPageActive(currentState);
|
||||
const isCreateFlow = isCreationDialogLocation(currentState);
|
||||
|
||||
if (isPageActive && isCreateFlow && !isLoading) {
|
||||
dispatch({
|
||||
type: 'trustedAppsPoliciesStateChanged',
|
||||
payload: {
|
||||
type: 'LoadingResourceState',
|
||||
previousState: currentPoliciesState,
|
||||
} as TrustedAppsListPageState['policies'],
|
||||
});
|
||||
|
||||
try {
|
||||
const policyList = await trustedAppsService.getPolicyList({
|
||||
query: {
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: 'trustedAppsPoliciesStateChanged',
|
||||
payload: {
|
||||
type: 'LoadedResourceState',
|
||||
data: policyList,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
type: 'trustedAppsPoliciesStateChanged',
|
||||
payload: {
|
||||
type: 'FailedResourceState',
|
||||
error: error.body || error,
|
||||
lastLoadedState: getLastLoadedResourceState(policiesState(getState())),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchEditTrustedAppIfNeeded = async (
|
||||
{ getState, dispatch }: ImmutableMiddlewareAPI<TrustedAppsListPageState, AppAction>,
|
||||
trustedAppsService: TrustedAppsService
|
||||
) => {
|
||||
const currentState = getState();
|
||||
const isPageActive = trustedAppsListPageActive(currentState);
|
||||
const isEditFlow = isEdit(currentState);
|
||||
const isAlreadyFetching = isFetchingEditTrustedAppItem(currentState);
|
||||
const editTrustedAppId = editItemId(currentState);
|
||||
|
||||
if (isPageActive && isEditFlow && !isAlreadyFetching) {
|
||||
if (!editTrustedAppId) {
|
||||
const errorMessage = i18n.translate(
|
||||
'xpack.securitySolution.trustedapps.middleware.editIdMissing',
|
||||
{
|
||||
defaultMessage: 'No id provided',
|
||||
}
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: 'trustedAppCreationEditItemStateChanged',
|
||||
payload: {
|
||||
type: 'FailedResourceState',
|
||||
error: Object.assign(new Error(errorMessage), { statusCode: 404, error: errorMessage }),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let trustedAppForEdit = editingTrustedApp(currentState);
|
||||
|
||||
// If Trusted App is already loaded, then do nothing
|
||||
if (trustedAppForEdit && trustedAppForEdit.id === editTrustedAppId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// See if we can get the Trusted App record from the current list of Trusted Apps being displayed
|
||||
trustedAppForEdit = getListItems(currentState).find((ta) => ta.id === editTrustedAppId);
|
||||
|
||||
try {
|
||||
// Retrieve Trusted App record via API if it was not in the list data.
|
||||
// This would be the case when linking from another place or using an UUID for a Trusted App
|
||||
// that is not currently displayed on the list view.
|
||||
if (!trustedAppForEdit) {
|
||||
dispatch({
|
||||
type: 'trustedAppCreationEditItemStateChanged',
|
||||
payload: {
|
||||
type: 'LoadingResourceState',
|
||||
// No easy way to get around this that I can see. `previousState` does not
|
||||
// seem to allow everything that `editItem` state can hold, so not even sure if using
|
||||
// type guards would work here
|
||||
// @ts-ignore
|
||||
previousState: editItemState(currentState)!,
|
||||
},
|
||||
});
|
||||
|
||||
trustedAppForEdit = (await trustedAppsService.getTrustedApp({ id: editTrustedAppId })).data;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'trustedAppCreationEditItemStateChanged',
|
||||
payload: {
|
||||
type: 'LoadedResourceState',
|
||||
data: trustedAppForEdit,
|
||||
},
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: 'trustedAppCreationDialogFormStateUpdated',
|
||||
payload: {
|
||||
entry: toUpdateTrustedApp(trustedAppForEdit),
|
||||
isValid: true,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
dispatch({
|
||||
type: 'trustedAppCreationEditItemStateChanged',
|
||||
payload: {
|
||||
type: 'FailedResourceState',
|
||||
error: e,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const createTrustedAppsPageMiddleware = (
|
||||
trustedAppsService: TrustedAppsService
|
||||
): ImmutableMiddleware<TrustedAppsListPageState, AppAction> => {
|
||||
|
@ -282,6 +441,8 @@ export const createTrustedAppsPageMiddleware = (
|
|||
|
||||
if (action.type === 'userChangedUrl') {
|
||||
updateCreationDialogIfNeeded(store);
|
||||
retrieveListOfPoliciesIfNeeded(store, trustedAppsService);
|
||||
fetchEditTrustedAppIfNeeded(store, trustedAppsService);
|
||||
}
|
||||
|
||||
if (action.type === 'trustedAppCreationDialogConfirmed') {
|
||||
|
|
|
@ -37,7 +37,13 @@ describe('reducer', () => {
|
|||
|
||||
expect(result).toStrictEqual({
|
||||
...initialState,
|
||||
location: { page_index: 5, page_size: 50, show: 'create', view_type: 'list' },
|
||||
location: {
|
||||
page_index: 5,
|
||||
page_size: 50,
|
||||
show: 'create',
|
||||
view_type: 'list',
|
||||
id: undefined,
|
||||
},
|
||||
active: true,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,6 +29,8 @@ import {
|
|||
TrustedAppCreationDialogConfirmed,
|
||||
TrustedAppCreationDialogClosed,
|
||||
TrustedAppsExistResponse,
|
||||
TrustedAppsPoliciesStateChanged,
|
||||
TrustedAppCreationEditItemStateChanged,
|
||||
} from './action';
|
||||
|
||||
import { TrustedAppsListPageState } from '../state';
|
||||
|
@ -37,7 +39,7 @@ import {
|
|||
initialDeletionDialogState,
|
||||
initialTrustedAppsPageState,
|
||||
} from './builders';
|
||||
import { entriesExistState } from './selectors';
|
||||
import { entriesExistState, trustedAppsListPageActive } from './selectors';
|
||||
|
||||
type StateReducer = ImmutableReducer<TrustedAppsListPageState, AppAction>;
|
||||
type CaseReducer<T extends AppAction> = (
|
||||
|
@ -110,7 +112,7 @@ const trustedAppCreationDialogStarted: CaseReducer<TrustedAppCreationDialogStart
|
|||
...state,
|
||||
creationDialog: {
|
||||
...initialCreationDialogState(),
|
||||
formState: { ...action.payload, isValid: true },
|
||||
formState: { ...action.payload, isValid: false },
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -125,6 +127,16 @@ const trustedAppCreationDialogFormStateUpdated: CaseReducer<TrustedAppCreationDi
|
|||
};
|
||||
};
|
||||
|
||||
const handleUpdateToEditItemState: CaseReducer<TrustedAppCreationEditItemStateChanged> = (
|
||||
state,
|
||||
action
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
creationDialog: { ...state.creationDialog, editItem: action.payload },
|
||||
};
|
||||
};
|
||||
|
||||
const trustedAppCreationDialogConfirmed: CaseReducer<TrustedAppCreationDialogConfirmed> = (
|
||||
state
|
||||
) => {
|
||||
|
@ -155,6 +167,16 @@ const updateEntriesExists: CaseReducer<TrustedAppsExistResponse> = (state, { pay
|
|||
return state;
|
||||
};
|
||||
|
||||
const updatePolicies: CaseReducer<TrustedAppsPoliciesStateChanged> = (state, { payload }) => {
|
||||
if (trustedAppsListPageActive(state)) {
|
||||
return {
|
||||
...state,
|
||||
policies: payload,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export const trustedAppsPageReducer: StateReducer = (
|
||||
state = initialTrustedAppsPageState(),
|
||||
action
|
||||
|
@ -187,6 +209,9 @@ export const trustedAppsPageReducer: StateReducer = (
|
|||
case 'trustedAppCreationDialogFormStateUpdated':
|
||||
return trustedAppCreationDialogFormStateUpdated(state, action);
|
||||
|
||||
case 'trustedAppCreationEditItemStateChanged':
|
||||
return handleUpdateToEditItemState(state, action);
|
||||
|
||||
case 'trustedAppCreationDialogConfirmed':
|
||||
return trustedAppCreationDialogConfirmed(state, action);
|
||||
|
||||
|
@ -198,6 +223,9 @@ export const trustedAppsPageReducer: StateReducer = (
|
|||
|
||||
case 'trustedAppsExistStateChanged':
|
||||
return updateEntriesExists(state, action);
|
||||
|
||||
case 'trustedAppsPoliciesStateChanged':
|
||||
return updatePolicies(state, action);
|
||||
}
|
||||
|
||||
return state;
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
TrustedAppsListPageLocation,
|
||||
TrustedAppsListPageState,
|
||||
} from '../state';
|
||||
import { GetPolicyListResponse } from '../../policy/types';
|
||||
|
||||
export const needsRefreshOfListData = (state: Immutable<TrustedAppsListPageState>): boolean => {
|
||||
const freshDataTimestamp = state.listView.freshDataTimestamp;
|
||||
|
@ -130,7 +131,7 @@ export const getDeletionDialogEntry = (
|
|||
};
|
||||
|
||||
export const isCreationDialogLocation = (state: Immutable<TrustedAppsListPageState>): boolean => {
|
||||
return state.location.show === 'create';
|
||||
return !!state.location.show;
|
||||
};
|
||||
|
||||
export const getCreationSubmissionResourceState = (
|
||||
|
@ -185,3 +186,56 @@ export const entriesExist: (state: Immutable<TrustedAppsListPageState>) => boole
|
|||
export const trustedAppsListPageActive: (state: Immutable<TrustedAppsListPageState>) => boolean = (
|
||||
state
|
||||
) => state.active;
|
||||
|
||||
export const policiesState = (
|
||||
state: Immutable<TrustedAppsListPageState>
|
||||
): Immutable<TrustedAppsListPageState['policies']> => state.policies;
|
||||
|
||||
export const loadingPolicies: (
|
||||
state: Immutable<TrustedAppsListPageState>
|
||||
) => boolean = createSelector(policiesState, (policies) => isLoadingResourceState(policies));
|
||||
|
||||
export const listOfPolicies: (
|
||||
state: Immutable<TrustedAppsListPageState>
|
||||
) => Immutable<GetPolicyListResponse['items']> = createSelector(policiesState, (policies) => {
|
||||
return isLoadedResourceState(policies) ? policies.data.items : [];
|
||||
});
|
||||
|
||||
export const isEdit: (state: Immutable<TrustedAppsListPageState>) => boolean = createSelector(
|
||||
getCurrentLocation,
|
||||
({ show }) => {
|
||||
return show === 'edit';
|
||||
}
|
||||
);
|
||||
|
||||
export const editItemId: (
|
||||
state: Immutable<TrustedAppsListPageState>
|
||||
) => string | undefined = createSelector(getCurrentLocation, ({ id }) => {
|
||||
return id;
|
||||
});
|
||||
|
||||
export const editItemState: (
|
||||
state: Immutable<TrustedAppsListPageState>
|
||||
) => Immutable<TrustedAppsListPageState>['creationDialog']['editItem'] = (state) => {
|
||||
return state.creationDialog.editItem;
|
||||
};
|
||||
|
||||
export const isFetchingEditTrustedAppItem: (
|
||||
state: Immutable<TrustedAppsListPageState>
|
||||
) => boolean = createSelector(editItemState, (editTrustedAppState) => {
|
||||
return editTrustedAppState ? isLoadingResourceState(editTrustedAppState) : false;
|
||||
});
|
||||
|
||||
export const editTrustedAppFetchError: (
|
||||
state: Immutable<TrustedAppsListPageState>
|
||||
) => ServerApiError | undefined = createSelector(editItemState, (itemForEditState) => {
|
||||
return itemForEditState && getCurrentResourceError(itemForEditState);
|
||||
});
|
||||
|
||||
export const editingTrustedApp: (
|
||||
state: Immutable<TrustedAppsListPageState>
|
||||
) => undefined | Immutable<TrustedApp> = createSelector(editItemState, (editTrustedAppState) => {
|
||||
if (editTrustedAppState && isLoadedResourceState(editTrustedAppState)) {
|
||||
return editTrustedAppState.data;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -44,12 +44,16 @@ const generate = <T>(count: number, generator: (i: number) => T) =>
|
|||
export const createSampleTrustedApp = (i: number, longTexts?: boolean): TrustedApp => {
|
||||
return {
|
||||
id: String(i),
|
||||
version: 'abc123',
|
||||
name: generate(longTexts ? 10 : 1, () => `trusted app ${i}`).join(' '),
|
||||
description: generate(longTexts ? 10 : 1, () => `Trusted App ${i}`).join(' '),
|
||||
created_at: '1 minute ago',
|
||||
created_by: 'someone',
|
||||
updated_at: '1 minute ago',
|
||||
updated_by: 'someone',
|
||||
os: OPERATING_SYSTEMS[i % 3],
|
||||
entries: [],
|
||||
effectScope: { type: 'global' },
|
||||
};
|
||||
};
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -22,26 +22,46 @@ import React, { memo, useCallback, useEffect, useMemo } from 'react';
|
|||
import { EuiFlyoutProps } from '@elastic/eui/src/components/flyout/flyout';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CreateTrustedAppForm, CreateTrustedAppFormProps } from './create_trusted_app_form';
|
||||
import {
|
||||
editTrustedAppFetchError,
|
||||
getCreationDialogFormEntry,
|
||||
getCreationError,
|
||||
getCurrentLocation,
|
||||
isCreationDialogFormValid,
|
||||
isCreationInProgress,
|
||||
isCreationSuccessful,
|
||||
isEdit,
|
||||
listOfPolicies,
|
||||
loadingPolicies,
|
||||
} from '../../store/selectors';
|
||||
import { AppAction } from '../../../../../common/store/actions';
|
||||
import { useTrustedAppsSelector } from '../hooks';
|
||||
|
||||
import { ABOUT_TRUSTED_APPS, CREATE_TRUSTED_APP_ERROR } from '../translations';
|
||||
import { defaultNewTrustedApp } from '../../store/builders';
|
||||
import { getTrustedAppsListPath } from '../../../../common/routing';
|
||||
import { useToasts } from '../../../../../common/lib/kibana';
|
||||
|
||||
type CreateTrustedAppFlyoutProps = Omit<EuiFlyoutProps, 'hideCloseButton'>;
|
||||
export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
|
||||
({ onClose, ...flyoutProps }) => {
|
||||
const dispatch = useDispatch<(action: AppAction) => void>();
|
||||
const history = useHistory();
|
||||
const toasts = useToasts();
|
||||
|
||||
const creationInProgress = useTrustedAppsSelector(isCreationInProgress);
|
||||
const creationErrors = useTrustedAppsSelector(getCreationError);
|
||||
const creationSuccessful = useTrustedAppsSelector(isCreationSuccessful);
|
||||
const isFormValid = useTrustedAppsSelector(isCreationDialogFormValid);
|
||||
const isLoadingPolicies = useTrustedAppsSelector(loadingPolicies);
|
||||
const policyList = useTrustedAppsSelector(listOfPolicies);
|
||||
const isEditMode = useTrustedAppsSelector(isEdit);
|
||||
const trustedAppFetchError = useTrustedAppsSelector(editTrustedAppFetchError);
|
||||
const formValues = useTrustedAppsSelector(getCreationDialogFormEntry) || defaultNewTrustedApp();
|
||||
const location = useTrustedAppsSelector(getCurrentLocation);
|
||||
|
||||
const dataTestSubj = flyoutProps['data-test-subj'];
|
||||
|
||||
|
@ -53,6 +73,13 @@ export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
|
|||
: undefined,
|
||||
[creationErrors]
|
||||
);
|
||||
const policies = useMemo<CreateTrustedAppFormProps['policies']>(() => {
|
||||
return {
|
||||
// Casting is needed due to the use of `Immutable<>` on the return value from the selector above
|
||||
options: policyList as CreateTrustedAppFormProps['policies']['options'],
|
||||
isLoading: isLoadingPolicies,
|
||||
};
|
||||
}, [isLoadingPolicies, policyList]);
|
||||
|
||||
const getTestId = useCallback(
|
||||
(suffix: string): string | undefined => {
|
||||
|
@ -62,16 +89,19 @@ export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
|
|||
},
|
||||
[dataTestSubj]
|
||||
);
|
||||
|
||||
const handleCancelClick = useCallback(() => {
|
||||
if (creationInProgress) {
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
}, [onClose, creationInProgress]);
|
||||
|
||||
const handleSaveClick = useCallback(
|
||||
() => dispatch({ type: 'trustedAppCreationDialogConfirmed' }),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleFormOnChange = useCallback<CreateTrustedAppFormProps['onChange']>(
|
||||
(newFormState) => {
|
||||
dispatch({
|
||||
|
@ -82,6 +112,33 @@ export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
|
|||
[dispatch]
|
||||
);
|
||||
|
||||
// If there was a failure trying to retrieve the Trusted App for edit item,
|
||||
// then redirect back to the list ++ show toast message.
|
||||
useEffect(() => {
|
||||
if (trustedAppFetchError) {
|
||||
// Replace the current URL route so that user does not keep hitting this page via browser back/fwd buttons
|
||||
history.replace(
|
||||
getTrustedAppsListPath({
|
||||
...location,
|
||||
show: undefined,
|
||||
id: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
toasts.addWarning(
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.trustedapps.createTrustedAppFlyout.notFoundToastMessage',
|
||||
{
|
||||
defaultMessage: 'Unable to edit trusted application ({apiMsg})',
|
||||
values: {
|
||||
apiMsg: trustedAppFetchError.message,
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [history, location, toasts, trustedAppFetchError]);
|
||||
|
||||
// If it was created, then close flyout
|
||||
useEffect(() => {
|
||||
if (creationSuccessful) {
|
||||
|
@ -94,24 +151,35 @@ export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
|
|||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2 data-test-subj={getTestId('headerTitle')}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.trustedapps.createTrustedAppFlyout.title"
|
||||
defaultMessage="Add trusted application"
|
||||
/>
|
||||
{isEditMode ? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.trustedapps.createTrustedAppFlyout.editTitle"
|
||||
defaultMessage="Edit trusted application"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.trustedapps.createTrustedAppFlyout.createTitle"
|
||||
defaultMessage="Add trusted application"
|
||||
/>
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<EuiFlyoutBody>
|
||||
<EuiText color="subdued" size="xs">
|
||||
<p data-test-subj={getTestId('about')}>{ABOUT_TRUSTED_APPS}</p>
|
||||
<EuiSpacer size="m" />
|
||||
</EuiText>
|
||||
{!isEditMode && (
|
||||
<EuiText color="subdued" size="xs">
|
||||
<p data-test-subj={getTestId('about')}>{ABOUT_TRUSTED_APPS}</p>
|
||||
<EuiSpacer size="m" />
|
||||
</EuiText>
|
||||
)}
|
||||
<CreateTrustedAppForm
|
||||
fullWidth
|
||||
onChange={handleFormOnChange}
|
||||
isInvalid={!!creationErrors}
|
||||
error={creationErrorsMessage}
|
||||
policies={policies}
|
||||
trustedApp={formValues}
|
||||
data-test-subj={getTestId('createForm')}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
|
@ -139,10 +207,17 @@ export const CreateTrustedAppFlyout = memo<CreateTrustedAppFlyoutProps>(
|
|||
isLoading={creationInProgress}
|
||||
data-test-subj={getTestId('createButton')}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.trustedapps.createTrustedAppFlyout.saveButton"
|
||||
defaultMessage="Add trusted application"
|
||||
/>
|
||||
{isEditMode ? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.trustedapps.createTrustedAppFlyout.editSaveButton"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.trustedapps.createTrustedAppFlyout.createSaveButton"
|
||||
defaultMessage="Add trusted application"
|
||||
/>
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -9,20 +9,48 @@ import React from 'react';
|
|||
import * as reactTestingLibrary from '@testing-library/react';
|
||||
import { fireEvent, getByTestId } from '@testing-library/dom';
|
||||
|
||||
import { ConditionEntryField, OperatingSystem } from '../../../../../../common/endpoint/types';
|
||||
import {
|
||||
ConditionEntryField,
|
||||
NewTrustedApp,
|
||||
OperatingSystem,
|
||||
} from '../../../../../../common/endpoint/types';
|
||||
import {
|
||||
AppContextTestRender,
|
||||
createAppRootMockRenderer,
|
||||
} from '../../../../../common/mock/endpoint';
|
||||
|
||||
import { CreateTrustedAppForm, CreateTrustedAppFormProps } from './create_trusted_app_form';
|
||||
import { defaultNewTrustedApp } from '../../store/builders';
|
||||
import { forceHTMLElementOffsetWidth } from './effected_policy_select/test_utils';
|
||||
import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||
|
||||
describe('When showing the Trusted App Create Form', () => {
|
||||
jest.mock('../../../../../common/hooks/use_experimental_features');
|
||||
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
|
||||
|
||||
describe('When using the Trusted App Form', () => {
|
||||
const dataTestSubjForForm = 'createForm';
|
||||
type RenderResultType = ReturnType<AppContextTestRender['render']>;
|
||||
const generator = new EndpointDocGenerator('effected-policy-select');
|
||||
|
||||
let render: () => RenderResultType;
|
||||
let resetHTMLElementOffsetWidth: ReturnType<typeof forceHTMLElementOffsetWidth>;
|
||||
|
||||
let mockedContext: AppContextTestRender;
|
||||
let formProps: jest.Mocked<CreateTrustedAppFormProps>;
|
||||
let renderResult: ReturnType<AppContextTestRender['render']>;
|
||||
|
||||
// As the form's `onChange()` callback is executed, this variable will
|
||||
// hold the latest updated trusted app. Use it to re-render
|
||||
let latestUpdatedTrustedApp: NewTrustedApp;
|
||||
|
||||
const getUI = () => <CreateTrustedAppForm {...formProps} />;
|
||||
const render = () => {
|
||||
return (renderResult = mockedContext.render(getUI()));
|
||||
};
|
||||
const rerender = () => renderResult.rerender(getUI());
|
||||
const rerenderWithLatestTrustedApp = () => {
|
||||
formProps.trustedApp = latestUpdatedTrustedApp;
|
||||
rerender();
|
||||
};
|
||||
|
||||
// Some helpers
|
||||
const setTextFieldValue = (textField: HTMLInputElement | HTMLTextAreaElement, value: string) => {
|
||||
|
@ -33,35 +61,27 @@ describe('When showing the Trusted App Create Form', () => {
|
|||
fireEvent.blur(textField);
|
||||
});
|
||||
};
|
||||
const getNameField = (
|
||||
renderResult: RenderResultType,
|
||||
dataTestSub: string = dataTestSubjForForm
|
||||
): HTMLInputElement => {
|
||||
const getNameField = (dataTestSub: string = dataTestSubjForForm): HTMLInputElement => {
|
||||
return renderResult.getByTestId(`${dataTestSub}-nameTextField`) as HTMLInputElement;
|
||||
};
|
||||
const getOsField = (
|
||||
renderResult: RenderResultType,
|
||||
dataTestSub: string = dataTestSubjForForm
|
||||
): HTMLButtonElement => {
|
||||
const getOsField = (dataTestSub: string = dataTestSubjForForm): HTMLButtonElement => {
|
||||
return renderResult.getByTestId(`${dataTestSub}-osSelectField`) as HTMLButtonElement;
|
||||
};
|
||||
const getDescriptionField = (
|
||||
renderResult: RenderResultType,
|
||||
dataTestSub: string = dataTestSubjForForm
|
||||
): HTMLTextAreaElement => {
|
||||
const getGlobalSwitchField = (dataTestSub: string = dataTestSubjForForm): HTMLButtonElement => {
|
||||
return renderResult.getByTestId(
|
||||
`${dataTestSub}-effectedPolicies-globalSwitch`
|
||||
) as HTMLButtonElement;
|
||||
};
|
||||
const getDescriptionField = (dataTestSub: string = dataTestSubjForForm): HTMLTextAreaElement => {
|
||||
return renderResult.getByTestId(`${dataTestSub}-descriptionField`) as HTMLTextAreaElement;
|
||||
};
|
||||
const getCondition = (
|
||||
renderResult: RenderResultType,
|
||||
index: number = 0,
|
||||
dataTestSub: string = dataTestSubjForForm
|
||||
): HTMLElement => {
|
||||
return renderResult.getByTestId(`${dataTestSub}-conditionsBuilder-group1-entry${index}`);
|
||||
};
|
||||
const getAllConditions = (
|
||||
renderResult: RenderResultType,
|
||||
dataTestSub: string = dataTestSubjForForm
|
||||
): HTMLElement[] => {
|
||||
const getAllConditions = (dataTestSub: string = dataTestSubjForForm): HTMLElement[] => {
|
||||
return Array.from(
|
||||
renderResult.getByTestId(`${dataTestSub}-conditionsBuilder-group1-entries`).children
|
||||
) as HTMLElement[];
|
||||
|
@ -76,7 +96,6 @@ describe('When showing the Trusted App Create Form', () => {
|
|||
return getByTestId(condition, `${condition.dataset.testSubj}-value`) as HTMLInputElement;
|
||||
};
|
||||
const getConditionBuilderAndButton = (
|
||||
renderResult: RenderResultType,
|
||||
dataTestSub: string = dataTestSubjForForm
|
||||
): HTMLButtonElement => {
|
||||
return renderResult.getByTestId(
|
||||
|
@ -84,65 +103,84 @@ describe('When showing the Trusted App Create Form', () => {
|
|||
) as HTMLButtonElement;
|
||||
};
|
||||
const getConditionBuilderAndConnectorBadge = (
|
||||
renderResult: RenderResultType,
|
||||
dataTestSub: string = dataTestSubjForForm
|
||||
): HTMLElement => {
|
||||
return renderResult.getByTestId(`${dataTestSub}-conditionsBuilder-group1-andConnector`);
|
||||
};
|
||||
const getAllValidationErrors = (renderResult: RenderResultType): HTMLElement[] => {
|
||||
const getAllValidationErrors = (): HTMLElement[] => {
|
||||
return Array.from(renderResult.container.querySelectorAll('.euiFormErrorText'));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const mockedContext = createAppRootMockRenderer();
|
||||
resetHTMLElementOffsetWidth = forceHTMLElementOffsetWidth();
|
||||
useIsExperimentalFeatureEnabledMock.mockReturnValue(true);
|
||||
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
|
||||
latestUpdatedTrustedApp = defaultNewTrustedApp();
|
||||
|
||||
formProps = {
|
||||
'data-test-subj': dataTestSubjForForm,
|
||||
onChange: jest.fn(),
|
||||
trustedApp: latestUpdatedTrustedApp,
|
||||
onChange: jest.fn((updates) => {
|
||||
latestUpdatedTrustedApp = updates.item;
|
||||
}),
|
||||
policies: {
|
||||
options: [],
|
||||
},
|
||||
};
|
||||
render = () => mockedContext.render(<CreateTrustedAppForm {...formProps} />);
|
||||
});
|
||||
|
||||
it('should show Name as required', () => {
|
||||
expect(getNameField(render()).required).toBe(true);
|
||||
afterEach(() => {
|
||||
resetHTMLElementOffsetWidth();
|
||||
reactTestingLibrary.cleanup();
|
||||
});
|
||||
|
||||
it('should default OS to Windows', () => {
|
||||
expect(getOsField(render()).textContent).toEqual('Windows');
|
||||
});
|
||||
describe('and the form is rendered', () => {
|
||||
beforeEach(() => render());
|
||||
|
||||
it('should allow user to select between 3 OSs', () => {
|
||||
const renderResult = render();
|
||||
const osField = getOsField(renderResult);
|
||||
reactTestingLibrary.act(() => {
|
||||
fireEvent.click(osField, { button: 1 });
|
||||
it('should show Name as required', () => {
|
||||
expect(getNameField().required).toBe(true);
|
||||
});
|
||||
const options = Array.from(
|
||||
renderResult.baseElement.querySelectorAll(
|
||||
'.euiSuperSelect__listbox button.euiSuperSelect__item'
|
||||
)
|
||||
).map((button) => button.textContent);
|
||||
expect(options).toEqual(['Mac', 'Windows', 'Linux']);
|
||||
});
|
||||
|
||||
it('should show Description as optional', () => {
|
||||
expect(getDescriptionField(render()).required).toBe(false);
|
||||
});
|
||||
it('should default OS to Windows', () => {
|
||||
expect(getOsField().textContent).toEqual('Windows');
|
||||
});
|
||||
|
||||
it('should NOT initially show any inline validation errors', () => {
|
||||
expect(render().container.querySelectorAll('.euiFormErrorText').length).toBe(0);
|
||||
});
|
||||
it('should allow user to select between 3 OSs', () => {
|
||||
const osField = getOsField();
|
||||
reactTestingLibrary.act(() => {
|
||||
fireEvent.click(osField, { button: 1 });
|
||||
});
|
||||
const options = Array.from(
|
||||
renderResult.baseElement.querySelectorAll(
|
||||
'.euiSuperSelect__listbox button.euiSuperSelect__item'
|
||||
)
|
||||
).map((button) => button.textContent);
|
||||
expect(options).toEqual(['Mac', 'Windows', 'Linux']);
|
||||
});
|
||||
|
||||
it('should show top-level Errors', () => {
|
||||
formProps.isInvalid = true;
|
||||
formProps.error = 'a top level error';
|
||||
const { queryByText } = render();
|
||||
expect(queryByText(formProps.error as string)).not.toBeNull();
|
||||
it('should show Description as optional', () => {
|
||||
expect(getDescriptionField().required).toBe(false);
|
||||
});
|
||||
|
||||
it('should NOT initially show any inline validation errors', () => {
|
||||
expect(renderResult.container.querySelectorAll('.euiFormErrorText').length).toBe(0);
|
||||
});
|
||||
|
||||
it('should show top-level Errors', () => {
|
||||
formProps.isInvalid = true;
|
||||
formProps.error = 'a top level error';
|
||||
rerender();
|
||||
expect(renderResult.queryByText(formProps.error as string)).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('the condition builder component', () => {
|
||||
beforeEach(() => render());
|
||||
|
||||
it('should show an initial condition entry with labels', () => {
|
||||
const defaultCondition = getCondition(render());
|
||||
const defaultCondition = getCondition();
|
||||
const labels = Array.from(
|
||||
defaultCondition.querySelectorAll('.euiFormRow__labelWrapper')
|
||||
).map((label) => (label.textContent || '').trim());
|
||||
|
@ -150,13 +188,12 @@ describe('When showing the Trusted App Create Form', () => {
|
|||
});
|
||||
|
||||
it('should not allow the entry to be removed if its the only one displayed', () => {
|
||||
const defaultCondition = getCondition(render());
|
||||
const defaultCondition = getCondition();
|
||||
expect(getConditionRemoveButton(defaultCondition).disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should display 2 options for Field', () => {
|
||||
const renderResult = render();
|
||||
const conditionFieldSelect = getConditionFieldSelect(getCondition(renderResult));
|
||||
const conditionFieldSelect = getConditionFieldSelect(getCondition());
|
||||
reactTestingLibrary.act(() => {
|
||||
fireEvent.click(conditionFieldSelect, { button: 1 });
|
||||
});
|
||||
|
@ -173,53 +210,113 @@ describe('When showing the Trusted App Create Form', () => {
|
|||
});
|
||||
|
||||
it('should show the value field as required', () => {
|
||||
expect(getConditionValue(getCondition(render())).required).toEqual(true);
|
||||
expect(getConditionValue(getCondition()).required).toEqual(true);
|
||||
});
|
||||
|
||||
it('should display the `AND` button', () => {
|
||||
const andButton = getConditionBuilderAndButton(render());
|
||||
const andButton = getConditionBuilderAndButton();
|
||||
expect(andButton.textContent).toEqual('AND');
|
||||
expect(andButton.disabled).toEqual(false);
|
||||
});
|
||||
|
||||
describe('and when the AND button is clicked', () => {
|
||||
let renderResult: RenderResultType;
|
||||
|
||||
beforeEach(() => {
|
||||
renderResult = render();
|
||||
const andButton = getConditionBuilderAndButton(renderResult);
|
||||
const andButton = getConditionBuilderAndButton();
|
||||
reactTestingLibrary.act(() => {
|
||||
fireEvent.click(andButton, { button: 1 });
|
||||
});
|
||||
// re-render with updated `newTrustedApp`
|
||||
formProps.trustedApp = formProps.onChange.mock.calls[0][0].item;
|
||||
rerender();
|
||||
});
|
||||
|
||||
it('should add a new condition entry when `AND` is clicked with no labels', () => {
|
||||
const condition2 = getCondition(renderResult, 1);
|
||||
it('should add a new condition entry when `AND` is clicked with no column labels', () => {
|
||||
const condition2 = getCondition(1);
|
||||
expect(condition2.querySelectorAll('.euiFormRow__labelWrapper')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should have remove buttons enabled when multiple conditions are present', () => {
|
||||
getAllConditions(renderResult).forEach((condition) => {
|
||||
getAllConditions().forEach((condition) => {
|
||||
expect(getConditionRemoveButton(condition).disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should show the AND visual connector when multiple entries are present', () => {
|
||||
expect(getConditionBuilderAndConnectorBadge(renderResult).textContent).toEqual('AND');
|
||||
expect(getConditionBuilderAndConnectorBadge().textContent).toEqual('AND');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and the user visits required fields but does not fill them out', () => {
|
||||
let renderResult: RenderResultType;
|
||||
describe('the Policy Selection area', () => {
|
||||
it('should show loader when setting `policies.isLoading` to true', () => {
|
||||
formProps.policies.isLoading = true;
|
||||
render();
|
||||
expect(
|
||||
renderResult.getByTestId(`${dataTestSubjForForm}-effectedPolicies-policiesSelectable`)
|
||||
.textContent
|
||||
).toEqual('Loading options');
|
||||
});
|
||||
|
||||
describe('and policies exist', () => {
|
||||
beforeEach(() => {
|
||||
const policy = generator.generatePolicyPackagePolicy();
|
||||
policy.name = 'test policy A';
|
||||
policy.id = '123';
|
||||
|
||||
formProps.policies.options = [policy];
|
||||
});
|
||||
|
||||
it('should display the policies available, but disabled if ', () => {
|
||||
render();
|
||||
expect(renderResult.getByTestId('policy-123'));
|
||||
});
|
||||
|
||||
it('should have `global` switch on if effective scope is global and policy options disabled', () => {
|
||||
render();
|
||||
expect(getGlobalSwitchField().getAttribute('aria-checked')).toEqual('true');
|
||||
expect(renderResult.getByTestId('policy-123').getAttribute('aria-disabled')).toEqual(
|
||||
'true'
|
||||
);
|
||||
expect(renderResult.getByTestId('policy-123').getAttribute('aria-selected')).toEqual(
|
||||
'false'
|
||||
);
|
||||
});
|
||||
|
||||
it('should have specific policies checked if scope is per-policy', () => {
|
||||
(formProps.trustedApp as NewTrustedApp).effectScope = {
|
||||
type: 'policy',
|
||||
policies: ['123'],
|
||||
};
|
||||
render();
|
||||
expect(getGlobalSwitchField().getAttribute('aria-checked')).toEqual('false');
|
||||
expect(renderResult.getByTestId('policy-123').getAttribute('aria-disabled')).toEqual(
|
||||
'false'
|
||||
);
|
||||
expect(renderResult.getByTestId('policy-123').getAttribute('aria-selected')).toEqual(
|
||||
'true'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('the Policy Selection area under feature flag', () => {
|
||||
it("shouldn't display the policiy selection area ", () => {
|
||||
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
|
||||
render();
|
||||
expect(
|
||||
renderResult.queryByText('Apply trusted application globally')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and the user visits required fields but does not fill them out', () => {
|
||||
beforeEach(() => {
|
||||
renderResult = render();
|
||||
render();
|
||||
reactTestingLibrary.act(() => {
|
||||
fireEvent.blur(getNameField(renderResult));
|
||||
fireEvent.blur(getNameField());
|
||||
});
|
||||
reactTestingLibrary.act(() => {
|
||||
fireEvent.blur(getConditionValue(getCondition(renderResult)));
|
||||
fireEvent.blur(getConditionValue(getCondition()));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -232,68 +329,51 @@ describe('When showing the Trusted App Create Form', () => {
|
|||
});
|
||||
|
||||
it('should NOT display any other errors', () => {
|
||||
expect(getAllValidationErrors(renderResult)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should call change callback with isValid set to false and contain the new item', () => {
|
||||
expect(formProps.onChange).toHaveBeenCalledWith({
|
||||
isValid: false,
|
||||
item: {
|
||||
description: '',
|
||||
entries: [
|
||||
{
|
||||
field: ConditionEntryField.HASH,
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
name: '',
|
||||
os: OperatingSystem.WINDOWS,
|
||||
},
|
||||
});
|
||||
expect(getAllValidationErrors()).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and invalid data is entered', () => {
|
||||
let renderResult: RenderResultType;
|
||||
|
||||
beforeEach(() => {
|
||||
renderResult = render();
|
||||
});
|
||||
beforeEach(() => render());
|
||||
|
||||
it('should validate that Name has a non empty space value', () => {
|
||||
setTextFieldValue(getNameField(renderResult), ' ');
|
||||
setTextFieldValue(getNameField(), ' ');
|
||||
expect(renderResult.getByText('Name is required'));
|
||||
});
|
||||
|
||||
it('should validate invalid Hash value', () => {
|
||||
setTextFieldValue(getConditionValue(getCondition(renderResult)), 'someHASH');
|
||||
setTextFieldValue(getConditionValue(getCondition()), 'someHASH');
|
||||
expect(renderResult.getByText('[1] Invalid hash value'));
|
||||
});
|
||||
|
||||
it('should validate that a condition value has a non empty space value', () => {
|
||||
setTextFieldValue(getConditionValue(getCondition(renderResult)), ' ');
|
||||
setTextFieldValue(getConditionValue(getCondition()), ' ');
|
||||
expect(renderResult.getByText('[1] Field entry must have a value'));
|
||||
});
|
||||
|
||||
it('should validate all condition values (when multiples exist) have non empty space value', () => {
|
||||
const andButton = getConditionBuilderAndButton(renderResult);
|
||||
const andButton = getConditionBuilderAndButton();
|
||||
reactTestingLibrary.act(() => {
|
||||
fireEvent.click(andButton, { button: 1 });
|
||||
});
|
||||
rerenderWithLatestTrustedApp();
|
||||
|
||||
setTextFieldValue(getConditionValue(getCondition()), 'someHASH');
|
||||
rerenderWithLatestTrustedApp();
|
||||
|
||||
setTextFieldValue(getConditionValue(getCondition(renderResult)), 'someHASH');
|
||||
expect(renderResult.getByText('[2] Field entry must have a value'));
|
||||
});
|
||||
|
||||
it('should validate multiple errors in form', () => {
|
||||
const andButton = getConditionBuilderAndButton(renderResult);
|
||||
const andButton = getConditionBuilderAndButton();
|
||||
|
||||
reactTestingLibrary.act(() => {
|
||||
fireEvent.click(andButton, { button: 1 });
|
||||
});
|
||||
rerenderWithLatestTrustedApp();
|
||||
|
||||
setTextFieldValue(getConditionValue(getCondition(renderResult)), 'someHASH');
|
||||
setTextFieldValue(getConditionValue(getCondition()), 'someHASH');
|
||||
rerenderWithLatestTrustedApp();
|
||||
expect(renderResult.getByText('[1] Invalid hash value'));
|
||||
expect(renderResult.getByText('[2] Field entry must have a value'));
|
||||
});
|
||||
|
@ -301,19 +381,25 @@ describe('When showing the Trusted App Create Form', () => {
|
|||
|
||||
describe('and all required data passes validation', () => {
|
||||
it('should call change callback with isValid set to true and contain the new item', () => {
|
||||
const renderResult = render();
|
||||
setTextFieldValue(getNameField(renderResult), 'Some Process');
|
||||
setTextFieldValue(
|
||||
getConditionValue(getCondition(renderResult)),
|
||||
'e50fb1a0e5fff590ece385082edc6c41'
|
||||
);
|
||||
setTextFieldValue(getDescriptionField(renderResult), 'some description');
|
||||
render();
|
||||
|
||||
expect(getAllValidationErrors(renderResult)).toHaveLength(0);
|
||||
setTextFieldValue(getNameField(), 'Some Process');
|
||||
rerenderWithLatestTrustedApp();
|
||||
|
||||
setTextFieldValue(getConditionValue(getCondition()), 'e50fb1a0e5fff590ece385082edc6c41');
|
||||
rerenderWithLatestTrustedApp();
|
||||
|
||||
setTextFieldValue(getDescriptionField(), 'some description');
|
||||
rerenderWithLatestTrustedApp();
|
||||
|
||||
expect(getAllValidationErrors()).toHaveLength(0);
|
||||
expect(formProps.onChange).toHaveBeenLastCalledWith({
|
||||
isValid: true,
|
||||
item: {
|
||||
name: 'Some Process',
|
||||
description: 'some description',
|
||||
os: OperatingSystem.WINDOWS,
|
||||
effectScope: { type: 'global' },
|
||||
entries: [
|
||||
{
|
||||
field: ConditionEntryField.HASH,
|
||||
|
@ -322,8 +408,6 @@ describe('When showing the Trusted App Create Form', () => {
|
|||
value: 'e50fb1a0e5fff590ece385082edc6c41',
|
||||
},
|
||||
],
|
||||
name: 'Some Process',
|
||||
os: OperatingSystem.WINDOWS,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
EuiFieldText,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiHorizontalRule,
|
||||
EuiSuperSelect,
|
||||
EuiSuperSelectOption,
|
||||
EuiTextArea,
|
||||
|
@ -18,19 +19,29 @@ import { i18n } from '@kbn/i18n';
|
|||
import { EuiFormProps } from '@elastic/eui/src/components/form/form';
|
||||
import {
|
||||
ConditionEntryField,
|
||||
EffectScope,
|
||||
MacosLinuxConditionEntry,
|
||||
MaybeImmutable,
|
||||
NewTrustedApp,
|
||||
OperatingSystem,
|
||||
} from '../../../../../../common/endpoint/types';
|
||||
import { isValidHash } from '../../../../../../common/endpoint/validation/trusted_apps';
|
||||
import { isValidHash } from '../../../../../../common/endpoint/service/trusted_apps/validations';
|
||||
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||
import {
|
||||
isGlobalEffectScope,
|
||||
isMacosLinuxTrustedAppCondition,
|
||||
isPolicyEffectScope,
|
||||
isWindowsTrustedAppCondition,
|
||||
} from '../../state/type_guards';
|
||||
import { defaultConditionEntry, defaultNewTrustedApp } from '../../store/builders';
|
||||
import { defaultConditionEntry } from '../../store/builders';
|
||||
import { OS_TITLES } from '../translations';
|
||||
import { LogicalConditionBuilder, LogicalConditionBuilderProps } from './logical_condition';
|
||||
import {
|
||||
EffectedPolicySelect,
|
||||
EffectedPolicySelection,
|
||||
EffectedPolicySelectProps,
|
||||
} from './effected_policy_select';
|
||||
|
||||
const OPERATING_SYSTEMS: readonly OperatingSystem[] = [
|
||||
OperatingSystem.MAC,
|
||||
|
@ -73,7 +84,7 @@ const addResultToValidation = (
|
|||
validation.result[field]!.isInvalid = true;
|
||||
};
|
||||
|
||||
const validateFormValues = (values: NewTrustedApp): ValidationResult => {
|
||||
const validateFormValues = (values: MaybeImmutable<NewTrustedApp>): ValidationResult => {
|
||||
let isValid: ValidationResult['isValid'] = true;
|
||||
const validation: ValidationResult = {
|
||||
isValid,
|
||||
|
@ -159,23 +170,38 @@ export type CreateTrustedAppFormProps = Pick<
|
|||
EuiFormProps,
|
||||
'className' | 'data-test-subj' | 'isInvalid' | 'error' | 'invalidCallout'
|
||||
> & {
|
||||
/** The trusted app values that will be passed to the form */
|
||||
trustedApp: MaybeImmutable<NewTrustedApp>;
|
||||
onChange: (state: TrustedAppFormState) => void;
|
||||
/** Setting passed on to the EffectedPolicySelect component */
|
||||
policies: Pick<EffectedPolicySelectProps, 'options' | 'isLoading'>;
|
||||
/** if form should be shown full width of parent container */
|
||||
fullWidth?: boolean;
|
||||
onChange: (state: TrustedAppFormState) => void;
|
||||
};
|
||||
export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
|
||||
({ fullWidth, onChange, ...formProps }) => {
|
||||
({ fullWidth, onChange, trustedApp: _trustedApp, policies = { options: [] }, ...formProps }) => {
|
||||
const trustedApp = _trustedApp as NewTrustedApp;
|
||||
|
||||
const dataTestSubj = formProps['data-test-subj'];
|
||||
|
||||
const isTrustedAppsByPolicyEnabled = useIsExperimentalFeatureEnabled(
|
||||
'trustedAppsByPolicyEnabled'
|
||||
);
|
||||
|
||||
const osOptions: Array<EuiSuperSelectOption<OperatingSystem>> = useMemo(
|
||||
() => OPERATING_SYSTEMS.map((os) => ({ value: os, inputDisplay: OS_TITLES[os] })),
|
||||
[]
|
||||
);
|
||||
|
||||
const [formValues, setFormValues] = useState<NewTrustedApp>(defaultNewTrustedApp());
|
||||
// We create local state for the list of policies because we want the selected policies to
|
||||
// persist while the user is on the form and possibly toggling between global/non-global
|
||||
const [selectedPolicies, setSelectedPolicies] = useState<EffectedPolicySelection>({
|
||||
isGlobal: isGlobalEffectScope(trustedApp.effectScope),
|
||||
selected: [],
|
||||
});
|
||||
|
||||
const [validationResult, setValidationResult] = useState<ValidationResult>(() =>
|
||||
validateFormValues(formValues)
|
||||
validateFormValues(trustedApp)
|
||||
);
|
||||
|
||||
const [wasVisited, setWasVisited] = useState<
|
||||
|
@ -195,42 +221,52 @@ export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
|
|||
[dataTestSubj]
|
||||
);
|
||||
|
||||
const notifyOfChange = useCallback(
|
||||
(updatedFormValues: TrustedAppFormState['item']) => {
|
||||
const updatedValidationResult = validateFormValues(updatedFormValues);
|
||||
|
||||
setValidationResult(updatedValidationResult);
|
||||
|
||||
onChange({
|
||||
item: updatedFormValues,
|
||||
isValid: updatedValidationResult.isValid,
|
||||
});
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleAndClick = useCallback(() => {
|
||||
setFormValues(
|
||||
(prevState): NewTrustedApp => {
|
||||
if (prevState.os === OperatingSystem.WINDOWS) {
|
||||
return {
|
||||
...prevState,
|
||||
entries: [...prevState.entries, defaultConditionEntry()].filter(
|
||||
isWindowsTrustedAppCondition
|
||||
),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...prevState,
|
||||
entries: [
|
||||
...prevState.entries.filter(isMacosLinuxTrustedAppCondition),
|
||||
defaultConditionEntry(),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}, [setFormValues]);
|
||||
if (trustedApp.os === OperatingSystem.WINDOWS) {
|
||||
notifyOfChange({
|
||||
...trustedApp,
|
||||
entries: [...trustedApp.entries, defaultConditionEntry()].filter(
|
||||
isWindowsTrustedAppCondition
|
||||
),
|
||||
});
|
||||
} else {
|
||||
notifyOfChange({
|
||||
...trustedApp,
|
||||
entries: [
|
||||
...trustedApp.entries.filter(isMacosLinuxTrustedAppCondition),
|
||||
defaultConditionEntry(),
|
||||
],
|
||||
});
|
||||
}
|
||||
}, [notifyOfChange, trustedApp]);
|
||||
|
||||
const handleDomChangeEvents = useCallback<
|
||||
ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>
|
||||
>(({ target: { name, value } }) => {
|
||||
setFormValues(
|
||||
(prevState): NewTrustedApp => {
|
||||
return {
|
||||
...prevState,
|
||||
[name]: value,
|
||||
};
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
>(
|
||||
({ target: { name, value } }) => {
|
||||
notifyOfChange({
|
||||
...trustedApp,
|
||||
[name]: value,
|
||||
});
|
||||
},
|
||||
[notifyOfChange, trustedApp]
|
||||
);
|
||||
|
||||
// Handles keeping track if an input form field has been visited
|
||||
const handleDomBlurEvents = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||
({ target: { name } }) => {
|
||||
setWasVisited((prevState) => {
|
||||
|
@ -243,77 +279,73 @@ export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
|
|||
[]
|
||||
);
|
||||
|
||||
const handleOsChange = useCallback<(v: OperatingSystem) => void>((newOsValue) => {
|
||||
setFormValues(
|
||||
(prevState): NewTrustedApp => {
|
||||
const updatedState: NewTrustedApp = {
|
||||
...prevState,
|
||||
entries: [],
|
||||
os: newOsValue,
|
||||
};
|
||||
if (updatedState.os !== OperatingSystem.WINDOWS) {
|
||||
updatedState.entries.push(
|
||||
...(prevState.entries.filter((entry) =>
|
||||
isMacosLinuxTrustedAppCondition(entry)
|
||||
) as MacosLinuxConditionEntry[])
|
||||
);
|
||||
if (updatedState.entries.length === 0) {
|
||||
updatedState.entries.push(defaultConditionEntry());
|
||||
}
|
||||
} else {
|
||||
updatedState.entries.push(...prevState.entries);
|
||||
}
|
||||
return updatedState;
|
||||
}
|
||||
);
|
||||
setWasVisited((prevState) => {
|
||||
return {
|
||||
...prevState,
|
||||
os: true,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleEntryRemove = useCallback((entry: NewTrustedApp['entries'][0]) => {
|
||||
setFormValues(
|
||||
(prevState): NewTrustedApp => {
|
||||
const handleOsChange = useCallback<(v: OperatingSystem) => void>(
|
||||
(newOsValue) => {
|
||||
setWasVisited((prevState) => {
|
||||
return {
|
||||
...prevState,
|
||||
entries: prevState.entries.filter((item) => item !== entry),
|
||||
} as NewTrustedApp;
|
||||
os: true,
|
||||
};
|
||||
});
|
||||
|
||||
const updatedState: NewTrustedApp = {
|
||||
...trustedApp,
|
||||
entries: [],
|
||||
os: newOsValue,
|
||||
};
|
||||
if (updatedState.os !== OperatingSystem.WINDOWS) {
|
||||
updatedState.entries.push(
|
||||
...(trustedApp.entries.filter((entry) =>
|
||||
isMacosLinuxTrustedAppCondition(entry)
|
||||
) as MacosLinuxConditionEntry[])
|
||||
);
|
||||
if (updatedState.entries.length === 0) {
|
||||
updatedState.entries.push(defaultConditionEntry());
|
||||
}
|
||||
} else {
|
||||
updatedState.entries.push(...trustedApp.entries);
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
notifyOfChange(updatedState);
|
||||
},
|
||||
[notifyOfChange, trustedApp]
|
||||
);
|
||||
|
||||
const handleEntryRemove = useCallback(
|
||||
(entry: NewTrustedApp['entries'][0]) => {
|
||||
notifyOfChange({
|
||||
...trustedApp,
|
||||
entries: trustedApp.entries.filter((item) => item !== entry),
|
||||
} as NewTrustedApp);
|
||||
},
|
||||
[notifyOfChange, trustedApp]
|
||||
);
|
||||
|
||||
const handleEntryChange = useCallback<LogicalConditionBuilderProps['onEntryChange']>(
|
||||
(newEntry, oldEntry) => {
|
||||
setFormValues(
|
||||
(prevState): NewTrustedApp => {
|
||||
if (prevState.os === OperatingSystem.WINDOWS) {
|
||||
return {
|
||||
...prevState,
|
||||
entries: prevState.entries.map((item) => {
|
||||
if (item === oldEntry) {
|
||||
return newEntry;
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
} as NewTrustedApp;
|
||||
} else {
|
||||
return {
|
||||
...prevState,
|
||||
entries: prevState.entries.map((item) => {
|
||||
if (item === oldEntry) {
|
||||
return newEntry;
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
} as NewTrustedApp;
|
||||
}
|
||||
}
|
||||
);
|
||||
if (trustedApp.os === OperatingSystem.WINDOWS) {
|
||||
notifyOfChange({
|
||||
...trustedApp,
|
||||
entries: trustedApp.entries.map((item) => {
|
||||
if (item === oldEntry) {
|
||||
return newEntry;
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
} as NewTrustedApp);
|
||||
} else {
|
||||
notifyOfChange({
|
||||
...trustedApp,
|
||||
entries: trustedApp.entries.map((item) => {
|
||||
if (item === oldEntry) {
|
||||
return newEntry;
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
} as NewTrustedApp);
|
||||
}
|
||||
},
|
||||
[]
|
||||
[notifyOfChange, trustedApp]
|
||||
);
|
||||
|
||||
const handleConditionBuilderOnVisited: LogicalConditionBuilderProps['onVisited'] = useCallback(() => {
|
||||
|
@ -325,18 +357,77 @@ export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
|
|||
});
|
||||
}, []);
|
||||
|
||||
const handlePolicySelectChange: EffectedPolicySelectProps['onChange'] = useCallback(
|
||||
(selection) => {
|
||||
setSelectedPolicies(() => selection);
|
||||
|
||||
let newEffectedScope: EffectScope;
|
||||
|
||||
if (selection.isGlobal) {
|
||||
newEffectedScope = {
|
||||
type: 'global',
|
||||
};
|
||||
} else {
|
||||
newEffectedScope = {
|
||||
type: 'policy',
|
||||
policies: selection.selected.map((policy) => policy.id),
|
||||
};
|
||||
}
|
||||
|
||||
notifyOfChange({
|
||||
...trustedApp,
|
||||
effectScope: newEffectedScope,
|
||||
});
|
||||
},
|
||||
[notifyOfChange, trustedApp]
|
||||
);
|
||||
|
||||
// Anytime the form values change, re-validate
|
||||
useEffect(() => {
|
||||
setValidationResult(validateFormValues(formValues));
|
||||
}, [formValues]);
|
||||
setValidationResult((prevState) => {
|
||||
const newResults = validateFormValues(trustedApp);
|
||||
|
||||
// Anytime the form values change - validate and notify
|
||||
useEffect(() => {
|
||||
onChange({
|
||||
isValid: validationResult.isValid,
|
||||
item: formValues,
|
||||
// Only notify if the overall validation result is different
|
||||
if (newResults.isValid !== prevState.isValid) {
|
||||
notifyOfChange(trustedApp);
|
||||
}
|
||||
|
||||
return newResults;
|
||||
});
|
||||
}, [formValues, onChange, validationResult.isValid]);
|
||||
}, [notifyOfChange, trustedApp]);
|
||||
|
||||
// Anytime the TrustedApp has an effective scope of `policies`, then ensure that
|
||||
// those polices are selected in the UI while at teh same time preserving prior
|
||||
// selections (UX requirement)
|
||||
useEffect(() => {
|
||||
setSelectedPolicies((currentSelection) => {
|
||||
if (isPolicyEffectScope(trustedApp.effectScope) && policies.options.length > 0) {
|
||||
const missingSelectedPolicies: EffectedPolicySelectProps['selected'] = [];
|
||||
|
||||
for (const policyId of trustedApp.effectScope.policies) {
|
||||
if (
|
||||
!currentSelection.selected.find(
|
||||
(currentlySelectedPolicyItem) => currentlySelectedPolicyItem.id === policyId
|
||||
)
|
||||
) {
|
||||
const newSelectedPolicy = policies.options.find((policy) => policy.id === policyId);
|
||||
if (newSelectedPolicy) {
|
||||
missingSelectedPolicies.push(newSelectedPolicy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (missingSelectedPolicies.length) {
|
||||
return {
|
||||
...currentSelection,
|
||||
selected: [...currentSelection.selected, ...missingSelectedPolicies],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return currentSelection;
|
||||
});
|
||||
}, [policies.options, trustedApp.effectScope]);
|
||||
|
||||
return (
|
||||
<EuiForm {...formProps} component="div">
|
||||
|
@ -351,7 +442,7 @@ export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
|
|||
>
|
||||
<EuiFieldText
|
||||
name="name"
|
||||
value={formValues.name}
|
||||
value={trustedApp.name}
|
||||
onChange={handleDomChangeEvents}
|
||||
onBlur={handleDomBlurEvents}
|
||||
fullWidth
|
||||
|
@ -372,7 +463,7 @@ export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
|
|||
<EuiSuperSelect
|
||||
name="os"
|
||||
options={osOptions}
|
||||
valueOfSelected={formValues.os}
|
||||
valueOfSelected={trustedApp.os}
|
||||
onChange={handleOsChange}
|
||||
fullWidth
|
||||
data-test-subj={getTestId('osSelectField')}
|
||||
|
@ -385,8 +476,8 @@ export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
|
|||
error={validationResult.result.entries?.errors}
|
||||
>
|
||||
<LogicalConditionBuilder
|
||||
entries={formValues.entries}
|
||||
os={formValues.os}
|
||||
entries={trustedApp.entries}
|
||||
os={trustedApp.os}
|
||||
onAndClicked={handleAndClick}
|
||||
onEntryRemove={handleEntryRemove}
|
||||
onEntryChange={handleEntryChange}
|
||||
|
@ -403,13 +494,30 @@ export const CreateTrustedAppForm = memo<CreateTrustedAppFormProps>(
|
|||
>
|
||||
<EuiTextArea
|
||||
name="description"
|
||||
value={formValues.description}
|
||||
value={trustedApp.description}
|
||||
onChange={handleDomChangeEvents}
|
||||
fullWidth
|
||||
compressed={isTrustedAppsByPolicyEnabled ? true : false}
|
||||
maxLength={256}
|
||||
data-test-subj={getTestId('descriptionField')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{isTrustedAppsByPolicyEnabled ? (
|
||||
<>
|
||||
<EuiHorizontalRule />
|
||||
<EuiFormRow fullWidth={fullWidth} data-test-subj={getTestId('policySelection')}>
|
||||
<EffectedPolicySelect
|
||||
isGlobal={isGlobalEffectScope(trustedApp.effectScope)}
|
||||
selected={selectedPolicies.selected}
|
||||
options={policies.options}
|
||||
onChange={handlePolicySelectChange}
|
||||
isLoading={policies?.isLoading}
|
||||
data-test-subj={getTestId('effectedPolicies')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
) : null}
|
||||
</EuiForm>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* 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 { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data';
|
||||
import { EffectedPolicySelect, EffectedPolicySelectProps } from './effected_policy_select';
|
||||
import {
|
||||
AppContextTestRender,
|
||||
createAppRootMockRenderer,
|
||||
} from '../../../../../../common/mock/endpoint';
|
||||
import React from 'react';
|
||||
import { forceHTMLElementOffsetWidth } from './test_utils';
|
||||
import { fireEvent, act } from '@testing-library/react';
|
||||
|
||||
describe('when using EffectedPolicySelect component', () => {
|
||||
const generator = new EndpointDocGenerator('effected-policy-select');
|
||||
|
||||
let mockedContext: AppContextTestRender;
|
||||
let componentProps: EffectedPolicySelectProps;
|
||||
let renderResult: ReturnType<AppContextTestRender['render']>;
|
||||
|
||||
const handleOnChange: jest.MockedFunction<EffectedPolicySelectProps['onChange']> = jest.fn();
|
||||
const render = (props: Partial<EffectedPolicySelectProps> = {}) => {
|
||||
componentProps = {
|
||||
...componentProps,
|
||||
...props,
|
||||
};
|
||||
renderResult = mockedContext.render(<EffectedPolicySelect {...componentProps} />);
|
||||
return renderResult;
|
||||
};
|
||||
let resetHTMLElementOffsetWidth: () => void;
|
||||
|
||||
beforeAll(() => {
|
||||
resetHTMLElementOffsetWidth = forceHTMLElementOffsetWidth();
|
||||
});
|
||||
|
||||
afterAll(() => resetHTMLElementOffsetWidth());
|
||||
|
||||
beforeEach(() => {
|
||||
// Default props
|
||||
componentProps = {
|
||||
options: [],
|
||||
isGlobal: true,
|
||||
onChange: handleOnChange,
|
||||
'data-test-subj': 'test',
|
||||
};
|
||||
mockedContext = createAppRootMockRenderer();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
handleOnChange.mockClear();
|
||||
});
|
||||
|
||||
describe('and no policy entries exist', () => {
|
||||
it('should display no options available message', () => {
|
||||
const { getByTestId } = render();
|
||||
expect(getByTestId('test-policiesSelectable').textContent).toEqual('No options available');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and policy entries exist', () => {
|
||||
const policyId = 'abc123';
|
||||
const policyTestSubj = `policy-${policyId}`;
|
||||
|
||||
const toggleGlobalSwitch = () => {
|
||||
act(() => {
|
||||
fireEvent.click(renderResult.getByTestId('test-globalSwitch'));
|
||||
});
|
||||
};
|
||||
|
||||
const clickOnPolicy = () => {
|
||||
act(() => {
|
||||
fireEvent.click(renderResult.getByTestId(policyTestSubj));
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const policy = generator.generatePolicyPackagePolicy();
|
||||
policy.name = 'test policy A';
|
||||
policy.id = policyId;
|
||||
|
||||
componentProps = {
|
||||
...componentProps,
|
||||
options: [policy],
|
||||
};
|
||||
|
||||
handleOnChange.mockImplementation((selection) => {
|
||||
componentProps = {
|
||||
...componentProps,
|
||||
...selection,
|
||||
};
|
||||
renderResult.rerender(<EffectedPolicySelect {...componentProps} />);
|
||||
});
|
||||
});
|
||||
|
||||
it('should display policies', () => {
|
||||
const { getByTestId } = render();
|
||||
expect(getByTestId(policyTestSubj));
|
||||
});
|
||||
|
||||
it('should disable policy items if global is checked', () => {
|
||||
const { getByTestId } = render();
|
||||
expect(getByTestId(policyTestSubj).getAttribute('aria-disabled')).toEqual('true');
|
||||
});
|
||||
|
||||
it('should enable policy items if global is unchecked', async () => {
|
||||
const { getByTestId } = render();
|
||||
toggleGlobalSwitch();
|
||||
expect(getByTestId(policyTestSubj).getAttribute('aria-disabled')).toEqual('false');
|
||||
});
|
||||
|
||||
it('should call onChange with selection when global is toggled', () => {
|
||||
render();
|
||||
|
||||
toggleGlobalSwitch();
|
||||
expect(handleOnChange.mock.calls[0][0]).toEqual({
|
||||
isGlobal: false,
|
||||
selected: [],
|
||||
});
|
||||
|
||||
toggleGlobalSwitch();
|
||||
expect(handleOnChange.mock.calls[1][0]).toEqual({
|
||||
isGlobal: true,
|
||||
selected: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow clicking on policies when global is true', () => {
|
||||
render();
|
||||
|
||||
clickOnPolicy();
|
||||
expect(handleOnChange.mock.calls.length).toBe(0);
|
||||
|
||||
// Select a Policy, then switch back to global and try to click the policy again (should be disabled and trigger onChange())
|
||||
toggleGlobalSwitch();
|
||||
clickOnPolicy();
|
||||
toggleGlobalSwitch();
|
||||
clickOnPolicy();
|
||||
expect(handleOnChange.mock.calls.length).toBe(3);
|
||||
expect(handleOnChange.mock.calls[2][0]).toEqual({
|
||||
isGlobal: true,
|
||||
selected: [componentProps.options[0]],
|
||||
});
|
||||
});
|
||||
|
||||
it('should maintain policies selection even if global was checked', () => {
|
||||
render();
|
||||
|
||||
toggleGlobalSwitch();
|
||||
clickOnPolicy();
|
||||
expect(handleOnChange.mock.calls[1][0]).toEqual({
|
||||
isGlobal: false,
|
||||
selected: [componentProps.options[0]],
|
||||
});
|
||||
|
||||
// Toggle isGlobal back to True
|
||||
toggleGlobalSwitch();
|
||||
expect(handleOnChange.mock.calls[2][0]).toEqual({
|
||||
isGlobal: true,
|
||||
selected: [componentProps.options[0]],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* 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 React, { memo, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
EuiCheckbox,
|
||||
EuiFormRow,
|
||||
EuiSelectable,
|
||||
EuiSelectableProps,
|
||||
EuiSwitch,
|
||||
EuiSwitchProps,
|
||||
EuiText,
|
||||
htmlIdGenerator,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSelectableOption } from '@elastic/eui/src/components/selectable/selectable_option';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import styled from 'styled-components';
|
||||
import { PolicyData } from '../../../../../../../common/endpoint/types';
|
||||
import { MANAGEMENT_APP_ID } from '../../../../../common/constants';
|
||||
import { getPolicyDetailPath } from '../../../../../common/routing';
|
||||
import { useFormatUrl } from '../../../../../../common/components/link_to';
|
||||
import { SecurityPageName } from '../../../../../../../common/constants';
|
||||
import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app';
|
||||
|
||||
const NOOP = () => {};
|
||||
const DEFAULT_LIST_PROPS: EuiSelectableProps['listProps'] = { bordered: true, showIcons: false };
|
||||
|
||||
const EffectivePolicyFormContainer = styled.div`
|
||||
.policy-name .euiSelectableListItem__text {
|
||||
text-decoration: none !important;
|
||||
color: ${(props) => props.theme.eui.euiTextColor} !important;
|
||||
}
|
||||
`;
|
||||
|
||||
interface OptionPolicyData {
|
||||
policy: PolicyData;
|
||||
}
|
||||
|
||||
type EffectedPolicyOption = EuiSelectableOption<OptionPolicyData>;
|
||||
|
||||
export interface EffectedPolicySelection {
|
||||
isGlobal: boolean;
|
||||
selected: PolicyData[];
|
||||
}
|
||||
|
||||
export type EffectedPolicySelectProps = Omit<
|
||||
EuiSelectableProps<OptionPolicyData>,
|
||||
'onChange' | 'options' | 'children' | 'searchable'
|
||||
> & {
|
||||
options: PolicyData[];
|
||||
isGlobal: boolean;
|
||||
onChange: (selection: EffectedPolicySelection) => void;
|
||||
selected?: PolicyData[];
|
||||
};
|
||||
export const EffectedPolicySelect = memo<EffectedPolicySelectProps>(
|
||||
({
|
||||
isGlobal,
|
||||
onChange,
|
||||
listProps,
|
||||
options,
|
||||
selected = [],
|
||||
'data-test-subj': dataTestSubj,
|
||||
...otherSelectableProps
|
||||
}) => {
|
||||
const { formatUrl } = useFormatUrl(SecurityPageName.administration);
|
||||
|
||||
const getTestId = useCallback(
|
||||
(suffix): string | undefined => {
|
||||
if (dataTestSubj) {
|
||||
return `${dataTestSubj}-${suffix}`;
|
||||
}
|
||||
},
|
||||
[dataTestSubj]
|
||||
);
|
||||
|
||||
const selectableOptions: EffectedPolicyOption[] = useMemo(() => {
|
||||
const isPolicySelected = new Set<string>(selected.map((policy) => policy.id));
|
||||
|
||||
return options
|
||||
.map<EffectedPolicyOption>((policy) => ({
|
||||
label: policy.name,
|
||||
className: 'policy-name',
|
||||
prepend: (
|
||||
<EuiCheckbox
|
||||
id={htmlIdGenerator()()}
|
||||
onChange={NOOP}
|
||||
checked={isPolicySelected.has(policy.id)}
|
||||
disabled={isGlobal}
|
||||
/>
|
||||
),
|
||||
append: (
|
||||
<LinkToApp
|
||||
href={formatUrl(getPolicyDetailPath(policy.id))}
|
||||
appId={MANAGEMENT_APP_ID}
|
||||
appPath={getPolicyDetailPath(policy.id)}
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.effectedPolicySelect.viewPolicyLinkLabel"
|
||||
defaultMessage="View policy"
|
||||
/>
|
||||
</LinkToApp>
|
||||
),
|
||||
policy,
|
||||
checked: isPolicySelected.has(policy.id) ? 'on' : undefined,
|
||||
disabled: isGlobal,
|
||||
'data-test-subj': `policy-${policy.id}`,
|
||||
}))
|
||||
.sort(({ label: labelA }, { label: labelB }) => labelA.localeCompare(labelB));
|
||||
}, [formatUrl, isGlobal, options, selected]);
|
||||
|
||||
const handleOnPolicySelectChange = useCallback<
|
||||
Required<EuiSelectableProps<OptionPolicyData>>['onChange']
|
||||
>(
|
||||
(currentOptions) => {
|
||||
onChange({
|
||||
isGlobal,
|
||||
selected: currentOptions.filter((opt) => opt.checked).map((opt) => opt.policy),
|
||||
});
|
||||
},
|
||||
[isGlobal, onChange]
|
||||
)!;
|
||||
|
||||
const handleGlobalSwitchChange: EuiSwitchProps['onChange'] = useCallback(
|
||||
({ target: { checked } }) => {
|
||||
onChange({
|
||||
isGlobal: checked,
|
||||
selected,
|
||||
});
|
||||
},
|
||||
[onChange, selected]
|
||||
);
|
||||
|
||||
const listBuilderCallback: EuiSelectableProps['children'] = useCallback((list, search) => {
|
||||
return (
|
||||
<>
|
||||
{search}
|
||||
{list}
|
||||
</>
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EffectivePolicyFormContainer>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<EuiText size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.trustedapps.policySelect.globalSectionTitle"
|
||||
defaultMessage="Assignment"
|
||||
/>
|
||||
</h3>
|
||||
</EuiText>
|
||||
}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={i18n.translate(
|
||||
'xpack.securitySolution.trustedapps.policySelect.globalSwitchTitle',
|
||||
{
|
||||
defaultMessage: 'Apply trusted application globally',
|
||||
}
|
||||
)}
|
||||
checked={isGlobal}
|
||||
onChange={handleGlobalSwitchChange}
|
||||
data-test-subj={getTestId('globalSwitch')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.securitySolution.policySelect.policySpecificSectionTitle', {
|
||||
defaultMessage: 'Apply to specific endpoint policies',
|
||||
})}
|
||||
>
|
||||
<EuiSelectable<OptionPolicyData>
|
||||
{...otherSelectableProps}
|
||||
options={selectableOptions}
|
||||
listProps={listProps || DEFAULT_LIST_PROPS}
|
||||
onChange={handleOnPolicySelectChange}
|
||||
searchable={true}
|
||||
data-test-subj={getTestId('policiesSelectable')}
|
||||
>
|
||||
{listBuilderCallback}
|
||||
</EuiSelectable>
|
||||
</EuiFormRow>
|
||||
</EffectivePolicyFormContainer>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
EffectedPolicySelect.displayName = 'EffectedPolicySelect';
|
|
@ -5,8 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface TrustedAppsUrlParams {
|
||||
page_index: number;
|
||||
page_size: number;
|
||||
show?: 'create';
|
||||
}
|
||||
export * from './effected_policy_select';
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Forces the `offsetWidth` of `HTMLElement` to a given value. Needed due to the use of
|
||||
* `react-virtualized-auto-sizer` by the eui `Selectable` component
|
||||
*
|
||||
* @param [width=100]
|
||||
* @returns reset(): void
|
||||
*
|
||||
* @example
|
||||
* const resetEnv = forceHTMLElementOffsetWidth();
|
||||
* //... later
|
||||
* resetEnv();
|
||||
*/
|
||||
export const forceHTMLElementOffsetWidth = (width: number = 100): (() => void) => {
|
||||
const currentOffsetDefinition = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLElement.prototype,
|
||||
'offsetWidth'
|
||||
);
|
||||
|
||||
Object.defineProperties(window.HTMLElement.prototype, {
|
||||
offsetWidth: {
|
||||
...(currentOffsetDefinition || {}),
|
||||
get() {
|
||||
return width;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return function reset() {
|
||||
if (currentOffsetDefinition) {
|
||||
Object.defineProperties(window.HTMLElement.prototype, {
|
||||
offsetWidth: {
|
||||
...(currentOffsetDefinition || {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
|
@ -84,6 +84,15 @@ exports[`trusted_app_card TrustedAppCard should render correctly 1`] = `
|
|||
items={Array []}
|
||||
responsive={true}
|
||||
/>
|
||||
<ItemDetailsAction
|
||||
color="primary"
|
||||
data-test-subj="trustedAppEditButton"
|
||||
fill={true}
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
>
|
||||
Edit
|
||||
</ItemDetailsAction>
|
||||
<ItemDetailsAction
|
||||
color="danger"
|
||||
data-test-subj="trustedAppDeleteButton"
|
||||
|
@ -179,6 +188,15 @@ exports[`trusted_app_card TrustedAppCard should trim long texts 1`] = `
|
|||
items={Array []}
|
||||
responsive={true}
|
||||
/>
|
||||
<ItemDetailsAction
|
||||
color="primary"
|
||||
data-test-subj="trustedAppEditButton"
|
||||
fill={true}
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
>
|
||||
Edit
|
||||
</ItemDetailsAction>
|
||||
<ItemDetailsAction
|
||||
color="danger"
|
||||
data-test-subj="trustedAppDeleteButton"
|
||||
|
|
|
@ -50,19 +50,37 @@ storiesOf('TrustedApps/TrustedAppCard', module)
|
|||
trustedApp.created_at = '2020-09-17T14:52:33.899Z';
|
||||
trustedApp.entries = [PATH_CONDITION];
|
||||
|
||||
return <TrustedAppCard trustedApp={trustedApp} onDelete={action('onClick')} />;
|
||||
return (
|
||||
<TrustedAppCard
|
||||
trustedApp={trustedApp}
|
||||
onDelete={action('onClick')}
|
||||
onEdit={action('onClick')}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('multiple entries', () => {
|
||||
const trustedApp: TrustedApp = createSampleTrustedApp(5);
|
||||
trustedApp.created_at = '2020-09-17T14:52:33.899Z';
|
||||
trustedApp.entries = [PATH_CONDITION, SIGNER_CONDITION];
|
||||
|
||||
return <TrustedAppCard trustedApp={trustedApp} onDelete={action('onClick')} />;
|
||||
return (
|
||||
<TrustedAppCard
|
||||
trustedApp={trustedApp}
|
||||
onDelete={action('onClick')}
|
||||
onEdit={action('onClick')}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('longs texts', () => {
|
||||
const trustedApp: TrustedApp = createSampleTrustedApp(5, true);
|
||||
trustedApp.created_at = '2020-09-17T14:52:33.899Z';
|
||||
trustedApp.entries = [PATH_CONDITION, SIGNER_CONDITION];
|
||||
|
||||
return <TrustedAppCard trustedApp={trustedApp} onDelete={action('onClick')} />;
|
||||
return (
|
||||
<TrustedAppCard
|
||||
trustedApp={trustedApp}
|
||||
onDelete={action('onClick')}
|
||||
onEdit={action('onClick')}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -15,7 +15,11 @@ describe('trusted_app_card', () => {
|
|||
describe('TrustedAppCard', () => {
|
||||
it('should render correctly', () => {
|
||||
const element = shallow(
|
||||
<TrustedAppCard trustedApp={createSampleTrustedApp(4)} onDelete={() => {}} />
|
||||
<TrustedAppCard
|
||||
trustedApp={createSampleTrustedApp(4)}
|
||||
onDelete={() => {}}
|
||||
onEdit={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(element).toMatchSnapshot();
|
||||
|
@ -23,7 +27,11 @@ describe('trusted_app_card', () => {
|
|||
|
||||
it('should trim long texts', () => {
|
||||
const element = shallow(
|
||||
<TrustedAppCard trustedApp={createSampleTrustedApp(4, true)} onDelete={() => {}} />
|
||||
<TrustedAppCard
|
||||
trustedApp={createSampleTrustedApp(4, true)}
|
||||
onDelete={() => {}}
|
||||
onEdit={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(element).toMatchSnapshot();
|
||||
|
|
|
@ -21,6 +21,7 @@ import { TextFieldValue } from '../../../../../../common/components/text_field_v
|
|||
import {
|
||||
ItemDetailsAction,
|
||||
ItemDetailsCard,
|
||||
ItemDetailsCardProps,
|
||||
ItemDetailsPropertySummary,
|
||||
} from '../../../../../../common/components/item_details_card';
|
||||
|
||||
|
@ -31,6 +32,7 @@ import {
|
|||
CARD_DELETE_BUTTON_LABEL,
|
||||
CONDITION_FIELD_TITLE,
|
||||
OPERATOR_TITLE,
|
||||
CARD_EDIT_BUTTON_LABEL,
|
||||
} from '../../translations';
|
||||
|
||||
type Entry = MacosLinuxConditionEntry | WindowsConditionEntry;
|
||||
|
@ -75,74 +77,88 @@ const getEntriesColumnDefinitions = (): Array<EuiTableFieldDataColumnType<Entry>
|
|||
},
|
||||
];
|
||||
|
||||
interface TrustedAppCardProps {
|
||||
export type TrustedAppCardProps = Pick<ItemDetailsCardProps, 'data-test-subj'> & {
|
||||
trustedApp: Immutable<TrustedApp>;
|
||||
onDelete: (trustedApp: Immutable<TrustedApp>) => void;
|
||||
}
|
||||
onEdit: (trustedApp: Immutable<TrustedApp>) => void;
|
||||
};
|
||||
|
||||
export const TrustedAppCard = memo(({ trustedApp, onDelete }: TrustedAppCardProps) => {
|
||||
const handleDelete = useCallback(() => onDelete(trustedApp), [onDelete, trustedApp]);
|
||||
export const TrustedAppCard = memo<TrustedAppCardProps>(
|
||||
({ trustedApp, onDelete, onEdit, ...otherProps }) => {
|
||||
const handleDelete = useCallback(() => onDelete(trustedApp), [onDelete, trustedApp]);
|
||||
const handleEdit = useCallback(() => onEdit(trustedApp), [onEdit, trustedApp]);
|
||||
|
||||
return (
|
||||
<ItemDetailsCard>
|
||||
<ItemDetailsPropertySummary
|
||||
name={PROPERTY_TITLES.name}
|
||||
value={
|
||||
<TextFieldValue
|
||||
fieldName={PROPERTY_TITLES.name}
|
||||
value={trustedApp.name}
|
||||
maxLength={100}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ItemDetailsPropertySummary
|
||||
name={PROPERTY_TITLES.os}
|
||||
value={<TextFieldValue fieldName={PROPERTY_TITLES.os} value={OS_TITLES[trustedApp.os]} />}
|
||||
/>
|
||||
<ItemDetailsPropertySummary
|
||||
name={PROPERTY_TITLES.created_at}
|
||||
value={
|
||||
<FormattedDate
|
||||
fieldName={PROPERTY_TITLES.created_at}
|
||||
value={trustedApp.created_at}
|
||||
className="eui-textTruncate"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ItemDetailsPropertySummary
|
||||
name={PROPERTY_TITLES.created_by}
|
||||
value={
|
||||
<TextFieldValue fieldName={PROPERTY_TITLES.created_by} value={trustedApp.created_by} />
|
||||
}
|
||||
/>
|
||||
<ItemDetailsPropertySummary
|
||||
name={PROPERTY_TITLES.description}
|
||||
value={
|
||||
<TextFieldValue
|
||||
fieldName={PROPERTY_TITLES.description || ''}
|
||||
value={trustedApp.description || ''}
|
||||
maxLength={100}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
return (
|
||||
<ItemDetailsCard {...otherProps}>
|
||||
<ItemDetailsPropertySummary
|
||||
name={PROPERTY_TITLES.name}
|
||||
value={
|
||||
<TextFieldValue
|
||||
fieldName={PROPERTY_TITLES.name}
|
||||
value={trustedApp.name}
|
||||
maxLength={100}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ItemDetailsPropertySummary
|
||||
name={PROPERTY_TITLES.os}
|
||||
value={<TextFieldValue fieldName={PROPERTY_TITLES.os} value={OS_TITLES[trustedApp.os]} />}
|
||||
/>
|
||||
<ItemDetailsPropertySummary
|
||||
name={PROPERTY_TITLES.created_at}
|
||||
value={
|
||||
<FormattedDate
|
||||
fieldName={PROPERTY_TITLES.created_at}
|
||||
value={trustedApp.created_at}
|
||||
className="eui-textTruncate"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ItemDetailsPropertySummary
|
||||
name={PROPERTY_TITLES.created_by}
|
||||
value={
|
||||
<TextFieldValue fieldName={PROPERTY_TITLES.created_by} value={trustedApp.created_by} />
|
||||
}
|
||||
/>
|
||||
<ItemDetailsPropertySummary
|
||||
name={PROPERTY_TITLES.description}
|
||||
value={
|
||||
<TextFieldValue
|
||||
fieldName={PROPERTY_TITLES.description || ''}
|
||||
value={trustedApp.description || ''}
|
||||
maxLength={100}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<ConditionsTable
|
||||
columns={useMemo(() => getEntriesColumnDefinitions(), [])}
|
||||
items={useMemo(() => [...trustedApp.entries], [trustedApp.entries])}
|
||||
badge="and"
|
||||
responsive
|
||||
/>
|
||||
<ConditionsTable
|
||||
columns={useMemo(() => getEntriesColumnDefinitions(), [])}
|
||||
items={useMemo(() => [...trustedApp.entries], [trustedApp.entries])}
|
||||
badge="and"
|
||||
responsive
|
||||
/>
|
||||
|
||||
<ItemDetailsAction
|
||||
size="s"
|
||||
color="danger"
|
||||
onClick={handleDelete}
|
||||
data-test-subj="trustedAppDeleteButton"
|
||||
>
|
||||
{CARD_DELETE_BUTTON_LABEL}
|
||||
</ItemDetailsAction>
|
||||
</ItemDetailsCard>
|
||||
);
|
||||
});
|
||||
<ItemDetailsAction
|
||||
size="s"
|
||||
color="primary"
|
||||
fill
|
||||
onClick={handleEdit}
|
||||
data-test-subj="trustedAppEditButton"
|
||||
>
|
||||
{CARD_EDIT_BUTTON_LABEL}
|
||||
</ItemDetailsAction>
|
||||
|
||||
<ItemDetailsAction
|
||||
size="s"
|
||||
color="danger"
|
||||
onClick={handleDelete}
|
||||
data-test-subj="trustedAppDeleteButton"
|
||||
>
|
||||
{CARD_DELETE_BUTTON_LABEL}
|
||||
</ItemDetailsAction>
|
||||
</ItemDetailsCard>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TrustedAppCard.displayName = 'TrustedAppCard';
|
||||
|
|
|
@ -340,6 +340,27 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -592,6 +613,27 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -844,6 +886,27 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -1096,6 +1159,27 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -1348,6 +1432,27 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -1600,6 +1705,27 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -1852,6 +1978,27 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -2104,6 +2251,27 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -2356,6 +2524,27 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -2608,6 +2797,27 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -3149,6 +3359,27 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -3401,6 +3632,27 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -3653,6 +3905,27 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -3905,6 +4178,27 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -4157,6 +4451,27 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -4409,6 +4724,27 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -4661,6 +4997,27 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -4913,6 +5270,27 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -5165,6 +5543,27 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -5417,6 +5816,27 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -5916,6 +6336,27 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -6168,6 +6609,27 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -6420,6 +6882,27 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -6672,6 +7155,27 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -6924,6 +7428,27 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -7176,6 +7701,27 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -7428,6 +7974,27 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -7680,6 +8247,27 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -7932,6 +8520,27 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -8184,6 +8793,27 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
|
|
@ -16,9 +16,11 @@ import {
|
|||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Pagination } from '../../../state';
|
||||
|
||||
import {
|
||||
getCurrentLocation,
|
||||
getListErrorMessage,
|
||||
getListItems,
|
||||
getListPagination,
|
||||
|
@ -33,7 +35,8 @@ import {
|
|||
|
||||
import { NO_RESULTS_MESSAGE } from '../../translations';
|
||||
|
||||
import { TrustedAppCard } from '../trusted_app_card';
|
||||
import { TrustedAppCard, TrustedAppCardProps } from '../trusted_app_card';
|
||||
import { getTrustedAppsListPath } from '../../../../../common/routing';
|
||||
|
||||
export interface PaginationBarProps {
|
||||
pagination: Pagination;
|
||||
|
@ -75,15 +78,31 @@ const GridMessage: FC = ({ children }) => (
|
|||
);
|
||||
|
||||
export const TrustedAppsGrid = memo(() => {
|
||||
const history = useHistory();
|
||||
const pagination = useTrustedAppsSelector(getListPagination);
|
||||
const listItems = useTrustedAppsSelector(getListItems);
|
||||
const isLoading = useTrustedAppsSelector(isListLoading);
|
||||
const error = useTrustedAppsSelector(getListErrorMessage);
|
||||
const location = useTrustedAppsSelector(getCurrentLocation);
|
||||
|
||||
const handleTrustedAppDelete = useTrustedAppsStoreActionCallback((trustedApp) => ({
|
||||
type: 'trustedAppDeletionDialogStarted',
|
||||
payload: { entry: trustedApp },
|
||||
}));
|
||||
|
||||
const handleTrustedAppEdit: TrustedAppCardProps['onEdit'] = useCallback(
|
||||
(trustedApp) => {
|
||||
history.push(
|
||||
getTrustedAppsListPath({
|
||||
...location,
|
||||
show: 'edit',
|
||||
id: trustedApp.id,
|
||||
})
|
||||
);
|
||||
},
|
||||
[history, location]
|
||||
);
|
||||
|
||||
const handlePaginationChange = useTrustedAppsNavigateCallback(({ index, size }) => ({
|
||||
page_index: index,
|
||||
page_size: size,
|
||||
|
@ -114,7 +133,11 @@ export const TrustedAppsGrid = memo(() => {
|
|||
<EuiFlexGroup direction="column">
|
||||
{listItems.map((item) => (
|
||||
<EuiFlexItem grow={false} key={item.id}>
|
||||
<TrustedAppCard trustedApp={item} onDelete={handleTrustedAppDelete} />
|
||||
<TrustedAppCard
|
||||
trustedApp={item}
|
||||
onDelete={handleTrustedAppDelete}
|
||||
onEdit={handleTrustedAppEdit}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -635,6 +635,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -772,6 +773,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<div
|
||||
class="euiPanel euiPanel--borderRadiusMedium euiPanel--plain euiPanel--shadow"
|
||||
data-test-subj="trustedAppCard"
|
||||
>
|
||||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
|
@ -988,6 +990,27 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
<div
|
||||
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--justifyContentFlexEnd euiFlexGroup--directionRow euiFlexGroup--responsive"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--small euiButton--fill eui-fullWidth "
|
||||
data-test-subj="trustedAppEditButton"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem euiFlexItem--flexGrowZero"
|
||||
>
|
||||
|
@ -1023,6 +1046,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -1152,6 +1176,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -1281,6 +1306,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -1410,6 +1436,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -1539,6 +1566,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -1668,6 +1696,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -1797,6 +1826,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -1926,6 +1956,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -2055,6 +2086,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -2184,6 +2216,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -2313,6 +2346,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -2442,6 +2476,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -2571,6 +2606,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -2700,6 +2736,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -2829,6 +2866,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -2958,6 +2996,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -3087,6 +3126,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -3216,6 +3256,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -3345,6 +3386,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -3834,6 +3876,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -3963,6 +4006,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -4092,6 +4136,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -4221,6 +4266,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -4350,6 +4396,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -4479,6 +4526,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -4608,6 +4656,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -4737,6 +4786,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -4866,6 +4916,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -4995,6 +5046,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -5124,6 +5176,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -5253,6 +5306,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -5382,6 +5436,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -5511,6 +5566,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -5640,6 +5696,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -5769,6 +5826,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -5898,6 +5956,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -6027,6 +6086,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -6156,6 +6216,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -6285,6 +6346,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -6932,6 +6994,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -7061,6 +7124,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -7190,6 +7254,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -7319,6 +7384,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -7448,6 +7514,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -7577,6 +7644,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -7706,6 +7774,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -7835,6 +7904,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -7964,6 +8034,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -8093,6 +8164,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -8222,6 +8294,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -8351,6 +8424,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -8480,6 +8554,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -8609,6 +8684,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -8738,6 +8814,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -8867,6 +8944,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -8996,6 +9074,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -9125,6 +9204,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -9254,6 +9334,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -9383,6 +9464,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -9872,6 +9954,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -10001,6 +10084,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -10130,6 +10214,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -10259,6 +10344,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -10388,6 +10474,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -10517,6 +10604,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -10646,6 +10734,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -10775,6 +10864,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -10904,6 +10994,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -11033,6 +11124,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -11162,6 +11254,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -11291,6 +11384,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -11420,6 +11514,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -11549,6 +11644,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -11678,6 +11774,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -11807,6 +11904,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -11936,6 +12034,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -12065,6 +12164,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -12194,6 +12294,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
@ -12323,6 +12424,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
|
|||
>
|
||||
<td
|
||||
class="euiTableRowCell"
|
||||
data-test-subj="trustedAppNameTableCell"
|
||||
>
|
||||
<div
|
||||
class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop"
|
||||
|
|
|
@ -5,22 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Dispatch } from 'redux';
|
||||
import React, { memo, ReactNode, useMemo, useState } from 'react';
|
||||
import React, { memo, ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiBasicTableColumn,
|
||||
EuiButtonIcon,
|
||||
EuiTableActionsColumnType,
|
||||
RIGHT_ALIGNMENT,
|
||||
} from '@elastic/eui';
|
||||
import { EuiBasicTable, EuiBasicTableColumn, EuiButtonIcon, RIGHT_ALIGNMENT } from '@elastic/eui';
|
||||
|
||||
import { Immutable } from '../../../../../../../common/endpoint/types';
|
||||
import { AppAction } from '../../../../../../common/store/actions';
|
||||
import { TrustedApp } from '../../../../../../../common/endpoint/types/trusted_apps';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Immutable, TrustedApp } from '../../../../../../../common/endpoint/types';
|
||||
|
||||
import {
|
||||
getCurrentLocation,
|
||||
getListErrorMessage,
|
||||
getListItems,
|
||||
getListPagination,
|
||||
|
@ -33,165 +26,190 @@ import { TextFieldValue } from '../../../../../../common/components/text_field_v
|
|||
import { useTrustedAppsNavigateCallback, useTrustedAppsSelector } from '../../hooks';
|
||||
|
||||
import { ACTIONS_COLUMN_TITLE, LIST_ACTIONS, OS_TITLES, PROPERTY_TITLES } from '../../translations';
|
||||
import { TrustedAppCard } from '../trusted_app_card';
|
||||
import { TrustedAppCard, TrustedAppCardProps } from '../trusted_app_card';
|
||||
import { getTrustedAppsListPath } from '../../../../../common/routing';
|
||||
|
||||
interface DetailsMap {
|
||||
[K: string]: ReactNode;
|
||||
}
|
||||
|
||||
interface TrustedAppsListContext {
|
||||
dispatch: Dispatch<Immutable<AppAction>>;
|
||||
detailsMapState: [DetailsMap, (value: DetailsMap) => void];
|
||||
}
|
||||
const ExpandedRowContent = memo<Pick<TrustedAppCardProps, 'trustedApp'>>(({ trustedApp }) => {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
const location = useTrustedAppsSelector(getCurrentLocation);
|
||||
|
||||
type ColumnsList = Array<EuiBasicTableColumn<Immutable<TrustedApp>>>;
|
||||
type ActionsList = EuiTableActionsColumnType<Immutable<TrustedApp>>['actions'];
|
||||
const handleOnDelete = useCallback(() => {
|
||||
dispatch({
|
||||
type: 'trustedAppDeletionDialogStarted',
|
||||
payload: { entry: trustedApp },
|
||||
});
|
||||
}, [dispatch, trustedApp]);
|
||||
|
||||
const toggleItemDetailsInMap = (
|
||||
map: DetailsMap,
|
||||
item: Immutable<TrustedApp>,
|
||||
{ dispatch }: TrustedAppsListContext
|
||||
): DetailsMap => {
|
||||
const changedMap = { ...map };
|
||||
|
||||
if (changedMap[item.id]) {
|
||||
delete changedMap[item.id];
|
||||
} else {
|
||||
changedMap[item.id] = (
|
||||
<TrustedAppCard
|
||||
trustedApp={item}
|
||||
onDelete={() => {
|
||||
dispatch({
|
||||
type: 'trustedAppDeletionDialogStarted',
|
||||
payload: { entry: item },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
const handleOnEdit = useCallback(() => {
|
||||
history.push(
|
||||
getTrustedAppsListPath({
|
||||
...location,
|
||||
show: 'edit',
|
||||
id: trustedApp.id,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [history, location, trustedApp.id]);
|
||||
|
||||
return changedMap;
|
||||
};
|
||||
|
||||
const getActionDefinitions = ({ dispatch }: TrustedAppsListContext): ActionsList => [
|
||||
{
|
||||
name: LIST_ACTIONS.delete.name,
|
||||
description: LIST_ACTIONS.delete.description,
|
||||
'data-test-subj': 'trustedAppDeleteAction',
|
||||
isPrimary: true,
|
||||
icon: 'trash',
|
||||
color: 'danger',
|
||||
type: 'icon',
|
||||
onClick: (item: Immutable<TrustedApp>) => {
|
||||
dispatch({
|
||||
type: 'trustedAppDeletionDialogStarted',
|
||||
payload: { entry: item },
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getColumnDefinitions = (context: TrustedAppsListContext): ColumnsList => {
|
||||
const [itemDetailsMap, setItemDetailsMap] = context.detailsMapState;
|
||||
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
name: PROPERTY_TITLES.name,
|
||||
render(value: TrustedApp['name'], record: Immutable<TrustedApp>) {
|
||||
return (
|
||||
<TextFieldValue
|
||||
fieldName={PROPERTY_TITLES.name}
|
||||
value={value}
|
||||
className="eui-textTruncate"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'os',
|
||||
name: PROPERTY_TITLES.os,
|
||||
render(value: TrustedApp['os'], record: Immutable<TrustedApp>) {
|
||||
return (
|
||||
<TextFieldValue
|
||||
fieldName={PROPERTY_TITLES.os}
|
||||
value={OS_TITLES[value]}
|
||||
className="eui-textTruncate"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'created_at',
|
||||
name: PROPERTY_TITLES.created_at,
|
||||
render(value: TrustedApp['created_at'], record: Immutable<TrustedApp>) {
|
||||
return (
|
||||
<FormattedDate
|
||||
fieldName={PROPERTY_TITLES.created_at}
|
||||
value={value}
|
||||
className="eui-textTruncate"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'created_by',
|
||||
name: PROPERTY_TITLES.created_by,
|
||||
render(value: TrustedApp['created_by'], record: Immutable<TrustedApp>) {
|
||||
return (
|
||||
<TextFieldValue
|
||||
fieldName={PROPERTY_TITLES.created_by}
|
||||
value={value}
|
||||
className="eui-textTruncate"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ACTIONS_COLUMN_TITLE,
|
||||
actions: getActionDefinitions(context),
|
||||
},
|
||||
{
|
||||
align: RIGHT_ALIGNMENT,
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
render(item: Immutable<TrustedApp>) {
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
onClick={() => setItemDetailsMap(toggleItemDetailsInMap(itemDetailsMap, item, context))}
|
||||
aria-label={itemDetailsMap[item.id] ? 'Collapse' : 'Expand'}
|
||||
iconType={itemDetailsMap[item.id] ? 'arrowUp' : 'arrowDown'}
|
||||
data-test-subj="trustedAppsListItemExpandButton"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
return (
|
||||
<TrustedAppCard
|
||||
trustedApp={trustedApp}
|
||||
onEdit={handleOnEdit}
|
||||
onDelete={handleOnDelete}
|
||||
data-test-subj="trustedAppCard"
|
||||
/>
|
||||
);
|
||||
});
|
||||
ExpandedRowContent.displayName = 'ExpandedRowContent';
|
||||
|
||||
export const TrustedAppsList = memo(() => {
|
||||
const [detailsMap, setDetailsMap] = useState<DetailsMap>({});
|
||||
const pagination = useTrustedAppsSelector(getListPagination);
|
||||
const listItems = useTrustedAppsSelector(getListItems);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [showDetailsFor, setShowDetailsFor] = useState<{ [key: string]: boolean }>({});
|
||||
|
||||
// Cast below is needed because EuiBasicTable expects listItems to be mutable
|
||||
const listItems = useTrustedAppsSelector(getListItems) as TrustedApp[];
|
||||
const pagination = useTrustedAppsSelector(getListPagination);
|
||||
const listError = useTrustedAppsSelector(getListErrorMessage);
|
||||
const isLoading = useTrustedAppsSelector(isListLoading);
|
||||
|
||||
const toggleShowDetailsFor = useCallback((trustedAppId) => {
|
||||
setShowDetailsFor((prevState) => {
|
||||
const newState = { ...prevState };
|
||||
if (prevState[trustedAppId]) {
|
||||
delete newState[trustedAppId];
|
||||
} else {
|
||||
newState[trustedAppId] = true;
|
||||
}
|
||||
return newState;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const detailsMap = useMemo<DetailsMap>(() => {
|
||||
return Object.keys(showDetailsFor).reduce<DetailsMap>((expandMap, trustedAppId) => {
|
||||
const trustedApp = listItems.find((ta) => ta.id === trustedAppId);
|
||||
|
||||
if (trustedApp) {
|
||||
expandMap[trustedAppId] = <ExpandedRowContent trustedApp={trustedApp} />;
|
||||
}
|
||||
|
||||
return expandMap;
|
||||
}, {});
|
||||
}, [listItems, showDetailsFor]);
|
||||
|
||||
const handleTableOnChange = useTrustedAppsNavigateCallback(({ page }) => ({
|
||||
page_index: page.index,
|
||||
page_size: page.size,
|
||||
}));
|
||||
|
||||
const tableColumns: Array<EuiBasicTableColumn<Immutable<TrustedApp>>> = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
field: 'name',
|
||||
name: PROPERTY_TITLES.name,
|
||||
'data-test-subj': 'trustedAppNameTableCell',
|
||||
render(value: TrustedApp['name']) {
|
||||
return (
|
||||
<TextFieldValue
|
||||
fieldName={PROPERTY_TITLES.name}
|
||||
value={value}
|
||||
className="eui-textTruncate"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'os',
|
||||
name: PROPERTY_TITLES.os,
|
||||
render(value: TrustedApp['os']) {
|
||||
return (
|
||||
<TextFieldValue
|
||||
fieldName={PROPERTY_TITLES.os}
|
||||
value={OS_TITLES[value]}
|
||||
className="eui-textTruncate"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'created_at',
|
||||
name: PROPERTY_TITLES.created_at,
|
||||
render(value: TrustedApp['created_at']) {
|
||||
return (
|
||||
<FormattedDate
|
||||
fieldName={PROPERTY_TITLES.created_at}
|
||||
value={value}
|
||||
className="eui-textTruncate"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'created_by',
|
||||
name: PROPERTY_TITLES.created_by,
|
||||
render(value: TrustedApp['created_by']) {
|
||||
return (
|
||||
<TextFieldValue
|
||||
fieldName={PROPERTY_TITLES.created_by}
|
||||
value={value}
|
||||
className="eui-textTruncate"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: ACTIONS_COLUMN_TITLE,
|
||||
actions: [
|
||||
{
|
||||
name: LIST_ACTIONS.delete.name,
|
||||
description: LIST_ACTIONS.delete.description,
|
||||
'data-test-subj': 'trustedAppDeleteAction',
|
||||
isPrimary: true,
|
||||
icon: 'trash',
|
||||
color: 'danger',
|
||||
type: 'icon',
|
||||
onClick: (item: Immutable<TrustedApp>) => {
|
||||
dispatch({
|
||||
type: 'trustedAppDeletionDialogStarted',
|
||||
payload: { entry: item },
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
align: RIGHT_ALIGNMENT,
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
render({ id }: Immutable<TrustedApp>) {
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
onClick={() => toggleShowDetailsFor(id)}
|
||||
aria-label={detailsMap[id] ? 'Collapse' : 'Expand'}
|
||||
iconType={detailsMap[id] ? 'arrowUp' : 'arrowDown'}
|
||||
data-test-subj="trustedAppsListItemExpandButton"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [detailsMap, dispatch, toggleShowDetailsFor]);
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
columns={useMemo(
|
||||
() => getColumnDefinitions({ dispatch, detailsMapState: [detailsMap, setDetailsMap] }),
|
||||
[dispatch, detailsMap, setDetailsMap]
|
||||
)}
|
||||
items={useMemo(() => [...listItems], [listItems])}
|
||||
error={useTrustedAppsSelector(getListErrorMessage)}
|
||||
loading={useTrustedAppsSelector(isListLoading)}
|
||||
columns={tableColumns}
|
||||
items={listItems}
|
||||
error={listError}
|
||||
loading={isLoading}
|
||||
itemId="id"
|
||||
itemIdToExpandedRowMap={detailsMap}
|
||||
isExpandable={true}
|
||||
pagination={pagination}
|
||||
onChange={useTrustedAppsNavigateCallback(({ page }) => ({
|
||||
page_index: page.index,
|
||||
page_size: page.size,
|
||||
}))}
|
||||
onChange={handleTableOnChange}
|
||||
data-test-subj="trustedAppsList"
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -59,7 +59,7 @@ export const OPERATOR_TITLE: { [K in ConditionEntry['operator']]: string } = {
|
|||
};
|
||||
|
||||
export const PROPERTY_TITLES: Readonly<
|
||||
{ [K in keyof Omit<TrustedApp, 'id' | 'entries'>]: string }
|
||||
{ [K in keyof Omit<TrustedApp, 'id' | 'entries' | 'version'>]: string }
|
||||
> = {
|
||||
name: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.name', {
|
||||
defaultMessage: 'Name',
|
||||
|
@ -73,9 +73,18 @@ export const PROPERTY_TITLES: Readonly<
|
|||
created_by: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.createdBy', {
|
||||
defaultMessage: 'Created By',
|
||||
}),
|
||||
updated_at: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.updatedAt', {
|
||||
defaultMessage: 'Date Updated',
|
||||
}),
|
||||
updated_by: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.updatedBy', {
|
||||
defaultMessage: 'Updated By',
|
||||
}),
|
||||
description: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.description', {
|
||||
defaultMessage: 'Description',
|
||||
}),
|
||||
effectScope: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.effectScope', {
|
||||
defaultMessage: 'Effect scope',
|
||||
}),
|
||||
};
|
||||
|
||||
export const ENTRY_PROPERTY_TITLES: Readonly<
|
||||
|
@ -120,6 +129,13 @@ export const CARD_DELETE_BUTTON_LABEL = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const CARD_EDIT_BUTTON_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.trustedapps.card.editButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Edit',
|
||||
}
|
||||
);
|
||||
|
||||
export const GRID_VIEW_TOGGLE_LABEL = i18n.translate(
|
||||
'xpack.securitySolution.trustedapps.view.toggle.grid',
|
||||
{
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { ServerApiError } from '../../../../common/types';
|
||||
|
@ -16,6 +16,7 @@ import {
|
|||
getDeletionError,
|
||||
isCreationSuccessful,
|
||||
isDeletionSuccessful,
|
||||
isEdit,
|
||||
} from '../store/selectors';
|
||||
|
||||
import { useToasts } from '../../../../common/lib/kibana';
|
||||
|
@ -56,14 +57,27 @@ const getCreationSuccessMessage = (entry: Immutable<NewTrustedApp>) => {
|
|||
);
|
||||
};
|
||||
|
||||
const getUpdateSuccessMessage = (entry: Immutable<NewTrustedApp>) => {
|
||||
return i18n.translate(
|
||||
'xpack.securitySolution.trustedapps.createTrustedAppFlyout.updateSuccessToastTitle',
|
||||
{
|
||||
defaultMessage: '"{name}" has been updated successfully',
|
||||
values: { name: entry.name },
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const TrustedAppsNotifications = memo(() => {
|
||||
const deletionError = useTrustedAppsSelector(getDeletionError);
|
||||
const deletionDialogEntry = useTrustedAppsSelector(getDeletionDialogEntry);
|
||||
const deletionSuccessful = useTrustedAppsSelector(isDeletionSuccessful);
|
||||
const creationDialogNewEntry = useTrustedAppsSelector(getCreationDialogFormEntry);
|
||||
const creationSuccessful = useTrustedAppsSelector(isCreationSuccessful);
|
||||
const editMode = useTrustedAppsSelector(isEdit);
|
||||
const toasts = useToasts();
|
||||
|
||||
const [wasAlreadyHandled] = useState(new WeakSet());
|
||||
|
||||
if (deletionError && deletionDialogEntry) {
|
||||
toasts.addDanger(getDeletionErrorMessage(deletionError, deletionDialogEntry));
|
||||
}
|
||||
|
@ -72,8 +86,17 @@ export const TrustedAppsNotifications = memo(() => {
|
|||
toasts.addSuccess(getDeletionSuccessMessage(deletionDialogEntry));
|
||||
}
|
||||
|
||||
if (creationSuccessful && creationDialogNewEntry) {
|
||||
toasts.addSuccess(getCreationSuccessMessage(creationDialogNewEntry));
|
||||
if (
|
||||
creationSuccessful &&
|
||||
creationDialogNewEntry &&
|
||||
!wasAlreadyHandled.has(creationDialogNewEntry)
|
||||
) {
|
||||
wasAlreadyHandled.add(creationDialogNewEntry);
|
||||
|
||||
toasts.addSuccess(
|
||||
(editMode && getUpdateSuccessMessage(creationDialogNewEntry)) ||
|
||||
getCreationSuccessMessage(creationDialogNewEntry)
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
|
|
|
@ -20,16 +20,35 @@ import {
|
|||
TrustedApp,
|
||||
} from '../../../../../common/endpoint/types';
|
||||
import { HttpFetchOptions } from 'kibana/public';
|
||||
import { TRUSTED_APPS_LIST_API } from '../../../../../common/endpoint/constants';
|
||||
import {
|
||||
TRUSTED_APPS_GET_API,
|
||||
TRUSTED_APPS_LIST_API,
|
||||
} from '../../../../../common/endpoint/constants';
|
||||
import {
|
||||
GetPackagePoliciesResponse,
|
||||
PACKAGE_POLICY_API_ROUTES,
|
||||
} from '../../../../../../fleet/common';
|
||||
import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data';
|
||||
import { isFailedResourceState, isLoadedResourceState } from '../state';
|
||||
import { forceHTMLElementOffsetWidth } from './components/effected_policy_select/test_utils';
|
||||
import { resolvePathVariables } from '../service/utils';
|
||||
import { toUpdateTrustedApp } from '../../../../../common/endpoint/service/trusted_apps/to_update_trusted_app';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
|
||||
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
|
||||
htmlIdGenerator: () => () => 'mockId',
|
||||
}));
|
||||
|
||||
// TODO: remove this mock when feature flag is removed
|
||||
jest.mock('../../../../common/hooks/use_experimental_features');
|
||||
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
|
||||
|
||||
describe('When on the Trusted Apps Page', () => {
|
||||
const expectedAboutInfo =
|
||||
'Add a trusted application to improve performance or alleviate conflicts with other applications running on your hosts. Trusted applications will be applied to hosts running Endpoint Security.';
|
||||
|
||||
const generator = new EndpointDocGenerator('policy-list');
|
||||
|
||||
let mockedContext: AppContextTestRender;
|
||||
let history: AppContextTestRender['history'];
|
||||
let coreStart: AppContextTestRender['coreStart'];
|
||||
|
@ -40,11 +59,15 @@ describe('When on the Trusted Apps Page', () => {
|
|||
|
||||
const getFakeTrustedApp = (): TrustedApp => ({
|
||||
id: '1111-2222-3333-4444',
|
||||
version: 'abc123',
|
||||
name: 'one app',
|
||||
os: OperatingSystem.WINDOWS,
|
||||
created_at: '2021-01-04T13:55:00.561Z',
|
||||
created_by: 'me',
|
||||
updated_at: '2021-01-04T13:55:00.561Z',
|
||||
updated_by: 'me',
|
||||
description: 'a good one',
|
||||
effectScope: { type: 'global' },
|
||||
entries: [
|
||||
{
|
||||
field: ConditionEntryField.PATH,
|
||||
|
@ -55,6 +78,19 @@ describe('When on the Trusted Apps Page', () => {
|
|||
],
|
||||
});
|
||||
|
||||
const createListApiResponse = (
|
||||
page: number = 1,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
per_page: number = 20
|
||||
): GetTrustedListAppsResponse => {
|
||||
return {
|
||||
data: [getFakeTrustedApp()],
|
||||
total: 50, // << Should be a value large enough to fulfill two pages
|
||||
page,
|
||||
per_page,
|
||||
};
|
||||
};
|
||||
|
||||
const mockListApis = (http: AppContextTestRender['coreStart']['http']) => {
|
||||
const currentGetHandler = http.get.getMockImplementation();
|
||||
|
||||
|
@ -64,13 +100,26 @@ describe('When on the Trusted Apps Page', () => {
|
|||
const httpOptions = args[1] as HttpFetchOptions;
|
||||
|
||||
if (path === TRUSTED_APPS_LIST_API) {
|
||||
return {
|
||||
data: [getFakeTrustedApp()],
|
||||
total: 50, // << Should be a value large enough to fulfill two pages
|
||||
page: httpOptions?.query?.page ?? 1,
|
||||
per_page: httpOptions?.query?.per_page ?? 20,
|
||||
};
|
||||
return createListApiResponse(
|
||||
Number(httpOptions?.query?.page ?? 1),
|
||||
Number(httpOptions?.query?.per_page ?? 20)
|
||||
);
|
||||
}
|
||||
|
||||
if (path === PACKAGE_POLICY_API_ROUTES.LIST_PATTERN) {
|
||||
const policy = generator.generatePolicyPackagePolicy();
|
||||
policy.name = 'test policy A';
|
||||
policy.id = 'abc123';
|
||||
|
||||
const response: GetPackagePoliciesResponse = {
|
||||
items: [policy],
|
||||
page: 1,
|
||||
perPage: 1000,
|
||||
total: 1,
|
||||
};
|
||||
return response;
|
||||
}
|
||||
|
||||
if (currentGetHandler) {
|
||||
return currentGetHandler(...args);
|
||||
}
|
||||
|
@ -98,7 +147,9 @@ describe('When on the Trusted Apps Page', () => {
|
|||
window.scrollTo = jest.fn();
|
||||
});
|
||||
|
||||
describe('and there is trusted app entries', () => {
|
||||
afterEach(() => reactTestingLibrary.cleanup());
|
||||
|
||||
describe('and there are trusted app entries', () => {
|
||||
const renderWithListData = async () => {
|
||||
const renderResult = render();
|
||||
await act(async () => {
|
||||
|
@ -119,20 +170,283 @@ describe('When on the Trusted Apps Page', () => {
|
|||
const addButton = await getByTestId('trustedAppsListAddButton');
|
||||
expect(addButton.textContent).toBe('Add Trusted Application');
|
||||
});
|
||||
|
||||
describe('and the Grid view is being displayed', () => {
|
||||
describe('and the edit trusted app button is clicked', () => {
|
||||
let renderResult: ReturnType<AppContextTestRender['render']>;
|
||||
|
||||
beforeEach(async () => {
|
||||
renderResult = await renderWithListData();
|
||||
act(() => {
|
||||
fireEvent.click(renderResult.getByTestId('trustedAppEditButton'));
|
||||
});
|
||||
});
|
||||
|
||||
it('should persist edit params to url', () => {
|
||||
expect(history.location.search).toEqual('?show=edit&id=1111-2222-3333-4444');
|
||||
});
|
||||
|
||||
it('should display the Edit flyout', () => {
|
||||
expect(renderResult.getByTestId('addTrustedAppFlyout'));
|
||||
});
|
||||
|
||||
it('should NOT display the about info for trusted apps', () => {
|
||||
expect(renderResult.queryByTestId('addTrustedAppFlyout-about')).toBeNull();
|
||||
});
|
||||
|
||||
it('should show correct flyout title', () => {
|
||||
expect(renderResult.getByTestId('addTrustedAppFlyout-headerTitle').textContent).toBe(
|
||||
'Edit trusted application'
|
||||
);
|
||||
});
|
||||
|
||||
it('should display the expected text for the Save button', () => {
|
||||
expect(renderResult.getByTestId('addTrustedAppFlyout-createButton').textContent).toEqual(
|
||||
'Save'
|
||||
);
|
||||
});
|
||||
|
||||
it('should display trusted app data for edit', async () => {
|
||||
const formNameInput = renderResult.getByTestId(
|
||||
'addTrustedAppFlyout-createForm-nameTextField'
|
||||
) as HTMLInputElement;
|
||||
const formDescriptionInput = renderResult.getByTestId(
|
||||
'addTrustedAppFlyout-createForm-descriptionField'
|
||||
) as HTMLTextAreaElement;
|
||||
|
||||
expect(formNameInput.value).toEqual('one app');
|
||||
expect(formDescriptionInput.value).toEqual('a good one');
|
||||
});
|
||||
|
||||
describe('and when Save is clicked', () => {
|
||||
it('should call the correct api (PUT)', () => {
|
||||
act(() => {
|
||||
fireEvent.click(renderResult.getByTestId('addTrustedAppFlyout-createButton'));
|
||||
});
|
||||
|
||||
expect(coreStart.http.put).toHaveBeenCalledTimes(1);
|
||||
|
||||
const lastCallToPut = (coreStart.http.put.mock.calls[0] as unknown) as [
|
||||
string,
|
||||
HttpFetchOptions
|
||||
];
|
||||
|
||||
expect(lastCallToPut[0]).toEqual('/api/endpoint/trusted_apps/1111-2222-3333-4444');
|
||||
expect(JSON.parse(lastCallToPut[1].body as string)).toEqual({
|
||||
name: 'one app',
|
||||
os: 'windows',
|
||||
entries: [
|
||||
{
|
||||
field: 'process.executable.caseless',
|
||||
value: 'one/two',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
},
|
||||
],
|
||||
description: 'a good one',
|
||||
effectScope: {
|
||||
type: 'global',
|
||||
},
|
||||
version: 'abc123',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and attempting to show Edit panel based on URL params', () => {
|
||||
const TRUSTED_APP_GET_URI = resolvePathVariables(TRUSTED_APPS_GET_API, {
|
||||
id: '9999-edit-8888',
|
||||
});
|
||||
|
||||
const renderAndWaitForGetApi = async () => {
|
||||
// the store action watcher is setup prior to render because `renderWithListData()`
|
||||
// also awaits API calls and this action could be missed.
|
||||
const apiResponseForEditTrustedApp = waitForAction(
|
||||
'trustedAppCreationEditItemStateChanged',
|
||||
{
|
||||
validate({ payload }) {
|
||||
return isLoadedResourceState(payload) || isFailedResourceState(payload);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const renderResult = await renderWithListData();
|
||||
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await apiResponseForEditTrustedApp;
|
||||
});
|
||||
|
||||
return renderResult;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the API GET for the trusted application
|
||||
const priorMockImplementation = coreStart.http.get.getMockImplementation();
|
||||
coreStart.http.get.mockImplementation(async (...args) => {
|
||||
if ('string' === typeof args[0] && args[0] === TRUSTED_APP_GET_URI) {
|
||||
return {
|
||||
data: {
|
||||
...getFakeTrustedApp(),
|
||||
id: '9999-edit-8888',
|
||||
name: 'one app for edit',
|
||||
},
|
||||
};
|
||||
}
|
||||
if (priorMockImplementation) {
|
||||
return priorMockImplementation(...args);
|
||||
}
|
||||
});
|
||||
|
||||
reactTestingLibrary.act(() => {
|
||||
history.push('/trusted_apps?show=edit&id=9999-edit-8888');
|
||||
});
|
||||
});
|
||||
|
||||
it('should retrieve trusted app via API using url `id`', async () => {
|
||||
const renderResult = await renderAndWaitForGetApi();
|
||||
|
||||
expect(coreStart.http.get).toHaveBeenCalledWith(TRUSTED_APP_GET_URI);
|
||||
|
||||
expect(
|
||||
(renderResult.getByTestId(
|
||||
'addTrustedAppFlyout-createForm-nameTextField'
|
||||
) as HTMLInputElement).value
|
||||
).toEqual('one app for edit');
|
||||
});
|
||||
|
||||
it('should redirect to list and show toast message if `id` is missing from URL', async () => {
|
||||
reactTestingLibrary.act(() => {
|
||||
history.push('/trusted_apps?show=edit&id=');
|
||||
});
|
||||
|
||||
await renderAndWaitForGetApi();
|
||||
|
||||
expect(history.location.search).toEqual('');
|
||||
expect(coreStart.notifications.toasts.addWarning.mock.calls[0][0]).toEqual(
|
||||
'Unable to edit trusted application (No id provided)'
|
||||
);
|
||||
});
|
||||
|
||||
it('should redirect to list and show toast message on API error for GET of `id`', async () => {
|
||||
// Mock the API GET for the trusted application
|
||||
const priorMockImplementation = coreStart.http.get.getMockImplementation();
|
||||
coreStart.http.get.mockImplementation(async (...args) => {
|
||||
if ('string' === typeof args[0] && args[0] === TRUSTED_APP_GET_URI) {
|
||||
throw new Error('test: api error response');
|
||||
}
|
||||
if (priorMockImplementation) {
|
||||
return priorMockImplementation(...args);
|
||||
}
|
||||
});
|
||||
|
||||
await renderAndWaitForGetApi();
|
||||
|
||||
expect(history.location.search).toEqual('');
|
||||
expect(coreStart.notifications.toasts.addWarning.mock.calls[0][0]).toEqual(
|
||||
'Unable to edit trusted application (test: api error response)'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and the List view is being displayed', () => {
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
|
||||
const expandFirstRow = () => {
|
||||
reactTestingLibrary.act(() => {
|
||||
fireEvent.click(renderResult.getByTestId('trustedAppsListItemExpandButton'));
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
reactTestingLibrary.act(() => {
|
||||
history.push('/trusted_apps?view_type=list');
|
||||
});
|
||||
|
||||
renderResult = await renderWithListData();
|
||||
});
|
||||
|
||||
it('should display the list', () => {
|
||||
expect(renderResult.getByTestId('trustedAppsList'));
|
||||
});
|
||||
|
||||
it('should show a card when row is expanded', () => {
|
||||
expandFirstRow();
|
||||
expect(renderResult.getByTestId('trustedAppCard'));
|
||||
});
|
||||
|
||||
it('should show Edit flyout when edit button on card is clicked', () => {
|
||||
expandFirstRow();
|
||||
reactTestingLibrary.act(() => {
|
||||
fireEvent.click(renderResult.getByTestId('trustedAppEditButton'));
|
||||
});
|
||||
expect(renderResult.findByTestId('addTrustedAppFlyout'));
|
||||
});
|
||||
|
||||
it('should reflect updated information on row and card when updated data is received', async () => {
|
||||
expandFirstRow();
|
||||
reactTestingLibrary.act(() => {
|
||||
const updatedListContent = createListApiResponse();
|
||||
updatedListContent.data[0]!.name = 'updated trusted app';
|
||||
updatedListContent.data[0]!.description = 'updated trusted app description';
|
||||
|
||||
mockedContext.store.dispatch({
|
||||
type: 'trustedAppsListResourceStateChanged',
|
||||
payload: {
|
||||
newState: {
|
||||
type: 'LoadedResourceState',
|
||||
data: {
|
||||
items: updatedListContent.data,
|
||||
pageIndex: updatedListContent.page,
|
||||
pageSize: updatedListContent.per_page,
|
||||
totalItemsCount: updatedListContent.total,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// The additional prefix of `Name` is due to the hidden element in DOM that is only shown
|
||||
// for mobile devices (inserted by the EuiBasicTable)
|
||||
expect(renderResult.getByTestId('trustedAppNameTableCell').textContent).toEqual(
|
||||
'Nameupdated trusted app'
|
||||
);
|
||||
expect(renderResult.getByText('updated trusted app description'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the Add Trusted App button is clicked', () => {
|
||||
describe('and the Add Trusted App button is clicked', () => {
|
||||
const renderAndClickAddButton = async (): Promise<
|
||||
ReturnType<AppContextTestRender['render']>
|
||||
> => {
|
||||
const renderResult = render();
|
||||
await act(async () => {
|
||||
await waitForAction('trustedAppsListResourceStateChanged');
|
||||
await Promise.all([
|
||||
waitForAction('trustedAppsListResourceStateChanged'),
|
||||
waitForAction('trustedAppsExistStateChanged', {
|
||||
validate({ payload }) {
|
||||
return isLoadedResourceState(payload);
|
||||
},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
const addButton = renderResult.getByTestId('trustedAppsListAddButton');
|
||||
reactTestingLibrary.act(() => {
|
||||
|
||||
act(() => {
|
||||
const addButton = renderResult.getByTestId('trustedAppsListAddButton');
|
||||
fireEvent.click(addButton, { button: 1 });
|
||||
});
|
||||
|
||||
// Wait for the policies to be loaded
|
||||
await act(async () => {
|
||||
await waitForAction('trustedAppsPoliciesStateChanged', {
|
||||
validate: (action) => {
|
||||
return isLoadedResourceState(action.payload);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return renderResult;
|
||||
};
|
||||
|
||||
|
@ -145,6 +459,8 @@ describe('When on the Trusted Apps Page', () => {
|
|||
|
||||
const flyoutTitle = getByTestId('addTrustedAppFlyout-headerTitle');
|
||||
expect(flyoutTitle.textContent).toBe('Add trusted application');
|
||||
|
||||
expect(getByTestId('addTrustedAppFlyout-about'));
|
||||
});
|
||||
|
||||
it('should update the URL to indicate the flyout is opened', async () => {
|
||||
|
@ -165,6 +481,14 @@ describe('When on the Trusted Apps Page', () => {
|
|||
expect(queryByTestId('addTrustedAppFlyout-createForm')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should have list of policies populated', async () => {
|
||||
useIsExperimentalFeatureEnabledMock.mockReturnValue(true);
|
||||
const resetEnv = forceHTMLElementOffsetWidth();
|
||||
const { getByTestId } = await renderAndClickAddButton();
|
||||
expect(getByTestId('policy-abc123'));
|
||||
resetEnv();
|
||||
});
|
||||
|
||||
it('should initially have the flyout Add button disabled', async () => {
|
||||
const { getByTestId } = await renderAndClickAddButton();
|
||||
expect((getByTestId('addTrustedAppFlyout-createButton') as HTMLButtonElement).disabled).toBe(
|
||||
|
@ -175,48 +499,45 @@ describe('When on the Trusted Apps Page', () => {
|
|||
it('should close flyout if cancel button is clicked', async () => {
|
||||
const { getByTestId, queryByTestId } = await renderAndClickAddButton();
|
||||
const cancelButton = getByTestId('addTrustedAppFlyout-cancelButton');
|
||||
reactTestingLibrary.act(() => {
|
||||
await reactTestingLibrary.act(async () => {
|
||||
fireEvent.click(cancelButton, { button: 1 });
|
||||
await waitForAction('trustedAppCreationDialogClosed');
|
||||
});
|
||||
expect(queryByTestId('addTrustedAppFlyout')).toBeNull();
|
||||
expect(history.location.search).toBe('');
|
||||
expect(queryByTestId('addTrustedAppFlyout')).toBeNull();
|
||||
});
|
||||
|
||||
it('should close flyout if flyout close button is clicked', async () => {
|
||||
const { getByTestId, queryByTestId } = await renderAndClickAddButton();
|
||||
const flyoutCloseButton = getByTestId('euiFlyoutCloseButton');
|
||||
reactTestingLibrary.act(() => {
|
||||
await reactTestingLibrary.act(async () => {
|
||||
fireEvent.click(flyoutCloseButton, { button: 1 });
|
||||
await waitForAction('trustedAppCreationDialogClosed');
|
||||
});
|
||||
expect(queryByTestId('addTrustedAppFlyout')).toBeNull();
|
||||
expect(history.location.search).toBe('');
|
||||
});
|
||||
|
||||
describe('and when the form data is valid', () => {
|
||||
const fillInCreateForm = ({ getByTestId }: ReturnType<AppContextTestRender['render']>) => {
|
||||
reactTestingLibrary.act(() => {
|
||||
fireEvent.change(getByTestId('addTrustedAppFlyout-createForm-nameTextField'), {
|
||||
target: { value: 'trusted app A' },
|
||||
});
|
||||
|
||||
fireEvent.change(
|
||||
getByTestId('addTrustedAppFlyout-createForm-conditionsBuilder-group1-entry0-value'),
|
||||
{ target: { value: '44ed10b389dbcd1cf16cec79d16d7378' } }
|
||||
);
|
||||
|
||||
fireEvent.change(getByTestId('addTrustedAppFlyout-createForm-descriptionField'), {
|
||||
target: { value: 'let this be' },
|
||||
});
|
||||
const fillInCreateForm = async () => {
|
||||
mockedContext.store.dispatch({
|
||||
type: 'trustedAppCreationDialogFormStateUpdated',
|
||||
payload: {
|
||||
isValid: true,
|
||||
entry: toUpdateTrustedApp<TrustedApp>(getFakeTrustedApp()),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it('should enable the Flyout Add button', async () => {
|
||||
const renderResult = await renderAndClickAddButton();
|
||||
const { getByTestId } = renderResult;
|
||||
fillInCreateForm(renderResult);
|
||||
const flyoutAddButton = getByTestId(
|
||||
|
||||
await fillInCreateForm();
|
||||
|
||||
const flyoutAddButton = renderResult.getByTestId(
|
||||
'addTrustedAppFlyout-createButton'
|
||||
) as HTMLButtonElement;
|
||||
|
||||
expect(flyoutAddButton.disabled).toBe(false);
|
||||
});
|
||||
|
||||
|
@ -242,7 +563,7 @@ describe('When on the Trusted Apps Page', () => {
|
|||
);
|
||||
|
||||
renderResult = await renderAndClickAddButton();
|
||||
fillInCreateForm(renderResult);
|
||||
await fillInCreateForm();
|
||||
const userClickedSaveActionWatcher = waitForAction('trustedAppCreationDialogConfirmed');
|
||||
reactTestingLibrary.act(() => {
|
||||
fireEvent.click(renderResult.getByTestId('addTrustedAppFlyout-createButton'), {
|
||||
|
@ -288,8 +609,11 @@ describe('When on the Trusted Apps Page', () => {
|
|||
data: {
|
||||
...(JSON.parse(httpPostBody) as NewTrustedApp),
|
||||
id: '1',
|
||||
version: 'abc123',
|
||||
created_at: '2020-09-16T14:09:45.484Z',
|
||||
created_by: 'kibana',
|
||||
updated_at: '2021-01-04T13:55:00.561Z',
|
||||
updated_by: 'me',
|
||||
},
|
||||
};
|
||||
await reactTestingLibrary.act(async () => {
|
||||
|
@ -308,7 +632,7 @@ describe('When on the Trusted Apps Page', () => {
|
|||
|
||||
it('should show success toast notification', async () => {
|
||||
expect(coreStart.notifications.toasts.addSuccess.mock.calls[0][0]).toEqual(
|
||||
'"trusted app A" has been added to the Trusted Applications list.'
|
||||
'"one app" has been added to the Trusted Applications list.'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -386,6 +710,19 @@ describe('When on the Trusted Apps Page', () => {
|
|||
expect(flyoutAddButton.disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and there is a feature flag for agents policy', () => {
|
||||
it('should hide agents policy if feature flag is disabled', async () => {
|
||||
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
|
||||
const renderResult = await renderAndClickAddButton();
|
||||
expect(renderResult).toMatchSnapshot();
|
||||
});
|
||||
it('should display agents policy if feature flag is enabled', async () => {
|
||||
useIsExperimentalFeatureEnabledMock.mockReturnValue(true);
|
||||
const renderResult = await renderAndClickAddButton();
|
||||
expect(renderResult).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and there are no trusted apps', () => {
|
||||
|
|
|
@ -46,13 +46,19 @@ export const TrustedAppsPage = memo(() => {
|
|||
const totalItemsCount = useTrustedAppsSelector(getListTotalItemsCount);
|
||||
const isCheckingIfEntriesExists = useTrustedAppsSelector(checkingIfEntriesExist);
|
||||
const doEntriesExist = useTrustedAppsSelector(entriesExist) === true;
|
||||
const handleAddButtonClick = useTrustedAppsNavigateCallback(() => ({ show: 'create' }));
|
||||
const handleAddFlyoutClose = useTrustedAppsNavigateCallback(() => ({ show: undefined }));
|
||||
const handleAddButtonClick = useTrustedAppsNavigateCallback(() => ({
|
||||
show: 'create',
|
||||
id: undefined,
|
||||
}));
|
||||
const handleAddFlyoutClose = useTrustedAppsNavigateCallback(() => ({
|
||||
show: undefined,
|
||||
id: undefined,
|
||||
}));
|
||||
const handleViewTypeChange = useTrustedAppsNavigateCallback((viewType: ViewType) => ({
|
||||
view_type: viewType,
|
||||
}));
|
||||
|
||||
const showCreateFlyout = location.show === 'create';
|
||||
const showCreateFlyout = !!location.show;
|
||||
|
||||
const backButton = useMemo(() => {
|
||||
if (routeState && routeState.onBackButtonNavigateTo) {
|
||||
|
|
|
@ -65,14 +65,19 @@ import {
|
|||
import { SecurityAppStore } from './common/store/store';
|
||||
import { getCaseConnectorUI } from './cases/components/connectors';
|
||||
import { licenseService } from './common/hooks/use_license';
|
||||
import { SecuritySolutionUiConfigType } from './common/types';
|
||||
|
||||
import { getLazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension';
|
||||
import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension';
|
||||
import { getLazyEndpointPackageCustomExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension';
|
||||
import { parseExperimentalConfigValue } from '../common/experimental_features';
|
||||
|
||||
export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, StartPlugins> {
|
||||
private kibanaVersion: string;
|
||||
private config: SecuritySolutionUiConfigType;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config = this.initializerContext.config.get<SecuritySolutionUiConfigType>();
|
||||
this.kibanaVersion = initializerContext.env.packageInfo.version;
|
||||
}
|
||||
private detectionsUpdater$ = new Subject<AppUpdater>();
|
||||
|
@ -520,6 +525,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
kibanaIndexPatterns,
|
||||
configIndexPatterns: configIndexPatterns.indicesExist,
|
||||
signalIndexName: signal.name,
|
||||
enableExperimental: parseExperimentalConfigValue(this.config.enableExperimental || []),
|
||||
}
|
||||
),
|
||||
{
|
||||
|
|
|
@ -98,10 +98,13 @@ const generateTrustedAppEntry: (options?: GenerateTrustedAppEntryOptions) => obj
|
|||
os = randomOperatingSystem(),
|
||||
name = randomName(),
|
||||
} = {}): NewTrustedApp => {
|
||||
return {
|
||||
const newTrustedApp: NewTrustedApp = {
|
||||
description: `Generator says we trust ${name}`,
|
||||
name,
|
||||
os,
|
||||
effectScope: {
|
||||
type: 'global',
|
||||
},
|
||||
entries: [
|
||||
{
|
||||
// @ts-ignore
|
||||
|
@ -119,6 +122,8 @@ const generateTrustedAppEntry: (options?: GenerateTrustedAppEntryOptions) => obj
|
|||
},
|
||||
],
|
||||
};
|
||||
|
||||
return newTrustedApp;
|
||||
};
|
||||
|
||||
const randomN = (max: number): number => Math.floor(Math.random() * max);
|
||||
|
|
|
@ -31,6 +31,7 @@ import { MetadataRequestContext } from './routes/metadata/handlers';
|
|||
// import { licenseMock } from '../../../licensing/common/licensing.mock';
|
||||
import { LicenseService } from '../../common/license/license';
|
||||
import { SecuritySolutionRequestHandlerContext } from '../types';
|
||||
import { parseExperimentalConfigValue } from '../../common/experimental_features';
|
||||
|
||||
/**
|
||||
* Creates a mocked EndpointAppContext.
|
||||
|
@ -42,6 +43,7 @@ export const createMockEndpointAppContext = (
|
|||
logFactory: loggingSystemMock.create(),
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
service: createMockEndpointAppContextService(mockManifestManager),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
httpServerMock,
|
||||
loggingSystemMock,
|
||||
} from 'src/core/server/mocks';
|
||||
import { parseExperimentalConfigValue } from '../../../../common/experimental_features';
|
||||
import { ArtifactConstants } from '../../lib/artifacts';
|
||||
import { registerDownloadArtifactRoute } from './download_artifact';
|
||||
import { EndpointAppContextService } from '../../endpoint_app_context_services';
|
||||
|
@ -130,6 +131,7 @@ describe('test alerts route', () => {
|
|||
logFactory: loggingSystemMock.create(),
|
||||
service: endpointAppContextService,
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
},
|
||||
cache
|
||||
);
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
HostStatus,
|
||||
MetadataQueryStrategyVersions,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import { parseExperimentalConfigValue } from '../../../../common/experimental_features';
|
||||
import { registerEndpointRoutes, METADATA_REQUEST_ROUTE } from './index';
|
||||
import {
|
||||
createMockEndpointAppContextServiceStartContract,
|
||||
|
@ -93,6 +94,7 @@ describe('test endpoint route', () => {
|
|||
logFactory: loggingSystemMock.create(),
|
||||
service: endpointAppContextService,
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -188,6 +190,7 @@ describe('test endpoint route', () => {
|
|||
logFactory: loggingSystemMock.create(),
|
||||
service: endpointAppContextService,
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ import {
|
|||
import { EndpointAppContextService } from '../../endpoint_app_context_services';
|
||||
import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__';
|
||||
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
|
||||
import { parseExperimentalConfigValue } from '../../../../common/experimental_features';
|
||||
import { Agent, EsAssetReference } from '../../../../../fleet/common/types/models';
|
||||
import { createV1SearchResponse } from './support/test_support';
|
||||
import { PackageService } from '../../../../../fleet/server/services';
|
||||
|
@ -84,6 +85,7 @@ describe('test endpoint route v1', () => {
|
|||
logFactory: loggingSystemMock.create(),
|
||||
service: endpointAppContextService,
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from '
|
|||
import { EndpointAppContextService } from '../../endpoint_app_context_services';
|
||||
import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__';
|
||||
import { metadataCurrentIndexPattern } from '../../../../common/endpoint/constants';
|
||||
import { parseExperimentalConfigValue } from '../../../../common/experimental_features';
|
||||
import { metadataQueryStrategyV2 } from './support/query_strategies';
|
||||
|
||||
describe('query builder', () => {
|
||||
|
@ -22,6 +23,7 @@ describe('query builder', () => {
|
|||
logFactory: loggingSystemMock.create(),
|
||||
service: new EndpointAppContextService(),
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
},
|
||||
metadataQueryStrategyV2()
|
||||
);
|
||||
|
@ -36,6 +38,7 @@ describe('query builder', () => {
|
|||
logFactory: loggingSystemMock.create(),
|
||||
service: new EndpointAppContextService(),
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
},
|
||||
metadataQueryStrategyV2()
|
||||
);
|
||||
|
@ -63,6 +66,7 @@ describe('query builder', () => {
|
|||
logFactory: loggingSystemMock.create(),
|
||||
service: new EndpointAppContextService(),
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
},
|
||||
metadataQueryStrategyV2()
|
||||
);
|
||||
|
@ -80,6 +84,7 @@ describe('query builder', () => {
|
|||
logFactory: loggingSystemMock.create(),
|
||||
service: new EndpointAppContextService(),
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
},
|
||||
metadataQueryStrategyV2(),
|
||||
{
|
||||
|
@ -111,6 +116,7 @@ describe('query builder', () => {
|
|||
logFactory: loggingSystemMock.create(),
|
||||
service: new EndpointAppContextService(),
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
},
|
||||
metadataQueryStrategyV2()
|
||||
);
|
||||
|
@ -149,6 +155,9 @@ describe('query builder', () => {
|
|||
logFactory: loggingSystemMock.create(),
|
||||
service: new EndpointAppContextService(),
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(
|
||||
createMockConfig().enableExperimental
|
||||
),
|
||||
},
|
||||
metadataQueryStrategyV2(),
|
||||
{
|
||||
|
|
|
@ -10,6 +10,7 @@ import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from '
|
|||
import { EndpointAppContextService } from '../../endpoint_app_context_services';
|
||||
import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__';
|
||||
import { metadataIndexPattern } from '../../../../common/endpoint/constants';
|
||||
import { parseExperimentalConfigValue } from '../../../../common/experimental_features';
|
||||
import { metadataQueryStrategyV1 } from './support/query_strategies';
|
||||
|
||||
describe('query builder v1', () => {
|
||||
|
@ -24,6 +25,7 @@ describe('query builder v1', () => {
|
|||
logFactory: loggingSystemMock.create(),
|
||||
service: new EndpointAppContextService(),
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
},
|
||||
metadataQueryStrategyV1()
|
||||
);
|
||||
|
@ -61,6 +63,9 @@ describe('query builder v1', () => {
|
|||
logFactory: loggingSystemMock.create(),
|
||||
service: new EndpointAppContextService(),
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(
|
||||
createMockConfig().enableExperimental
|
||||
),
|
||||
},
|
||||
metadataQueryStrategyV1(),
|
||||
{
|
||||
|
@ -88,6 +93,7 @@ describe('query builder v1', () => {
|
|||
logFactory: loggingSystemMock.create(),
|
||||
service: new EndpointAppContextService(),
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
},
|
||||
metadataQueryStrategyV1()
|
||||
);
|
||||
|
@ -126,6 +132,9 @@ describe('query builder v1', () => {
|
|||
logFactory: loggingSystemMock.create(),
|
||||
service: new EndpointAppContextService(),
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(
|
||||
createMockConfig().enableExperimental
|
||||
),
|
||||
},
|
||||
metadataQueryStrategyV1(),
|
||||
{
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
import { SearchResponse } from 'elasticsearch';
|
||||
import { GetHostPolicyResponse, HostPolicyResponse } from '../../../../common/endpoint/types';
|
||||
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
|
||||
import { parseExperimentalConfigValue } from '../../../../common/experimental_features';
|
||||
import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__';
|
||||
import { Agent } from '../../../../../fleet/common/types/models';
|
||||
import { AgentService } from '../../../../../fleet/server/services';
|
||||
|
@ -171,6 +172,7 @@ describe('test policy response handler', () => {
|
|||
logFactory: loggingSystemMock.create(),
|
||||
service: endpointAppContextService,
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
});
|
||||
|
||||
const mockRequest = httpServerMock.createKibanaRequest({
|
||||
|
@ -201,6 +203,7 @@ describe('test policy response handler', () => {
|
|||
logFactory: loggingSystemMock.create(),
|
||||
service: endpointAppContextService,
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
});
|
||||
|
||||
const mockRequest = httpServerMock.createKibanaRequest({
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
export class TrustedAppNotFoundError extends Error {
|
||||
constructor(id: string) {
|
||||
super(`Trusted Application (${id}) not found`);
|
||||
}
|
||||
}
|
||||
|
||||
export class TrustedAppVersionConflictError extends Error {
|
||||
constructor(id: string, public sourceError: Error) {
|
||||
super(`Trusted Application (${id}) has been updated since last retrieved`);
|
||||
}
|
||||
}
|
|
@ -14,45 +14,30 @@ import { listMock } from '../../../../../lists/server/mocks';
|
|||
import { ExceptionListClient } from '../../../../../lists/server';
|
||||
import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__';
|
||||
|
||||
import { ConditionEntryField, OperatingSystem } from '../../../../common/endpoint/types';
|
||||
import {
|
||||
ConditionEntryField,
|
||||
NewTrustedApp,
|
||||
OperatingSystem,
|
||||
TrustedApp,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import { parseExperimentalConfigValue } from '../../../../common/experimental_features';
|
||||
import { EndpointAppContextService } from '../../endpoint_app_context_services';
|
||||
import { createConditionEntry, createEntryMatch } from './mapping';
|
||||
import {
|
||||
getTrustedAppsCreateRouteHandler,
|
||||
getTrustedAppsDeleteRouteHandler,
|
||||
getTrustedAppsGetOneHandler,
|
||||
getTrustedAppsListRouteHandler,
|
||||
getTrustedAppsSummaryRouteHandler,
|
||||
getTrustedAppsUpdateRouteHandler,
|
||||
} from './handlers';
|
||||
import type { SecuritySolutionRequestHandlerContext } from '../../../types';
|
||||
|
||||
const exceptionsListClient = listMock.getExceptionListClient() as jest.Mocked<ExceptionListClient>;
|
||||
|
||||
const createAppContextMock = () => ({
|
||||
logFactory: loggingSystemMock.create(),
|
||||
service: new EndpointAppContextService(),
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
});
|
||||
|
||||
const createHandlerContextMock = () =>
|
||||
(({
|
||||
...xpackMocks.createRequestHandlerContext(),
|
||||
lists: {
|
||||
getListClient: jest.fn(),
|
||||
getExceptionListClient: jest.fn().mockReturnValue(exceptionsListClient),
|
||||
},
|
||||
} as unknown) as jest.Mocked<SecuritySolutionRequestHandlerContext>);
|
||||
|
||||
const assertResponse = <T>(
|
||||
response: jest.Mocked<KibanaResponseFactory>,
|
||||
expectedResponseType: keyof KibanaResponseFactory,
|
||||
expectedResponseBody: T
|
||||
) => {
|
||||
expect(response[expectedResponseType]).toBeCalled();
|
||||
expect(response[expectedResponseType].mock.calls[0][0]?.body).toEqual(expectedResponseBody);
|
||||
};
|
||||
import { TrustedAppNotFoundError, TrustedAppVersionConflictError } from './errors';
|
||||
import { updateExceptionListItemImplementationMock } from './test_utils';
|
||||
import { Logger } from '@kbn/logging';
|
||||
|
||||
const EXCEPTION_LIST_ITEM: ExceptionListItemSchema = {
|
||||
_version: '123',
|
||||
_version: 'abc123',
|
||||
id: '123',
|
||||
comments: [],
|
||||
created_at: '11/11/2011T11:11:11.111',
|
||||
|
@ -68,30 +53,35 @@ const EXCEPTION_LIST_ITEM: ExceptionListItemSchema = {
|
|||
name: 'linux trusted app 1',
|
||||
namespace_type: 'agnostic',
|
||||
os_types: ['linux'],
|
||||
tags: [],
|
||||
tags: ['policy:all'],
|
||||
type: 'simple',
|
||||
tie_breaker_id: '123',
|
||||
updated_at: '11/11/2011T11:11:11.111',
|
||||
updated_by: 'admin',
|
||||
updated_at: '2021-01-04T13:55:00.561Z',
|
||||
updated_by: 'me',
|
||||
};
|
||||
|
||||
const NEW_TRUSTED_APP = {
|
||||
const NEW_TRUSTED_APP: NewTrustedApp = {
|
||||
name: 'linux trusted app 1',
|
||||
description: 'Linux trusted app 1',
|
||||
os: OperatingSystem.LINUX,
|
||||
effectScope: { type: 'global' },
|
||||
entries: [
|
||||
createConditionEntry(ConditionEntryField.PATH, '/bin/malware'),
|
||||
createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'),
|
||||
],
|
||||
};
|
||||
|
||||
const TRUSTED_APP = {
|
||||
const TRUSTED_APP: TrustedApp = {
|
||||
id: '123',
|
||||
version: 'abc123',
|
||||
created_at: '11/11/2011T11:11:11.111',
|
||||
created_by: 'admin',
|
||||
updated_at: '2021-01-04T13:55:00.561Z',
|
||||
updated_by: 'me',
|
||||
name: 'linux trusted app 1',
|
||||
description: 'Linux trusted app 1',
|
||||
os: OperatingSystem.LINUX,
|
||||
effectScope: { type: 'global' },
|
||||
entries: [
|
||||
createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'),
|
||||
createConditionEntry(ConditionEntryField.PATH, '/bin/malware'),
|
||||
|
@ -99,20 +89,61 @@ const TRUSTED_APP = {
|
|||
};
|
||||
|
||||
describe('handlers', () => {
|
||||
const appContextMock = createAppContextMock();
|
||||
const createAppContextMock = () => {
|
||||
const context = {
|
||||
logFactory: loggingSystemMock.create(),
|
||||
service: new EndpointAppContextService(),
|
||||
config: () => Promise.resolve(createMockConfig()),
|
||||
experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental),
|
||||
};
|
||||
|
||||
// Ensure that `logFactory.get()` always returns the same instance for the same given prefix
|
||||
const instances = new Map<string, ReturnType<typeof context.logFactory.get>>();
|
||||
const logFactoryGetMock = context.logFactory.get.getMockImplementation();
|
||||
context.logFactory.get.mockImplementation(
|
||||
(prefix): Logger => {
|
||||
if (!instances.has(prefix)) {
|
||||
instances.set(prefix, logFactoryGetMock!(prefix)!);
|
||||
}
|
||||
return instances.get(prefix)!;
|
||||
}
|
||||
);
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
let appContextMock: ReturnType<typeof createAppContextMock> = createAppContextMock();
|
||||
let exceptionsListClient: jest.Mocked<ExceptionListClient> = listMock.getExceptionListClient() as jest.Mocked<ExceptionListClient>;
|
||||
|
||||
const createHandlerContextMock = () =>
|
||||
(({
|
||||
...xpackMocks.createRequestHandlerContext(),
|
||||
lists: {
|
||||
getListClient: jest.fn(),
|
||||
getExceptionListClient: jest.fn().mockReturnValue(exceptionsListClient),
|
||||
},
|
||||
} as unknown) as jest.Mocked<SecuritySolutionRequestHandlerContext>);
|
||||
|
||||
const assertResponse = <T>(
|
||||
response: jest.Mocked<KibanaResponseFactory>,
|
||||
expectedResponseType: keyof KibanaResponseFactory,
|
||||
expectedResponseBody: T
|
||||
) => {
|
||||
expect(response[expectedResponseType]).toBeCalled();
|
||||
expect(response[expectedResponseType].mock.calls[0][0]?.body).toEqual(expectedResponseBody);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
exceptionsListClient.deleteExceptionListItem.mockReset();
|
||||
exceptionsListClient.createExceptionListItem.mockReset();
|
||||
exceptionsListClient.findExceptionListItem.mockReset();
|
||||
exceptionsListClient.createTrustedAppsList.mockReset();
|
||||
|
||||
appContextMock.logFactory.get.mockClear();
|
||||
(appContextMock.logFactory.get().error as jest.Mock).mockClear();
|
||||
appContextMock = createAppContextMock();
|
||||
exceptionsListClient = listMock.getExceptionListClient() as jest.Mocked<ExceptionListClient>;
|
||||
});
|
||||
|
||||
describe('getTrustedAppsDeleteRouteHandler', () => {
|
||||
const deleteTrustedAppHandler = getTrustedAppsDeleteRouteHandler();
|
||||
let deleteTrustedAppHandler: ReturnType<typeof getTrustedAppsDeleteRouteHandler>;
|
||||
|
||||
beforeEach(() => {
|
||||
deleteTrustedAppHandler = getTrustedAppsDeleteRouteHandler(appContextMock);
|
||||
});
|
||||
|
||||
it('should return ok when trusted app deleted', async () => {
|
||||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
|
@ -131,13 +162,15 @@ describe('handlers', () => {
|
|||
it('should return notFound when trusted app missing', async () => {
|
||||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
|
||||
exceptionsListClient.deleteExceptionListItem.mockResolvedValue(null);
|
||||
|
||||
await deleteTrustedAppHandler(
|
||||
createHandlerContextMock(),
|
||||
httpServerMock.createKibanaRequest({ params: { id: '123' } }),
|
||||
mockResponse
|
||||
);
|
||||
|
||||
assertResponse(mockResponse, 'notFound', 'trusted app id [123] not found');
|
||||
assertResponse(mockResponse, 'notFound', new TrustedAppNotFoundError('123'));
|
||||
});
|
||||
|
||||
it('should return internalError when errors happen', async () => {
|
||||
|
@ -157,7 +190,11 @@ describe('handlers', () => {
|
|||
});
|
||||
|
||||
describe('getTrustedAppsCreateRouteHandler', () => {
|
||||
const createTrustedAppHandler = getTrustedAppsCreateRouteHandler();
|
||||
let createTrustedAppHandler: ReturnType<typeof getTrustedAppsCreateRouteHandler>;
|
||||
|
||||
beforeEach(() => {
|
||||
createTrustedAppHandler = getTrustedAppsCreateRouteHandler(appContextMock);
|
||||
});
|
||||
|
||||
it('should return ok with body when trusted app created', async () => {
|
||||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
|
@ -190,7 +227,11 @@ describe('handlers', () => {
|
|||
});
|
||||
|
||||
describe('getTrustedAppsListRouteHandler', () => {
|
||||
const getTrustedAppsListHandler = getTrustedAppsListRouteHandler();
|
||||
let getTrustedAppsListHandler: ReturnType<typeof getTrustedAppsListRouteHandler>;
|
||||
|
||||
beforeEach(() => {
|
||||
getTrustedAppsListHandler = getTrustedAppsListRouteHandler(appContextMock);
|
||||
});
|
||||
|
||||
it('should return ok with list when no errors', async () => {
|
||||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
|
@ -204,7 +245,7 @@ describe('handlers', () => {
|
|||
|
||||
await getTrustedAppsListHandler(
|
||||
createHandlerContextMock(),
|
||||
httpServerMock.createKibanaRequest({ params: { page: 1, per_page: 20 } }),
|
||||
httpServerMock.createKibanaRequest({ query: { page: 1, per_page: 20 } }),
|
||||
mockResponse
|
||||
);
|
||||
|
||||
|
@ -230,10 +271,39 @@ describe('handlers', () => {
|
|||
)
|
||||
).rejects.toThrowError(error);
|
||||
});
|
||||
|
||||
it('should pass all params to the service', async () => {
|
||||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
|
||||
exceptionsListClient.findExceptionListItem.mockResolvedValue({
|
||||
data: [EXCEPTION_LIST_ITEM],
|
||||
page: 5,
|
||||
per_page: 13,
|
||||
total: 100,
|
||||
});
|
||||
|
||||
const requestContext = createHandlerContextMock();
|
||||
|
||||
await getTrustedAppsListHandler(
|
||||
requestContext,
|
||||
httpServerMock.createKibanaRequest({
|
||||
query: { page: 5, per_page: 13, kuery: 'some-param.key: value' },
|
||||
}),
|
||||
mockResponse
|
||||
);
|
||||
|
||||
expect(exceptionsListClient.findExceptionListItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ filter: 'some-param.key: value', page: 5, perPage: 13 })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTrustedAppsSummaryHandler', () => {
|
||||
const getTrustedAppsSummaryHandler = getTrustedAppsSummaryRouteHandler();
|
||||
let getTrustedAppsSummaryHandler: ReturnType<typeof getTrustedAppsSummaryRouteHandler>;
|
||||
|
||||
beforeEach(() => {
|
||||
getTrustedAppsSummaryHandler = getTrustedAppsSummaryRouteHandler(appContextMock);
|
||||
});
|
||||
|
||||
it('should return ok with list when no errors', async () => {
|
||||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
|
@ -296,4 +366,147 @@ describe('handlers', () => {
|
|||
).rejects.toThrowError(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTrustedAppsGetOneHandler', () => {
|
||||
let getOneHandler: ReturnType<typeof getTrustedAppsGetOneHandler>;
|
||||
|
||||
beforeEach(() => {
|
||||
getOneHandler = getTrustedAppsGetOneHandler(appContextMock);
|
||||
});
|
||||
|
||||
it('should return single trusted app', async () => {
|
||||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
|
||||
exceptionsListClient.getExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM);
|
||||
|
||||
await getOneHandler(
|
||||
createHandlerContextMock(),
|
||||
httpServerMock.createKibanaRequest({ query: { page: 1, per_page: 20 } }),
|
||||
mockResponse
|
||||
);
|
||||
|
||||
assertResponse(mockResponse, 'ok', {
|
||||
data: TRUSTED_APP,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 if trusted app does not exist', async () => {
|
||||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
|
||||
exceptionsListClient.getExceptionListItem.mockResolvedValue(null);
|
||||
|
||||
await getOneHandler(
|
||||
createHandlerContextMock(),
|
||||
httpServerMock.createKibanaRequest({ query: { page: 1, per_page: 20 } }),
|
||||
mockResponse
|
||||
);
|
||||
|
||||
assertResponse(mockResponse, 'notFound', expect.any(TrustedAppNotFoundError));
|
||||
});
|
||||
|
||||
it.each([
|
||||
[new TrustedAppNotFoundError('123')],
|
||||
[new TrustedAppVersionConflictError('123', new Error('some conflict error'))],
|
||||
])('should log error: %s', async (error) => {
|
||||
const mockResponse = httpServerMock.createResponseFactory();
|
||||
exceptionsListClient.getExceptionListItem.mockImplementation(async () => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
await getOneHandler(
|
||||
createHandlerContextMock(),
|
||||
httpServerMock.createKibanaRequest({ query: { page: 1, per_page: 20 } }),
|
||||
mockResponse
|
||||
);
|
||||
|
||||
expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTrustedAppsUpdateRouteHandler', () => {
|
||||
let updateHandler: ReturnType<typeof getTrustedAppsUpdateRouteHandler>;
|
||||
let mockResponse: ReturnType<typeof httpServerMock.createResponseFactory>;
|
||||
|
||||
beforeEach(() => {
|
||||
updateHandler = getTrustedAppsUpdateRouteHandler(appContextMock);
|
||||
mockResponse = httpServerMock.createResponseFactory();
|
||||
});
|
||||
|
||||
it('should return success with updated trusted app', async () => {
|
||||
exceptionsListClient.getExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM);
|
||||
exceptionsListClient.updateExceptionListItem.mockImplementationOnce(
|
||||
updateExceptionListItemImplementationMock
|
||||
);
|
||||
|
||||
await updateHandler(
|
||||
createHandlerContextMock(),
|
||||
httpServerMock.createKibanaRequest({ params: { id: '123' }, body: NEW_TRUSTED_APP }),
|
||||
mockResponse
|
||||
);
|
||||
|
||||
expect(mockResponse.ok).toHaveBeenCalledWith({
|
||||
body: {
|
||||
data: {
|
||||
created_at: '11/11/2011T11:11:11.111',
|
||||
created_by: 'admin',
|
||||
updated_at: '11/11/2011T11:11:11.111',
|
||||
updated_by: 'admin',
|
||||
description: 'Linux trusted app 1',
|
||||
effectScope: {
|
||||
type: 'global',
|
||||
},
|
||||
entries: [
|
||||
{
|
||||
field: 'process.hash.*',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: '1234234659af249ddf3e40864e9fb241',
|
||||
},
|
||||
{
|
||||
field: 'process.executable.caseless',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: '/bin/malware',
|
||||
},
|
||||
],
|
||||
id: '123',
|
||||
name: 'linux trusted app 1',
|
||||
os: 'linux',
|
||||
version: 'abc123',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 if trusted app does not exist', async () => {
|
||||
exceptionsListClient.getExceptionListItem.mockResolvedValueOnce(null);
|
||||
|
||||
await updateHandler(
|
||||
createHandlerContextMock(),
|
||||
httpServerMock.createKibanaRequest({ params: { id: '123' }, body: NEW_TRUSTED_APP }),
|
||||
mockResponse
|
||||
);
|
||||
|
||||
expect(mockResponse.notFound).toHaveBeenCalledWith({
|
||||
body: expect.any(TrustedAppNotFoundError),
|
||||
});
|
||||
});
|
||||
|
||||
it('should should return 409 if version conflict occurs', async () => {
|
||||
exceptionsListClient.getExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM);
|
||||
exceptionsListClient.updateExceptionListItem.mockRejectedValue(
|
||||
Object.assign(new Error(), { output: { statusCode: 409 } })
|
||||
);
|
||||
|
||||
await updateHandler(
|
||||
createHandlerContextMock(),
|
||||
httpServerMock.createKibanaRequest({ params: { id: '123' }, body: NEW_TRUSTED_APP }),
|
||||
mockResponse
|
||||
);
|
||||
|
||||
expect(mockResponse.conflict).toHaveBeenCalledWith({
|
||||
body: expect.any(TrustedAppVersionConflictError),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,24 +5,42 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { RequestHandler } from 'kibana/server';
|
||||
import type { KibanaResponseFactory, RequestHandler, IKibanaResponse, Logger } from 'kibana/server';
|
||||
import type { SecuritySolutionRequestHandlerContext } from '../../../types';
|
||||
|
||||
import { ExceptionListClient } from '../../../../../lists/server';
|
||||
|
||||
import {
|
||||
DeleteTrustedAppsRequestParams,
|
||||
GetOneTrustedAppRequestParams,
|
||||
GetTrustedAppsListRequest,
|
||||
PostTrustedAppCreateRequest,
|
||||
PutTrustedAppsRequestParams,
|
||||
PutTrustedAppUpdateRequest,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import { EndpointAppContext } from '../../types';
|
||||
|
||||
import {
|
||||
createTrustedApp,
|
||||
deleteTrustedApp,
|
||||
getTrustedApp,
|
||||
getTrustedAppsList,
|
||||
getTrustedAppsSummary,
|
||||
MissingTrustedAppException,
|
||||
updateTrustedApp,
|
||||
} from './service';
|
||||
import { TrustedAppNotFoundError, TrustedAppVersionConflictError } from './errors';
|
||||
|
||||
const getBodyAfterFeatureFlagCheck = (
|
||||
body: PutTrustedAppUpdateRequest | PostTrustedAppCreateRequest,
|
||||
endpointAppContext: EndpointAppContext
|
||||
): PutTrustedAppUpdateRequest | PostTrustedAppCreateRequest => {
|
||||
const isTrustedAppsByPolicyEnabled =
|
||||
endpointAppContext.experimentalFeatures.trustedAppsByPolicyEnabled;
|
||||
return {
|
||||
...body,
|
||||
...(isTrustedAppsByPolicyEnabled ? body.effectScope : { effectSctope: { type: 'policy:all' } }),
|
||||
};
|
||||
};
|
||||
|
||||
const exceptionListClientFromContext = (
|
||||
context: SecuritySolutionRequestHandlerContext
|
||||
|
@ -36,62 +54,145 @@ const exceptionListClientFromContext = (
|
|||
return exceptionLists;
|
||||
};
|
||||
|
||||
export const getTrustedAppsDeleteRouteHandler = (): RequestHandler<
|
||||
const errorHandler = <E extends Error>(
|
||||
logger: Logger,
|
||||
res: KibanaResponseFactory,
|
||||
error: E
|
||||
): IKibanaResponse => {
|
||||
if (error instanceof TrustedAppNotFoundError) {
|
||||
logger.error(error);
|
||||
return res.notFound({ body: error });
|
||||
}
|
||||
|
||||
if (error instanceof TrustedAppVersionConflictError) {
|
||||
logger.error(error);
|
||||
return res.conflict({ body: error });
|
||||
}
|
||||
|
||||
// Kibana will take care of `500` errors when the handler `throw`'s, including logging the error
|
||||
throw error;
|
||||
};
|
||||
|
||||
export const getTrustedAppsDeleteRouteHandler = (
|
||||
endpointAppContext: EndpointAppContext
|
||||
): RequestHandler<
|
||||
DeleteTrustedAppsRequestParams,
|
||||
unknown,
|
||||
unknown,
|
||||
SecuritySolutionRequestHandlerContext
|
||||
> => {
|
||||
const logger = endpointAppContext.logFactory.get('trusted_apps');
|
||||
|
||||
return async (context, req, res) => {
|
||||
try {
|
||||
await deleteTrustedApp(exceptionListClientFromContext(context), req.params);
|
||||
|
||||
return res.ok();
|
||||
} catch (error) {
|
||||
if (error instanceof MissingTrustedAppException) {
|
||||
return res.notFound({ body: `trusted app id [${req.params.id}] not found` });
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
return errorHandler(logger, res, error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const getTrustedAppsListRouteHandler = (): RequestHandler<
|
||||
export const getTrustedAppsGetOneHandler = (
|
||||
endpointAppContext: EndpointAppContext
|
||||
): RequestHandler<
|
||||
GetOneTrustedAppRequestParams,
|
||||
unknown,
|
||||
unknown,
|
||||
SecuritySolutionRequestHandlerContext
|
||||
> => {
|
||||
const logger = endpointAppContext.logFactory.get('trusted_apps');
|
||||
|
||||
return async (context, req, res) => {
|
||||
try {
|
||||
return res.ok({
|
||||
body: await getTrustedApp(exceptionListClientFromContext(context), req.params.id),
|
||||
});
|
||||
} catch (error) {
|
||||
return errorHandler(logger, res, error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const getTrustedAppsListRouteHandler = (
|
||||
endpointAppContext: EndpointAppContext
|
||||
): RequestHandler<
|
||||
unknown,
|
||||
GetTrustedAppsListRequest,
|
||||
unknown,
|
||||
SecuritySolutionRequestHandlerContext
|
||||
> => {
|
||||
const logger = endpointAppContext.logFactory.get('trusted_apps');
|
||||
|
||||
return async (context, req, res) => {
|
||||
return res.ok({
|
||||
body: await getTrustedAppsList(exceptionListClientFromContext(context), req.query),
|
||||
});
|
||||
try {
|
||||
return res.ok({
|
||||
body: await getTrustedAppsList(exceptionListClientFromContext(context), req.query),
|
||||
});
|
||||
} catch (error) {
|
||||
return errorHandler(logger, res, error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const getTrustedAppsCreateRouteHandler = (): RequestHandler<
|
||||
export const getTrustedAppsCreateRouteHandler = (
|
||||
endpointAppContext: EndpointAppContext
|
||||
): RequestHandler<
|
||||
unknown,
|
||||
unknown,
|
||||
PostTrustedAppCreateRequest,
|
||||
SecuritySolutionRequestHandlerContext
|
||||
> => {
|
||||
const logger = endpointAppContext.logFactory.get('trusted_apps');
|
||||
return async (context, req, res) => {
|
||||
return res.ok({
|
||||
body: await createTrustedApp(exceptionListClientFromContext(context), req.body),
|
||||
});
|
||||
try {
|
||||
const body = getBodyAfterFeatureFlagCheck(req.body, endpointAppContext);
|
||||
|
||||
return res.ok({
|
||||
body: await createTrustedApp(exceptionListClientFromContext(context), body),
|
||||
});
|
||||
} catch (error) {
|
||||
return errorHandler(logger, res, error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const getTrustedAppsSummaryRouteHandler = (): RequestHandler<
|
||||
export const getTrustedAppsUpdateRouteHandler = (
|
||||
endpointAppContext: EndpointAppContext
|
||||
): RequestHandler<
|
||||
PutTrustedAppsRequestParams,
|
||||
unknown,
|
||||
unknown,
|
||||
PostTrustedAppCreateRequest,
|
||||
PutTrustedAppUpdateRequest,
|
||||
SecuritySolutionRequestHandlerContext
|
||||
> => {
|
||||
const logger = endpointAppContext.logFactory.get('trusted_apps');
|
||||
|
||||
return async (context, req, res) => {
|
||||
return res.ok({
|
||||
body: await getTrustedAppsSummary(exceptionListClientFromContext(context)),
|
||||
});
|
||||
try {
|
||||
const body = getBodyAfterFeatureFlagCheck(req.body, endpointAppContext);
|
||||
|
||||
return res.ok({
|
||||
body: await updateTrustedApp(exceptionListClientFromContext(context), req.params.id, body),
|
||||
});
|
||||
} catch (error) {
|
||||
return errorHandler(logger, res, error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const getTrustedAppsSummaryRouteHandler = (
|
||||
endpointAppContext: EndpointAppContext
|
||||
): RequestHandler<unknown, unknown, unknown, SecuritySolutionRequestHandlerContext> => {
|
||||
const logger = endpointAppContext.logFactory.get('trusted_apps');
|
||||
|
||||
return async (context, req, res) => {
|
||||
try {
|
||||
return res.ok({
|
||||
body: await getTrustedAppsSummary(exceptionListClientFromContext(context)),
|
||||
});
|
||||
} catch (error) {
|
||||
return errorHandler(logger, res, error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -7,24 +7,35 @@
|
|||
|
||||
import {
|
||||
DeleteTrustedAppsRequestSchema,
|
||||
GetOneTrustedAppRequestSchema,
|
||||
GetTrustedAppsRequestSchema,
|
||||
PostTrustedAppCreateRequestSchema,
|
||||
PutTrustedAppUpdateRequestSchema,
|
||||
} from '../../../../common/endpoint/schema/trusted_apps';
|
||||
import {
|
||||
TRUSTED_APPS_CREATE_API,
|
||||
TRUSTED_APPS_DELETE_API,
|
||||
TRUSTED_APPS_GET_API,
|
||||
TRUSTED_APPS_LIST_API,
|
||||
TRUSTED_APPS_UPDATE_API,
|
||||
TRUSTED_APPS_SUMMARY_API,
|
||||
} from '../../../../common/endpoint/constants';
|
||||
|
||||
import {
|
||||
getTrustedAppsCreateRouteHandler,
|
||||
getTrustedAppsDeleteRouteHandler,
|
||||
getTrustedAppsGetOneHandler,
|
||||
getTrustedAppsListRouteHandler,
|
||||
getTrustedAppsSummaryRouteHandler,
|
||||
getTrustedAppsUpdateRouteHandler,
|
||||
} from './handlers';
|
||||
import { SecuritySolutionPluginRouter } from '../../../types';
|
||||
import { EndpointAppContext } from '../../types';
|
||||
|
||||
export const registerTrustedAppsRoutes = (router: SecuritySolutionPluginRouter) => {
|
||||
export const registerTrustedAppsRoutes = (
|
||||
router: SecuritySolutionPluginRouter,
|
||||
endpointAppContext: EndpointAppContext
|
||||
) => {
|
||||
// DELETE one
|
||||
router.delete(
|
||||
{
|
||||
|
@ -32,7 +43,17 @@ export const registerTrustedAppsRoutes = (router: SecuritySolutionPluginRouter)
|
|||
validate: DeleteTrustedAppsRequestSchema,
|
||||
options: { authRequired: true },
|
||||
},
|
||||
getTrustedAppsDeleteRouteHandler()
|
||||
getTrustedAppsDeleteRouteHandler(endpointAppContext)
|
||||
);
|
||||
|
||||
// GET one
|
||||
router.get(
|
||||
{
|
||||
path: TRUSTED_APPS_GET_API,
|
||||
validate: GetOneTrustedAppRequestSchema,
|
||||
options: { authRequired: true },
|
||||
},
|
||||
getTrustedAppsGetOneHandler(endpointAppContext)
|
||||
);
|
||||
|
||||
// GET list
|
||||
|
@ -42,7 +63,7 @@ export const registerTrustedAppsRoutes = (router: SecuritySolutionPluginRouter)
|
|||
validate: GetTrustedAppsRequestSchema,
|
||||
options: { authRequired: true },
|
||||
},
|
||||
getTrustedAppsListRouteHandler()
|
||||
getTrustedAppsListRouteHandler(endpointAppContext)
|
||||
);
|
||||
|
||||
// CREATE
|
||||
|
@ -52,7 +73,17 @@ export const registerTrustedAppsRoutes = (router: SecuritySolutionPluginRouter)
|
|||
validate: PostTrustedAppCreateRequestSchema,
|
||||
options: { authRequired: true },
|
||||
},
|
||||
getTrustedAppsCreateRouteHandler()
|
||||
getTrustedAppsCreateRouteHandler(endpointAppContext)
|
||||
);
|
||||
|
||||
// PUT
|
||||
router.put(
|
||||
{
|
||||
path: TRUSTED_APPS_UPDATE_API,
|
||||
validate: PutTrustedAppUpdateRequestSchema,
|
||||
options: { authRequired: true },
|
||||
},
|
||||
getTrustedAppsUpdateRouteHandler(endpointAppContext)
|
||||
);
|
||||
|
||||
// SUMMARY
|
||||
|
@ -62,6 +93,6 @@ export const registerTrustedAppsRoutes = (router: SecuritySolutionPluginRouter)
|
|||
validate: false,
|
||||
options: { authRequired: true },
|
||||
},
|
||||
getTrustedAppsSummaryRouteHandler()
|
||||
getTrustedAppsSummaryRouteHandler(endpointAppContext)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
NewTrustedApp,
|
||||
OperatingSystem,
|
||||
TrustedApp,
|
||||
UpdateTrustedApp,
|
||||
} from '../../../../common/endpoint/types';
|
||||
|
||||
import {
|
||||
|
@ -21,6 +22,7 @@ import {
|
|||
createEntryNested,
|
||||
exceptionListItemToTrustedApp,
|
||||
newTrustedAppToCreateExceptionListItemOptions,
|
||||
updatedTrustedAppToUpdateExceptionListItemOptions,
|
||||
} from './mapping';
|
||||
|
||||
const createExceptionListItemOptions = (
|
||||
|
@ -43,7 +45,7 @@ const createExceptionListItemOptions = (
|
|||
const exceptionListItemSchema = (
|
||||
item: Partial<ExceptionListItemSchema>
|
||||
): ExceptionListItemSchema => ({
|
||||
_version: '123',
|
||||
_version: 'abc123',
|
||||
id: '',
|
||||
comments: [],
|
||||
created_at: '',
|
||||
|
@ -75,6 +77,7 @@ describe('mapping', () => {
|
|||
{
|
||||
name: 'linux trusted app',
|
||||
description: 'Linux Trusted App',
|
||||
effectScope: { type: 'global' },
|
||||
os: OperatingSystem.LINUX,
|
||||
entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')],
|
||||
},
|
||||
|
@ -92,6 +95,7 @@ describe('mapping', () => {
|
|||
{
|
||||
name: 'macos trusted app',
|
||||
description: 'MacOS Trusted App',
|
||||
effectScope: { type: 'global' },
|
||||
os: OperatingSystem.MAC,
|
||||
entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')],
|
||||
},
|
||||
|
@ -109,6 +113,7 @@ describe('mapping', () => {
|
|||
{
|
||||
name: 'windows trusted app',
|
||||
description: 'Windows Trusted App',
|
||||
effectScope: { type: 'global' },
|
||||
os: OperatingSystem.WINDOWS,
|
||||
entries: [createConditionEntry(ConditionEntryField.PATH, 'C:\\Program Files\\Malware')],
|
||||
},
|
||||
|
@ -126,6 +131,7 @@ describe('mapping', () => {
|
|||
{
|
||||
name: 'Signed trusted app',
|
||||
description: 'Signed Trusted App',
|
||||
effectScope: { type: 'global' },
|
||||
os: OperatingSystem.WINDOWS,
|
||||
entries: [createConditionEntry(ConditionEntryField.SIGNER, 'Microsoft Windows')],
|
||||
},
|
||||
|
@ -148,6 +154,7 @@ describe('mapping', () => {
|
|||
{
|
||||
name: 'MD5 trusted app',
|
||||
description: 'MD5 Trusted App',
|
||||
effectScope: { type: 'global' },
|
||||
os: OperatingSystem.LINUX,
|
||||
entries: [
|
||||
createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'),
|
||||
|
@ -167,6 +174,7 @@ describe('mapping', () => {
|
|||
{
|
||||
name: 'SHA1 trusted app',
|
||||
description: 'SHA1 Trusted App',
|
||||
effectScope: { type: 'global' },
|
||||
os: OperatingSystem.LINUX,
|
||||
entries: [
|
||||
createConditionEntry(
|
||||
|
@ -191,6 +199,7 @@ describe('mapping', () => {
|
|||
{
|
||||
name: 'SHA256 trusted app',
|
||||
description: 'SHA256 Trusted App',
|
||||
effectScope: { type: 'global' },
|
||||
os: OperatingSystem.LINUX,
|
||||
entries: [
|
||||
createConditionEntry(
|
||||
|
@ -218,6 +227,7 @@ describe('mapping', () => {
|
|||
{
|
||||
name: 'MD5 trusted app',
|
||||
description: 'MD5 Trusted App',
|
||||
effectScope: { type: 'global' },
|
||||
os: OperatingSystem.LINUX,
|
||||
entries: [
|
||||
createConditionEntry(ConditionEntryField.HASH, '1234234659Af249ddf3e40864E9FB241'),
|
||||
|
@ -251,10 +261,14 @@ describe('mapping', () => {
|
|||
}),
|
||||
{
|
||||
id: '123',
|
||||
version: 'abc123',
|
||||
name: 'linux trusted app',
|
||||
description: 'Linux Trusted App',
|
||||
effectScope: { type: 'global' },
|
||||
created_at: '11/11/2011T11:11:11.111',
|
||||
created_by: 'admin',
|
||||
updated_at: '11/11/2011T11:11:11.111',
|
||||
updated_by: 'admin',
|
||||
os: OperatingSystem.LINUX,
|
||||
entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')],
|
||||
}
|
||||
|
@ -274,10 +288,14 @@ describe('mapping', () => {
|
|||
}),
|
||||
{
|
||||
id: '123',
|
||||
version: 'abc123',
|
||||
name: 'macos trusted app',
|
||||
description: 'MacOS Trusted App',
|
||||
effectScope: { type: 'global' },
|
||||
created_at: '11/11/2011T11:11:11.111',
|
||||
created_by: 'admin',
|
||||
updated_at: '11/11/2011T11:11:11.111',
|
||||
updated_by: 'admin',
|
||||
os: OperatingSystem.MAC,
|
||||
entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')],
|
||||
}
|
||||
|
@ -297,10 +315,14 @@ describe('mapping', () => {
|
|||
}),
|
||||
{
|
||||
id: '123',
|
||||
created_at: '11/11/2011T11:11:11.111',
|
||||
created_by: 'admin',
|
||||
version: 'abc123',
|
||||
name: 'windows trusted app',
|
||||
description: 'Windows Trusted App',
|
||||
effectScope: { type: 'global' },
|
||||
created_at: '11/11/2011T11:11:11.111',
|
||||
created_by: 'admin',
|
||||
updated_at: '11/11/2011T11:11:11.111',
|
||||
updated_by: 'admin',
|
||||
os: OperatingSystem.WINDOWS,
|
||||
entries: [createConditionEntry(ConditionEntryField.PATH, 'C:\\Program Files\\Malware')],
|
||||
}
|
||||
|
@ -325,10 +347,14 @@ describe('mapping', () => {
|
|||
}),
|
||||
{
|
||||
id: '123',
|
||||
version: 'abc123',
|
||||
name: 'signed trusted app',
|
||||
description: 'Signed trusted app',
|
||||
effectScope: { type: 'global' },
|
||||
created_at: '11/11/2011T11:11:11.111',
|
||||
created_by: 'admin',
|
||||
updated_at: '11/11/2011T11:11:11.111',
|
||||
updated_by: 'admin',
|
||||
os: OperatingSystem.WINDOWS,
|
||||
entries: [createConditionEntry(ConditionEntryField.SIGNER, 'Microsoft Windows')],
|
||||
}
|
||||
|
@ -348,10 +374,14 @@ describe('mapping', () => {
|
|||
}),
|
||||
{
|
||||
id: '123',
|
||||
version: 'abc123',
|
||||
name: 'MD5 trusted app',
|
||||
description: 'MD5 Trusted App',
|
||||
effectScope: { type: 'global' },
|
||||
created_at: '11/11/2011T11:11:11.111',
|
||||
created_by: 'admin',
|
||||
updated_at: '11/11/2011T11:11:11.111',
|
||||
updated_by: 'admin',
|
||||
os: OperatingSystem.LINUX,
|
||||
entries: [
|
||||
createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'),
|
||||
|
@ -375,10 +405,14 @@ describe('mapping', () => {
|
|||
}),
|
||||
{
|
||||
id: '123',
|
||||
version: 'abc123',
|
||||
name: 'SHA1 trusted app',
|
||||
description: 'SHA1 Trusted App',
|
||||
effectScope: { type: 'global' },
|
||||
created_at: '11/11/2011T11:11:11.111',
|
||||
created_by: 'admin',
|
||||
updated_at: '11/11/2011T11:11:11.111',
|
||||
updated_by: 'admin',
|
||||
os: OperatingSystem.LINUX,
|
||||
entries: [
|
||||
createConditionEntry(
|
||||
|
@ -408,10 +442,14 @@ describe('mapping', () => {
|
|||
}),
|
||||
{
|
||||
id: '123',
|
||||
version: 'abc123',
|
||||
name: 'SHA256 trusted app',
|
||||
description: 'SHA256 Trusted App',
|
||||
effectScope: { type: 'global' },
|
||||
created_at: '11/11/2011T11:11:11.111',
|
||||
created_by: 'admin',
|
||||
updated_at: '11/11/2011T11:11:11.111',
|
||||
updated_by: 'admin',
|
||||
os: OperatingSystem.LINUX,
|
||||
entries: [
|
||||
createConditionEntry(
|
||||
|
@ -423,4 +461,43 @@ describe('mapping', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatedTrustedAppToUpdateExceptionListItemOptions', () => {
|
||||
it('should map to UpdateExceptionListItemOptions', () => {
|
||||
const updatedTrustedApp: UpdateTrustedApp = {
|
||||
name: 'Linux trusted app',
|
||||
description: 'Linux Trusted App',
|
||||
effectScope: { type: 'global' },
|
||||
os: OperatingSystem.LINUX,
|
||||
entries: [createConditionEntry(ConditionEntryField.PATH, '/bin/malware')],
|
||||
version: 'abc',
|
||||
};
|
||||
|
||||
expect(
|
||||
updatedTrustedAppToUpdateExceptionListItemOptions(
|
||||
exceptionListItemSchema({ id: 'original-id-here', item_id: 'original-item-id-here' }),
|
||||
updatedTrustedApp
|
||||
)
|
||||
).toEqual({
|
||||
_version: 'abc',
|
||||
comments: [],
|
||||
description: 'Linux Trusted App',
|
||||
entries: [
|
||||
{
|
||||
field: 'process.executable.caseless',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: '/bin/malware',
|
||||
},
|
||||
],
|
||||
id: 'original-id-here',
|
||||
itemId: 'original-item-id-here',
|
||||
name: 'Linux trusted app',
|
||||
namespaceType: 'agnostic',
|
||||
osTypes: ['linux'],
|
||||
tags: ['policy:all'],
|
||||
type: 'simple',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,22 +7,27 @@
|
|||
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { OsType } from '../../../../../lists/common/schemas/common';
|
||||
import { OsType } from '../../../../../lists/common/schemas';
|
||||
import {
|
||||
EntriesArray,
|
||||
EntryMatch,
|
||||
EntryNested,
|
||||
ExceptionListItemSchema,
|
||||
NestedEntriesArray,
|
||||
} from '../../../../../lists/common/shared_exports';
|
||||
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common';
|
||||
import { CreateExceptionListItemOptions } from '../../../../../lists/server';
|
||||
} from '../../../../../lists/common';
|
||||
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants';
|
||||
import {
|
||||
CreateExceptionListItemOptions,
|
||||
UpdateExceptionListItemOptions,
|
||||
} from '../../../../../lists/server';
|
||||
import {
|
||||
ConditionEntry,
|
||||
ConditionEntryField,
|
||||
EffectScope,
|
||||
NewTrustedApp,
|
||||
OperatingSystem,
|
||||
TrustedApp,
|
||||
UpdateTrustedApp,
|
||||
} from '../../../../common/endpoint/types';
|
||||
|
||||
type ConditionEntriesMap = { [K in ConditionEntryField]?: ConditionEntry<K> };
|
||||
|
@ -40,6 +45,8 @@ const OPERATING_SYSTEM_TO_OS_TYPE: Mapping<OperatingSystem, OsType> = {
|
|||
[OperatingSystem.WINDOWS]: 'windows',
|
||||
};
|
||||
|
||||
const POLICY_REFERENCE_PREFIX = 'policy:';
|
||||
|
||||
const filterUndefined = <T>(list: Array<T | undefined>): T[] => {
|
||||
return list.filter((item: T | undefined): item is T => item !== undefined);
|
||||
};
|
||||
|
@ -51,6 +58,21 @@ export const createConditionEntry = <T extends ConditionEntryField>(
|
|||
return { field, value, type: 'match', operator: 'included' };
|
||||
};
|
||||
|
||||
export const tagsToEffectScope = (tags: string[]): EffectScope => {
|
||||
const policyReferenceTags = tags.filter((tag) => tag.startsWith(POLICY_REFERENCE_PREFIX));
|
||||
|
||||
if (policyReferenceTags.some((tag) => tag === `${POLICY_REFERENCE_PREFIX}all`)) {
|
||||
return {
|
||||
type: 'global',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'policy',
|
||||
policies: policyReferenceTags.map((tag) => tag.substr(POLICY_REFERENCE_PREFIX.length)),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const entriesToConditionEntriesMap = (entries: EntriesArray): ConditionEntriesMap => {
|
||||
return entries.reduce((result, entry) => {
|
||||
if (entry.field.startsWith('process.hash') && entry.type === 'match') {
|
||||
|
@ -96,10 +118,14 @@ export const exceptionListItemToTrustedApp = (
|
|||
|
||||
return {
|
||||
id: exceptionListItem.id,
|
||||
version: exceptionListItem._version || '',
|
||||
name: exceptionListItem.name,
|
||||
description: exceptionListItem.description,
|
||||
effectScope: tagsToEffectScope(exceptionListItem.tags),
|
||||
created_at: exceptionListItem.created_at,
|
||||
created_by: exceptionListItem.created_by,
|
||||
updated_at: exceptionListItem.updated_at,
|
||||
updated_by: exceptionListItem.updated_by,
|
||||
...(os === OperatingSystem.LINUX || os === OperatingSystem.MAC
|
||||
? {
|
||||
os,
|
||||
|
@ -147,6 +173,14 @@ export const createEntryNested = (field: string, entries: NestedEntriesArray): E
|
|||
return { field, entries, type: 'nested' };
|
||||
};
|
||||
|
||||
export const effectScopeToTags = (effectScope: EffectScope) => {
|
||||
if (effectScope.type === 'policy') {
|
||||
return effectScope.policies.map((policy) => `${POLICY_REFERENCE_PREFIX}${policy}`);
|
||||
} else {
|
||||
return [`${POLICY_REFERENCE_PREFIX}all`];
|
||||
}
|
||||
};
|
||||
|
||||
export const conditionEntriesToEntries = (conditionEntries: ConditionEntry[]): EntriesArray => {
|
||||
return conditionEntries.map((conditionEntry) => {
|
||||
if (conditionEntry.field === ConditionEntryField.HASH) {
|
||||
|
@ -173,6 +207,7 @@ export const newTrustedAppToCreateExceptionListItemOptions = ({
|
|||
entries,
|
||||
name,
|
||||
description = '',
|
||||
effectScope,
|
||||
}: NewTrustedApp): CreateExceptionListItemOptions => {
|
||||
return {
|
||||
comments: [],
|
||||
|
@ -184,7 +219,42 @@ export const newTrustedAppToCreateExceptionListItemOptions = ({
|
|||
name,
|
||||
namespaceType: 'agnostic',
|
||||
osTypes: [OPERATING_SYSTEM_TO_OS_TYPE[os]],
|
||||
tags: ['policy:all'],
|
||||
tags: effectScopeToTags(effectScope),
|
||||
type: 'simple',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Map UpdateTrustedApp to UpdateExceptionListItemOptions
|
||||
*
|
||||
* @param {ExceptionListItemSchema} currentTrustedAppExceptionItem
|
||||
* @param {UpdateTrustedApp} updatedTrustedApp
|
||||
*/
|
||||
export const updatedTrustedAppToUpdateExceptionListItemOptions = (
|
||||
{
|
||||
id,
|
||||
item_id: itemId,
|
||||
namespace_type: namespaceType,
|
||||
type,
|
||||
comments,
|
||||
meta,
|
||||
}: ExceptionListItemSchema,
|
||||
{ os, entries, name, description = '', effectScope, version }: UpdateTrustedApp
|
||||
): UpdateExceptionListItemOptions => {
|
||||
return {
|
||||
_version: version,
|
||||
name,
|
||||
description,
|
||||
entries: conditionEntriesToEntries(entries),
|
||||
osTypes: [OPERATING_SYSTEM_TO_OS_TYPE[os]],
|
||||
tags: effectScopeToTags(effectScope),
|
||||
|
||||
// Copied from current trusted app exception item
|
||||
id,
|
||||
comments,
|
||||
itemId,
|
||||
meta,
|
||||
namespaceType,
|
||||
type,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,20 +8,29 @@
|
|||
import { ExceptionListItemSchema } from '../../../../../lists/common/schemas/response';
|
||||
import { listMock } from '../../../../../lists/server/mocks';
|
||||
import { ExceptionListClient } from '../../../../../lists/server';
|
||||
import { ConditionEntryField, OperatingSystem } from '../../../../common/endpoint/types';
|
||||
import {
|
||||
ConditionEntryField,
|
||||
OperatingSystem,
|
||||
TrustedApp,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import { createConditionEntry, createEntryMatch } from './mapping';
|
||||
import {
|
||||
createTrustedApp,
|
||||
deleteTrustedApp,
|
||||
getTrustedApp,
|
||||
getTrustedAppsList,
|
||||
getTrustedAppsSummary,
|
||||
MissingTrustedAppException,
|
||||
updateTrustedApp,
|
||||
} from './service';
|
||||
import { TrustedAppNotFoundError, TrustedAppVersionConflictError } from './errors';
|
||||
import { toUpdateTrustedApp } from '../../../../common/endpoint/service/trusted_apps/to_update_trusted_app';
|
||||
import { updateExceptionListItemImplementationMock } from './test_utils';
|
||||
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common';
|
||||
|
||||
const exceptionsListClient = listMock.getExceptionListClient() as jest.Mocked<ExceptionListClient>;
|
||||
|
||||
const EXCEPTION_LIST_ITEM: ExceptionListItemSchema = {
|
||||
_version: '123',
|
||||
_version: 'abc123',
|
||||
id: '123',
|
||||
comments: [],
|
||||
created_at: '11/11/2011T11:11:11.111',
|
||||
|
@ -37,20 +46,24 @@ const EXCEPTION_LIST_ITEM: ExceptionListItemSchema = {
|
|||
name: 'linux trusted app 1',
|
||||
namespace_type: 'agnostic',
|
||||
os_types: ['linux'],
|
||||
tags: [],
|
||||
tags: ['policy:all'],
|
||||
type: 'simple',
|
||||
tie_breaker_id: '123',
|
||||
updated_at: '11/11/2011T11:11:11.111',
|
||||
updated_by: 'admin',
|
||||
};
|
||||
|
||||
const TRUSTED_APP = {
|
||||
const TRUSTED_APP: TrustedApp = {
|
||||
id: '123',
|
||||
version: 'abc123',
|
||||
created_at: '11/11/2011T11:11:11.111',
|
||||
created_by: 'admin',
|
||||
updated_at: '11/11/2011T11:11:11.111',
|
||||
updated_by: 'admin',
|
||||
name: 'linux trusted app 1',
|
||||
description: 'Linux trusted app 1',
|
||||
os: OperatingSystem.LINUX,
|
||||
effectScope: { type: 'global' },
|
||||
entries: [
|
||||
createConditionEntry(ConditionEntryField.HASH, '1234234659af249ddf3e40864e9fb241'),
|
||||
createConditionEntry(ConditionEntryField.PATH, '/bin/malware'),
|
||||
|
@ -81,7 +94,7 @@ describe('service', () => {
|
|||
exceptionsListClient.deleteExceptionListItem.mockResolvedValue(null);
|
||||
|
||||
await expect(deleteTrustedApp(exceptionsListClient, { id: '123' })).rejects.toBeInstanceOf(
|
||||
MissingTrustedAppException
|
||||
TrustedAppNotFoundError
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -93,6 +106,7 @@ describe('service', () => {
|
|||
const result = await createTrustedApp(exceptionsListClient, {
|
||||
name: 'linux trusted app 1',
|
||||
description: 'Linux trusted app 1',
|
||||
effectScope: { type: 'global' },
|
||||
os: OperatingSystem.LINUX,
|
||||
entries: [
|
||||
createConditionEntry(ConditionEntryField.PATH, '/bin/malware'),
|
||||
|
@ -107,20 +121,41 @@ describe('service', () => {
|
|||
});
|
||||
|
||||
describe('getTrustedAppsList', () => {
|
||||
it('should get trusted apps', async () => {
|
||||
beforeEach(() => {
|
||||
exceptionsListClient.findExceptionListItem.mockResolvedValue({
|
||||
data: [EXCEPTION_LIST_ITEM],
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
total: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get trusted apps', async () => {
|
||||
const result = await getTrustedAppsList(exceptionsListClient, { page: 1, per_page: 20 });
|
||||
|
||||
expect(result).toEqual({ data: [TRUSTED_APP], page: 1, per_page: 20, total: 100 });
|
||||
|
||||
expect(exceptionsListClient.createTrustedAppsList).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow KQL to be defined', async () => {
|
||||
const result = await getTrustedAppsList(exceptionsListClient, {
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
kuery: 'some-param.key: value',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ data: [TRUSTED_APP], page: 1, per_page: 20, total: 100 });
|
||||
expect(exceptionsListClient.findExceptionListItem).toHaveBeenCalledWith({
|
||||
listId: ENDPOINT_TRUSTED_APPS_LIST_ID,
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
filter: 'some-param.key: value',
|
||||
namespaceType: 'agnostic',
|
||||
sortField: 'name',
|
||||
sortOrder: 'asc',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTrustedAppsSummary', () => {
|
||||
|
@ -170,4 +205,95 @@ describe('service', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTrustedApp', () => {
|
||||
beforeEach(() => {
|
||||
exceptionsListClient.getExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM);
|
||||
|
||||
exceptionsListClient.updateExceptionListItem.mockImplementationOnce(
|
||||
updateExceptionListItemImplementationMock
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => jest.resetAllMocks());
|
||||
|
||||
it('should update exception item with trusted app data', async () => {
|
||||
const trustedAppForUpdate = toUpdateTrustedApp(TRUSTED_APP);
|
||||
trustedAppForUpdate.name = 'updated name';
|
||||
trustedAppForUpdate.description = 'updated description';
|
||||
trustedAppForUpdate.entries = [trustedAppForUpdate.entries[0]];
|
||||
|
||||
await expect(
|
||||
updateTrustedApp(exceptionsListClient, TRUSTED_APP.id, trustedAppForUpdate)
|
||||
).resolves.toEqual({
|
||||
data: {
|
||||
created_at: '11/11/2011T11:11:11.111',
|
||||
created_by: 'admin',
|
||||
updated_at: '11/11/2011T11:11:11.111',
|
||||
updated_by: 'admin',
|
||||
description: 'updated description',
|
||||
effectScope: {
|
||||
type: 'global',
|
||||
},
|
||||
entries: [
|
||||
{
|
||||
field: 'process.hash.*',
|
||||
operator: 'included',
|
||||
type: 'match',
|
||||
value: '1234234659af249ddf3e40864e9fb241',
|
||||
},
|
||||
],
|
||||
id: '123',
|
||||
name: 'updated name',
|
||||
os: 'linux',
|
||||
version: 'abc123',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw a Not Found error if trusted app is not found prior to making update', async () => {
|
||||
exceptionsListClient.getExceptionListItem.mockResolvedValueOnce(null);
|
||||
await expect(
|
||||
updateTrustedApp(exceptionsListClient, TRUSTED_APP.id, toUpdateTrustedApp(TRUSTED_APP))
|
||||
).rejects.toBeInstanceOf(TrustedAppNotFoundError);
|
||||
});
|
||||
|
||||
it('should throw a Version Conflict error if update fails with 409', async () => {
|
||||
exceptionsListClient.updateExceptionListItem.mockReset();
|
||||
exceptionsListClient.updateExceptionListItem.mockRejectedValueOnce(
|
||||
Object.assign(new Error('conflict'), { output: { statusCode: 409 } })
|
||||
);
|
||||
|
||||
await expect(
|
||||
updateTrustedApp(exceptionsListClient, TRUSTED_APP.id, toUpdateTrustedApp(TRUSTED_APP))
|
||||
).rejects.toBeInstanceOf(TrustedAppVersionConflictError);
|
||||
});
|
||||
|
||||
it('should throw Not Found if exception item is not found during update', async () => {
|
||||
exceptionsListClient.updateExceptionListItem.mockReset();
|
||||
exceptionsListClient.updateExceptionListItem.mockResolvedValueOnce(null);
|
||||
|
||||
exceptionsListClient.getExceptionListItem.mockReset();
|
||||
exceptionsListClient.getExceptionListItem.mockResolvedValueOnce(EXCEPTION_LIST_ITEM);
|
||||
exceptionsListClient.getExceptionListItem.mockResolvedValueOnce(null);
|
||||
|
||||
await expect(
|
||||
updateTrustedApp(exceptionsListClient, TRUSTED_APP.id, toUpdateTrustedApp(TRUSTED_APP))
|
||||
).rejects.toBeInstanceOf(TrustedAppNotFoundError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTrustedApp', () => {
|
||||
it('should return a single trusted app', async () => {
|
||||
exceptionsListClient.getExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM);
|
||||
expect(await getTrustedApp(exceptionsListClient, '123')).toEqual({ data: TRUSTED_APP });
|
||||
});
|
||||
|
||||
it('should return Trusted App Not Found Error if it does not exist', async () => {
|
||||
exceptionsListClient.getExceptionListItem.mockResolvedValue(null);
|
||||
await expect(getTrustedApp(exceptionsListClient, '123')).rejects.toBeInstanceOf(
|
||||
TrustedAppNotFoundError
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,26 +6,30 @@
|
|||
*/
|
||||
|
||||
import { ExceptionListClient } from '../../../../../lists/server';
|
||||
import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common';
|
||||
import {
|
||||
ENDPOINT_TRUSTED_APPS_LIST_ID,
|
||||
ExceptionListItemSchema,
|
||||
} from '../../../../../lists/common';
|
||||
|
||||
import {
|
||||
DeleteTrustedAppsRequestParams,
|
||||
GetOneTrustedAppResponse,
|
||||
GetTrustedAppsListRequest,
|
||||
GetTrustedAppsSummaryResponse,
|
||||
GetTrustedListAppsResponse,
|
||||
PostTrustedAppCreateRequest,
|
||||
PostTrustedAppCreateResponse,
|
||||
PutTrustedAppUpdateRequest,
|
||||
PutTrustedAppUpdateResponse,
|
||||
} from '../../../../common/endpoint/types';
|
||||
|
||||
import {
|
||||
exceptionListItemToTrustedApp,
|
||||
newTrustedAppToCreateExceptionListItemOptions,
|
||||
osFromExceptionItem,
|
||||
updatedTrustedAppToUpdateExceptionListItemOptions,
|
||||
} from './mapping';
|
||||
|
||||
export class MissingTrustedAppException {
|
||||
constructor(public id: string) {}
|
||||
}
|
||||
import { TrustedAppNotFoundError, TrustedAppVersionConflictError } from './errors';
|
||||
|
||||
export const deleteTrustedApp = async (
|
||||
exceptionsListClient: ExceptionListClient,
|
||||
|
@ -38,13 +42,32 @@ export const deleteTrustedApp = async (
|
|||
});
|
||||
|
||||
if (!exceptionListItem) {
|
||||
throw new MissingTrustedAppException(id);
|
||||
throw new TrustedAppNotFoundError(id);
|
||||
}
|
||||
};
|
||||
|
||||
export const getTrustedApp = async (
|
||||
exceptionsListClient: ExceptionListClient,
|
||||
id: string
|
||||
): Promise<GetOneTrustedAppResponse> => {
|
||||
const trustedAppExceptionItem = await exceptionsListClient.getExceptionListItem({
|
||||
itemId: '',
|
||||
id,
|
||||
namespaceType: 'agnostic',
|
||||
});
|
||||
|
||||
if (!trustedAppExceptionItem) {
|
||||
throw new TrustedAppNotFoundError(id);
|
||||
}
|
||||
|
||||
return {
|
||||
data: exceptionListItemToTrustedApp(trustedAppExceptionItem),
|
||||
};
|
||||
};
|
||||
|
||||
export const getTrustedAppsList = async (
|
||||
exceptionsListClient: ExceptionListClient,
|
||||
{ page, per_page: perPage }: GetTrustedAppsListRequest
|
||||
{ page, per_page: perPage, kuery }: GetTrustedAppsListRequest
|
||||
): Promise<GetTrustedListAppsResponse> => {
|
||||
// Ensure list is created if it does not exist
|
||||
await exceptionsListClient.createTrustedAppsList();
|
||||
|
@ -53,7 +76,7 @@ export const getTrustedAppsList = async (
|
|||
listId: ENDPOINT_TRUSTED_APPS_LIST_ID,
|
||||
page,
|
||||
perPage,
|
||||
filter: undefined,
|
||||
filter: kuery,
|
||||
namespaceType: 'agnostic',
|
||||
sortField: 'name',
|
||||
sortOrder: 'asc',
|
||||
|
@ -74,6 +97,9 @@ export const createTrustedApp = async (
|
|||
// Ensure list is created if it does not exist
|
||||
await exceptionsListClient.createTrustedAppsList();
|
||||
|
||||
// Validate update TA entry - error if not valid
|
||||
// TODO: implement validations
|
||||
|
||||
const createdTrustedAppExceptionItem = await exceptionsListClient.createExceptionListItem(
|
||||
newTrustedAppToCreateExceptionListItemOptions(newTrustedApp)
|
||||
);
|
||||
|
@ -81,6 +107,48 @@ export const createTrustedApp = async (
|
|||
return { data: exceptionListItemToTrustedApp(createdTrustedAppExceptionItem) };
|
||||
};
|
||||
|
||||
export const updateTrustedApp = async (
|
||||
exceptionsListClient: ExceptionListClient,
|
||||
id: string,
|
||||
updatedTrustedApp: PutTrustedAppUpdateRequest
|
||||
): Promise<PutTrustedAppUpdateResponse> => {
|
||||
const currentTrustedApp = await exceptionsListClient.getExceptionListItem({
|
||||
itemId: '',
|
||||
id,
|
||||
namespaceType: 'agnostic',
|
||||
});
|
||||
|
||||
if (!currentTrustedApp) {
|
||||
throw new TrustedAppNotFoundError(id);
|
||||
}
|
||||
|
||||
// Validate update TA entry - error if not valid
|
||||
// TODO: implement validations
|
||||
|
||||
let updatedTrustedAppExceptionItem: ExceptionListItemSchema | null;
|
||||
|
||||
try {
|
||||
updatedTrustedAppExceptionItem = await exceptionsListClient.updateExceptionListItem(
|
||||
updatedTrustedAppToUpdateExceptionListItemOptions(currentTrustedApp, updatedTrustedApp)
|
||||
);
|
||||
} catch (e) {
|
||||
if (e?.output?.statusCode === 409) {
|
||||
throw new TrustedAppVersionConflictError(id, e);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
// If `null` is returned, then that means the TA does not exist (could happen in race conditions)
|
||||
if (!updatedTrustedAppExceptionItem) {
|
||||
throw new TrustedAppNotFoundError(id);
|
||||
}
|
||||
|
||||
return {
|
||||
data: exceptionListItemToTrustedApp(updatedTrustedAppExceptionItem),
|
||||
};
|
||||
};
|
||||
|
||||
export const getTrustedAppsSummary = async (
|
||||
exceptionsListClient: ExceptionListClient
|
||||
): Promise<GetTrustedAppsSummaryResponse> => {
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { ExceptionListClient } from '../../../../../lists/server';
|
||||
|
||||
export const updateExceptionListItemImplementationMock: ExceptionListClient['updateExceptionListItem'] = async (
|
||||
listItem
|
||||
) => {
|
||||
return {
|
||||
_version: listItem._version || 'abc123',
|
||||
id: listItem.id || '123',
|
||||
comments: [],
|
||||
created_at: '11/11/2011T11:11:11.111',
|
||||
created_by: 'admin',
|
||||
description: listItem.description || '',
|
||||
entries: listItem.entries || [],
|
||||
item_id: listItem.itemId || '',
|
||||
list_id: 'endpoint_trusted_apps',
|
||||
meta: undefined,
|
||||
name: listItem.name || '',
|
||||
namespace_type: listItem.namespaceType || '',
|
||||
os_types: listItem.osTypes || '',
|
||||
tags: listItem.tags || [],
|
||||
type: 'simple',
|
||||
tie_breaker_id: '123',
|
||||
updated_at: '11/11/2011T11:11:11.111',
|
||||
updated_by: 'admin',
|
||||
};
|
||||
};
|
|
@ -11,6 +11,7 @@ import { ConfigType } from '../config';
|
|||
import { EndpointAppContextService } from './endpoint_app_context_services';
|
||||
import { JsonObject } from '../../../../../src/plugins/kibana_utils/common';
|
||||
import { HostMetadata, MetadataQueryStrategyVersions } from '../../common/endpoint/types';
|
||||
import { ExperimentalFeatures } from '../../common/experimental_features';
|
||||
|
||||
/**
|
||||
* The context for Endpoint apps.
|
||||
|
@ -18,6 +19,7 @@ import { HostMetadata, MetadataQueryStrategyVersions } from '../../common/endpoi
|
|||
export interface EndpointAppContext {
|
||||
logFactory: LoggerFactory;
|
||||
config(): Promise<ConfigType>;
|
||||
experimentalFeatures: ExperimentalFeatures;
|
||||
|
||||
/**
|
||||
* Object readiness is tied to plugin start method
|
||||
|
|
|
@ -16,6 +16,9 @@ export const plugin = (context: PluginInitializerContext) => {
|
|||
};
|
||||
|
||||
export const config: PluginConfigDescriptor<ConfigType> = {
|
||||
exposeToBrowser: {
|
||||
enableExperimental: true,
|
||||
},
|
||||
schema: configSchema,
|
||||
deprecations: ({ renameFromRoot }) => [
|
||||
renameFromRoot('xpack.siem.enabled', 'xpack.securitySolution.enabled'),
|
||||
|
|
|
@ -37,6 +37,7 @@ import {
|
|||
} from '../../endpoint/mocks';
|
||||
import { PackageService } from '../../../../fleet/server/services';
|
||||
import { ElasticsearchAssetType } from '../../../../fleet/common/types/models';
|
||||
import { parseExperimentalConfigValue } from '../../../common/experimental_features';
|
||||
|
||||
jest.mock('./query.hosts.dsl', () => {
|
||||
return {
|
||||
|
@ -187,6 +188,7 @@ describe('hosts elasticsearch_adapter', () => {
|
|||
logFactory: mockLogger,
|
||||
service: endpointAppContextService,
|
||||
config: jest.fn(),
|
||||
experimentalFeatures: parseExperimentalConfigValue([]),
|
||||
};
|
||||
describe('#getHosts', () => {
|
||||
const mockCallWithRequest = jest.fn();
|
||||
|
|
|
@ -167,6 +167,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
logFactory: this.context.logger,
|
||||
service: this.endpointAppContextService,
|
||||
config: (): Promise<ConfigType> => Promise.resolve(config),
|
||||
experimentalFeatures: parseExperimentalConfigValue(config.enableExperimental),
|
||||
};
|
||||
|
||||
initUsageCollectors({
|
||||
|
@ -202,7 +203,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
registerLimitedConcurrencyRoutes(core);
|
||||
registerResolverRoutes(router);
|
||||
registerPolicyRoutes(router, endpointContext);
|
||||
registerTrustedAppsRoutes(router);
|
||||
registerTrustedAppsRoutes(router, endpointContext);
|
||||
registerDownloadArtifactRoute(router, endpointContext, this.artifactsCache);
|
||||
|
||||
plugins.features.registerKibanaFeature({
|
||||
|
|
|
@ -20649,9 +20649,7 @@
|
|||
"xpack.securitySolution.trustedapps.create.os": "オペレーティングシステムを選択",
|
||||
"xpack.securitySolution.trustedapps.create.osRequiredMsg": "オペレーティングシステムは必須です",
|
||||
"xpack.securitySolution.trustedapps.createTrustedAppFlyout.cancelButton": "キャンセル",
|
||||
"xpack.securitySolution.trustedapps.createTrustedAppFlyout.saveButton": "信頼できるアプリケーションを追加",
|
||||
"xpack.securitySolution.trustedapps.createTrustedAppFlyout.successToastTitle": "「{name}」は信頼できるアプリケーションリストに追加されました。",
|
||||
"xpack.securitySolution.trustedapps.createTrustedAppFlyout.title": "信頼できるアプリケーションを追加",
|
||||
"xpack.securitySolution.trustedapps.deletionDialog.cancelButton": "キャンセル",
|
||||
"xpack.securitySolution.trustedapps.deletionDialog.confirmButton": "信頼できるアプリケーションを削除",
|
||||
"xpack.securitySolution.trustedapps.deletionDialog.mainMessage": "信頼できるアプリケーション「{name}」を削除しています。",
|
||||
|
|
|
@ -20974,9 +20974,7 @@
|
|||
"xpack.securitySolution.trustedapps.create.os": "选择操作系统",
|
||||
"xpack.securitySolution.trustedapps.create.osRequiredMsg": "“操作系统”必填",
|
||||
"xpack.securitySolution.trustedapps.createTrustedAppFlyout.cancelButton": "取消",
|
||||
"xpack.securitySolution.trustedapps.createTrustedAppFlyout.saveButton": "添加受信任的应用程序",
|
||||
"xpack.securitySolution.trustedapps.createTrustedAppFlyout.successToastTitle": "“{name}”已添加到受信任的应用程序列表。",
|
||||
"xpack.securitySolution.trustedapps.createTrustedAppFlyout.title": "添加受信任的应用程序",
|
||||
"xpack.securitySolution.trustedapps.deletionDialog.cancelButton": "取消",
|
||||
"xpack.securitySolution.trustedapps.deletionDialog.confirmButton": "移除受信任的应用程序",
|
||||
"xpack.securitySolution.trustedapps.deletionDialog.mainMessage": "您正在移除受信任的应用程序“{name}”。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue