[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:
David Sánchez 2021-03-26 11:32:46 +01:00 committed by GitHub
parent 02a8f11ec8
commit 2af094a63d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 9572 additions and 692 deletions

View file

@ -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';

View file

@ -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';

View file

@ -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();
});
});
});

View file

@ -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),
};

View file

@ -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;
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { ConditionEntry, ConditionEntryField } from '../types';
import { ConditionEntry, ConditionEntryField } from '../../types';
const HASH_LENGTHS: readonly number[] = [
32, // MD5

View file

@ -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.
*/

View file

@ -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;
};
/**

View file

@ -13,6 +13,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues;
*/
const allowedExperimentalValues = Object.freeze({
fleetServerEnabled: false,
trustedAppsByPolicyEnabled: false,
});
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;

View file

@ -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';

View file

@ -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();
});
});

View file

@ -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];
});
};

View file

@ -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;
}

View file

@ -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([]),
}
);

View file

@ -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(),

View file

@ -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);

View file

@ -10,3 +10,7 @@ export interface ServerApiError {
error: string;
message: string;
}
export interface SecuritySolutionUiConfigType {
enableExperimental: string[];
}

View file

@ -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, {

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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';
};

View file

@ -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;

View file

@ -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,

View file

@ -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);

View file

@ -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') {

View file

@ -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,
});
});

View file

@ -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;

View file

@ -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;
}
});

View file

@ -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' },
};
};

View file

@ -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>

View file

@ -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,
},
});
});

View file

@ -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>
);
}

View file

@ -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]],
});
});
});
});

View file

@ -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';

View file

@ -5,8 +5,4 @@
* 2.0.
*/
export interface TrustedAppsUrlParams {
page_index: number;
page_size: number;
show?: 'create';
}
export * from './effected_policy_select';

View file

@ -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 || {}),
},
});
}
};
};

View file

@ -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"

View file

@ -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')}
/>
);
});

View file

@ -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();

View file

@ -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';

View file

@ -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"
>

View file

@ -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>

View file

@ -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"

View file

@ -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"
/>
);

View file

@ -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',
{

View file

@ -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 <></>;

View file

@ -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', () => {

View file

@ -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) {

View file

@ -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 || []),
}
),
{

View file

@ -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);

View file

@ -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),
};
};

View file

@ -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
);

View file

@ -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),
});
});

View file

@ -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),
});
});

View file

@ -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(),
{

View file

@ -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(),
{

View file

@ -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({

View file

@ -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`);
}
}

View file

@ -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),
});
});
});
});

View file

@ -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);
}
};
};

View file

@ -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)
);
};

View file

@ -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',
});
});
});
});

View file

@ -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,
};
};

View file

@ -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
);
});
});
});

View file

@ -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> => {

View file

@ -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',
};
};

View file

@ -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

View file

@ -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'),

View file

@ -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();

View file

@ -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({

View file

@ -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}」を削除しています。",

View file

@ -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}”。",