diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index a6018837fa4fe..cd59c2518794c 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -13,6 +13,7 @@ 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_SUPPORTED_OS_TYPES: readonly string[] = ['macos', 'windows', 'linux']; export const TRUSTED_APPS_LIST_API = '/api/endpoint/trusted_apps'; export const TRUSTED_APPS_CREATE_API = '/api/endpoint/trusted_apps'; export const TRUSTED_APPS_DELETE_API = '/api/endpoint/trusted_apps/{id}'; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts index b0c769216732d..fc94e9a7c312a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts @@ -76,7 +76,7 @@ describe('When invoking Trusted Apps Schema', () => { os: 'windows', entries: [ { - field: 'path', + field: 'process.path', type: 'match', operator: 'included', value: 'c:/programs files/Anti-Virus', @@ -111,14 +111,6 @@ describe('When invoking Trusted Apps Schema', () => { expect(body.validate(bodyMsg)).toStrictEqual(bodyMsg); }); - it('should validate `description` to be non-empty if defined', () => { - const bodyMsg = { - ...getCreateTrustedAppItem(), - description: '', - }; - expect(() => body.validate(bodyMsg)).toThrow(); - }); - it('should validate `os` to to only accept known values', () => { const bodyMsg = { ...getCreateTrustedAppItem(), @@ -202,7 +194,7 @@ describe('When invoking Trusted Apps Schema', () => { }; expect(() => body.validate(bodyMsg2)).toThrow(); - ['hash', 'path'].forEach((field) => { + ['process.hash.*', 'process.path'].forEach((field) => { const bodyMsg3 = { ...getCreateTrustedAppItem(), entries: [ @@ -217,9 +209,55 @@ describe('When invoking Trusted Apps Schema', () => { }); }); - it.todo('should validate `entry.type` is limited to known values'); + it('should validate `entry.type` is limited to known values', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: [ + { + ...getTrustedAppItemEntryItem(), + type: 'invalid', + }, + ], + }; + expect(() => body.validate(bodyMsg)).toThrow(); + + // Allow `match` + const bodyMsg2 = { + ...getCreateTrustedAppItem(), + entries: [ + { + ...getTrustedAppItemEntryItem(), + type: 'match', + }, + ], + }; + expect(() => body.validate(bodyMsg2)).not.toThrow(); + }); + + it('should validate `entry.operator` is limited to known values', () => { + const bodyMsg = { + ...getCreateTrustedAppItem(), + entries: [ + { + ...getTrustedAppItemEntryItem(), + operator: 'invalid', + }, + ], + }; + expect(() => body.validate(bodyMsg)).toThrow(); - it.todo('should validate `entry.operator` is limited to known values'); + // Allow `match` + const bodyMsg2 = { + ...getCreateTrustedAppItem(), + entries: [ + { + ...getTrustedAppItemEntryItem(), + operator: 'included', + }, + ], + }; + expect(() => body.validate(bodyMsg2)).not.toThrow(); + }); it('should validate `entry.value` required', () => { const { value, ...entry } = getTrustedAppItemEntryItem(); diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts index 7c0de84b637c9..72e24a7d694d4 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -22,11 +22,11 @@ export const GetTrustedAppsRequestSchema = { export const PostTrustedAppCreateRequestSchema = { body: schema.object({ name: schema.string({ minLength: 1 }), - description: schema.maybe(schema.string({ minLength: 1 })), + description: schema.maybe(schema.string({ minLength: 0, defaultValue: '' })), os: schema.oneOf([schema.literal('linux'), schema.literal('macos'), schema.literal('windows')]), entries: schema.arrayOf( schema.object({ - field: schema.oneOf([schema.literal('hash'), schema.literal('path')]), + field: schema.oneOf([schema.literal('process.hash.*'), schema.literal('process.path')]), type: schema.literal('match'), operator: schema.literal('included'), value: schema.string({ minLength: 1 }), diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index 7aeb6c6024b99..3356fc67d2682 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -25,17 +25,17 @@ export interface PostTrustedAppCreateResponse { data: TrustedApp; } -interface MacosLinuxConditionEntry { - field: 'hash' | 'path'; +export interface MacosLinuxConditionEntry { + field: 'process.hash.*' | 'process.path'; type: 'match'; operator: 'included'; value: string; } -type WindowsConditionEntry = +export type WindowsConditionEntry = | MacosLinuxConditionEntry | (Omit & { - field: 'signer'; + field: 'process.code_signature'; }); /** Type for a new Trusted App Entry */ diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index 62f360df90192..40320ed794203 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -21,6 +21,7 @@ import { import { AdministrationSubTab } from '../types'; import { appendSearch } from '../../common/components/link_to/helpers'; import { EndpointIndexUIQueryParams } from '../pages/endpoint_hosts/types'; +import { TrustedAppsUrlParams } from '../pages/trusted_apps/types'; // Taken from: https://github.com/microsoft/TypeScript/issues/12936#issuecomment-559034150 type ExactKeys = Exclude extends never ? T1 : never; @@ -89,18 +90,16 @@ export const getPolicyDetailPath = (policyId: string, search?: string) => { })}${appendSearch(search)}`; }; -interface ListPaginationParams { - page_index: number; - page_size: number; -} - -const isDefaultOrMissing = (value: number | undefined, defaultValue: number) => { +const isDefaultOrMissing = ( + value: number | string | undefined, + defaultValue: number | undefined +) => { return value === undefined || value === defaultValue; }; const normalizeListPaginationParams = ( - params?: Partial -): Partial => { + params?: Partial +): Partial => { if (params) { return { ...(!isDefaultOrMissing(params.page_index, MANAGEMENT_DEFAULT_PAGE) @@ -109,13 +108,19 @@ const normalizeListPaginationParams = ( ...(!isDefaultOrMissing(params.page_size, MANAGEMENT_DEFAULT_PAGE_SIZE) ? { page_size: params.page_size } : {}), + ...(!isDefaultOrMissing(params.show, undefined) ? { show: params.show } : {}), }; } else { return {}; } }; -const extractFirstParamValue = (query: querystring.ParsedUrlQuery, key: string): string => { +/** + * Given an object with url params, and a given key, return back only the first param value (case multiples were defined) + * @param query + * @param key + */ +export const extractFirstParamValue = (query: querystring.ParsedUrlQuery, key: string): string => { const value = query[key]; return Array.isArray(value) ? value[value.length - 1] : value; @@ -135,12 +140,12 @@ const extractPageSize = (query: querystring.ParsedUrlQuery): number => { export const extractListPaginationParams = ( query: querystring.ParsedUrlQuery -): ListPaginationParams => ({ +): TrustedAppsUrlParams => ({ page_index: extractPageIndex(query), page_size: extractPageSize(query), }); -export const getTrustedAppsListPath = (params?: Partial): string => { +export const getTrustedAppsListPath = (params?: Partial): string => { const path = generatePath(MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, { tabName: AdministrationSubTab.trustedApps, }); diff --git a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx index 3df525b4d59d6..372916581b35d 100644 --- a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx @@ -5,6 +5,7 @@ */ import React, { FC, memo } from 'react'; import { EuiPanel, EuiSpacer, CommonProps } from '@elastic/eui'; +import styled from 'styled-components'; import { SecurityPageName } from '../../../common/constants'; import { WrapperPage } from '../../common/components/wrapper_page'; import { HeaderPage } from '../../common/components/header_page'; @@ -14,6 +15,13 @@ import { AdministrationSubTab } from '../types'; import { ENDPOINTS_TAB, TRUSTED_APPS_TAB, BETA_BADGE_LABEL } from '../common/translations'; import { getEndpointListPath, getTrustedAppsListPath } from '../common/routing'; +/** Ensure that all flyouts z-index in Administation area show the flyout header */ +const EuiPanelStyled = styled(EuiPanel)` + .euiFlyout { + z-index: ${({ theme }) => theme.eui.euiZNavigation + 1}; + } +`; + interface AdministrationListPageProps { beta: boolean; title: React.ReactNode; @@ -54,7 +62,7 @@ export const AdministrationListPage: FC - {children} + {children} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts index 9308c137cfb9c..a3c5911aa3a86 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts @@ -5,14 +5,20 @@ */ import { HttpStart } from 'kibana/public'; -import { TRUSTED_APPS_LIST_API } from '../../../../../common/endpoint/constants'; +import { + TRUSTED_APPS_CREATE_API, + TRUSTED_APPS_LIST_API, +} from '../../../../../common/endpoint/constants'; import { GetTrustedListAppsResponse, GetTrustedAppsListRequest, + PostTrustedAppCreateRequest, + PostTrustedAppCreateResponse, } from '../../../../../common/endpoint/types/trusted_apps'; export interface TrustedAppsService { getTrustedAppsList(request: GetTrustedAppsListRequest): Promise; + createTrustedApp(request: PostTrustedAppCreateRequest): Promise; } export class TrustedAppsHttpService implements TrustedAppsService { @@ -23,4 +29,10 @@ export class TrustedAppsHttpService implements TrustedAppsService { query: request, }); } + + async createTrustedApp(request: PostTrustedAppCreateRequest) { + return this.http.post(TRUSTED_APPS_CREATE_API, { + body: JSON.stringify(request), + }); + } } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts index 23f4cfd576c56..071557ec1a815 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TrustedApp } from '../../../../../common/endpoint/types/trusted_apps'; +import { NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types/trusted_apps'; import { AsyncResourceState } from '.'; +import { TrustedAppsUrlParams } from '../types'; +import { ServerApiError } from '../../../../common/types'; export interface PaginationInfo { index: number; @@ -18,10 +20,34 @@ export interface TrustedAppsListData { paginationInfo: PaginationInfo; } +/** Store State when an API request has been sent to create a new trusted app entry */ +export interface TrustedAppCreatePending { + type: 'pending'; + data: NewTrustedApp; +} + +/** Store State when creation of a new Trusted APP entry was successful */ +export interface TrustedAppCreateSuccess { + type: 'success'; + data: TrustedApp; +} + +/** Store State when creation of a new Trusted App Entry failed */ +export interface TrustedAppCreateFailure { + type: 'failure'; + data: ServerApiError; +} + export interface TrustedAppsListPageState { listView: { currentListResourceState: AsyncResourceState; currentPaginationInfo: PaginationInfo; + show: TrustedAppsUrlParams['show'] | undefined; }; + createView: + | undefined + | TrustedAppCreatePending + | TrustedAppCreateSuccess + | TrustedAppCreateFailure; active: boolean; } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts new file mode 100644 index 0000000000000..1e8e0bc042b86 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + TrustedAppCreatePending, + TrustedAppsListPageState, + TrustedAppCreateFailure, + TrustedAppCreateSuccess, +} from './trusted_apps_list_page_state'; +import { + Immutable, + NewTrustedApp, + WindowsConditionEntry, +} from '../../../../../common/endpoint/types'; +import { TRUSTED_APPS_SUPPORTED_OS_TYPES } from '../../../../../common/endpoint/constants'; + +type CreateViewPossibleStates = + | TrustedAppsListPageState['createView'] + | Immutable; + +export const isTrustedAppCreatePendingState = ( + data: CreateViewPossibleStates +): data is TrustedAppCreatePending => { + return data?.type === 'pending'; +}; + +export const isTrustedAppCreateSuccessState = ( + data: CreateViewPossibleStates +): data is TrustedAppCreateSuccess => { + return data?.type === 'success'; +}; + +export const isTrustedAppCreateFailureState = ( + data: CreateViewPossibleStates +): data is TrustedAppCreateFailure => { + return data?.type === 'failure'; +}; + +export const isWindowsTrustedApp = ( + trustedApp: T +): trustedApp is T & { os: 'windows' } => { + return trustedApp.os === 'windows'; +}; + +export const isWindowsTrustedAppCondition = (condition: { + field: string; +}): condition is WindowsConditionEntry => { + return condition.field === 'process.code_signature' || true; +}; + +export const isTrustedAppSupportedOs = (os: string): os is NewTrustedApp['os'] => + TRUSTED_APPS_SUPPORTED_OS_TYPES.includes(os); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts index 2154a0eca462e..3a43ffe58262c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts @@ -4,7 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AsyncResourceState, TrustedAppsListData } from '../state'; +import { + AsyncResourceState, + TrustedAppCreateFailure, + TrustedAppCreatePending, + TrustedAppCreateSuccess, + TrustedAppsListData, +} from '../state'; export interface TrustedAppsListResourceStateChanged { type: 'trustedAppsListResourceStateChanged'; @@ -13,4 +19,23 @@ export interface TrustedAppsListResourceStateChanged { }; } -export type TrustedAppsPageAction = TrustedAppsListResourceStateChanged; +export interface UserClickedSaveNewTrustedAppButton { + type: 'userClickedSaveNewTrustedAppButton'; + payload: TrustedAppCreatePending; +} + +export interface ServerReturnedCreateTrustedAppSuccess { + type: 'serverReturnedCreateTrustedAppSuccess'; + payload: TrustedAppCreateSuccess; +} + +export interface ServerReturnedCreateTrustedAppFailure { + type: 'serverReturnedCreateTrustedAppFailure'; + payload: TrustedAppCreateFailure; +} + +export type TrustedAppsPageAction = + | TrustedAppsListResourceStateChanged + | UserClickedSaveNewTrustedAppButton + | ServerReturnedCreateTrustedAppSuccess + | ServerReturnedCreateTrustedAppFailure; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts index c5abaae473486..e5f00ee0ccf81 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts @@ -31,6 +31,7 @@ const createGetTrustedListAppsResponse = (pagination: PaginationInfo, totalItems const createTrustedAppsServiceMock = (): jest.Mocked => ({ getTrustedAppsList: jest.fn(), + createTrustedApp: jest.fn(), }); const createStoreSetup = (trustedAppsService: TrustedAppsService) => { @@ -70,6 +71,7 @@ describe('middleware', () => { expect(store.getState()).toStrictEqual({ listView: createLoadingListViewWithPagination(pagination), active: true, + createView: undefined, }); await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); @@ -77,6 +79,7 @@ describe('middleware', () => { expect(store.getState()).toStrictEqual({ listView: createLoadedListViewWithPagination(pagination, pagination, 500), active: true, + createView: undefined, }); }); @@ -99,6 +102,7 @@ describe('middleware', () => { expect(store.getState()).toStrictEqual({ listView: createLoadedListViewWithPagination(pagination, pagination, 500), active: true, + createView: undefined, }); }); @@ -118,6 +122,7 @@ describe('middleware', () => { createServerApiError('Internal Server Error') ), active: true, + createView: undefined, }); const infiniteLoopTest = async () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts index 31c301b8dbd2b..bf9cacff5caf0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Immutable } from '../../../../../common/endpoint/types'; +import { Immutable, PostTrustedAppCreateRequest } from '../../../../../common/endpoint/types'; import { AppAction } from '../../../../common/store/actions'; import { ImmutableMiddleware, @@ -28,6 +28,8 @@ import { getLastLoadedListResourceState, getListCurrentPageIndex, getListCurrentPageSize, + getTrustedAppCreateData, + isCreatePending, needsRefreshOfListData, } from './selectors'; @@ -81,6 +83,38 @@ const refreshList = async ( } }; +const createTrustedApp = async ( + store: ImmutableMiddlewareAPI, + trustedAppsService: TrustedAppsService +) => { + const { dispatch, getState } = store; + + if (isCreatePending(getState())) { + try { + const newTrustedApp = getTrustedAppCreateData(getState()); + const createdTrustedApp = ( + await trustedAppsService.createTrustedApp(newTrustedApp as PostTrustedAppCreateRequest) + ).data; + dispatch({ + type: 'serverReturnedCreateTrustedAppSuccess', + payload: { + type: 'success', + data: createdTrustedApp, + }, + }); + refreshList(store, trustedAppsService); + } catch (error) { + dispatch({ + type: 'serverReturnedCreateTrustedAppFailure', + payload: { + type: 'failure', + data: error.body || error, + }, + }); + } + } +}; + export const createTrustedAppsPageMiddleware = ( trustedAppsService: TrustedAppsService ): ImmutableMiddleware => { @@ -91,6 +125,10 @@ export const createTrustedAppsPageMiddleware = ( if (action.type === 'userChangedUrl' && needsRefreshOfListData(store.getState())) { await refreshList(store, trustedAppsService); } + + if (action.type === 'userClickedSaveNewTrustedAppButton') { + createTrustedApp(store, trustedAppsService); + } }; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts index 34325e0cf1398..76dd4b48e63d2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts @@ -26,6 +26,7 @@ describe('reducer', () => { currentPaginationInfo: { index: 5, size: 50 }, }, active: true, + createView: undefined, }); }); @@ -67,7 +68,7 @@ describe('reducer', () => { it('makes page state inactive and resets list to uninitialised state when navigating away', () => { const result = trustedAppsPageReducer( - { listView: createLoadedListViewWithPagination(), active: true }, + { listView: createLoadedListViewWithPagination(), active: true, createView: undefined }, createUserChangedUrlAction('/endpoints') ); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts index 4fdc6f90ef40c..d824a6e95c8d5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts @@ -11,14 +11,19 @@ import { ImmutableReducer } from '../../../../common/store'; import { AppLocation, Immutable } from '../../../../../common/endpoint/types'; import { UserChangedUrl } from '../../../../common/store/routing/action'; import { AppAction } from '../../../../common/store/actions'; -import { extractListPaginationParams } from '../../../common/routing'; +import { extractFirstParamValue, extractListPaginationParams } from '../../../common/routing'; import { MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE, } from '../../../common/constants'; -import { TrustedAppsListResourceStateChanged } from './action'; +import { + ServerReturnedCreateTrustedAppFailure, + ServerReturnedCreateTrustedAppSuccess, + TrustedAppsListResourceStateChanged, + UserClickedSaveNewTrustedAppButton, +} from './action'; import { TrustedAppsListPageState } from '../state'; type StateReducer = ImmutableReducer; @@ -51,7 +56,10 @@ const trustedAppsListResourceStateChanged: CaseReducer = (state, action) => { if (isTrustedAppsPageLocation(action.payload)) { - const paginationParams = extractListPaginationParams(parse(action.payload.search.slice(1))); + const parsedUrlsParams = parse(action.payload.search.slice(1)); + const paginationParams = extractListPaginationParams(parsedUrlsParams); + const show = + extractFirstParamValue(parsedUrlsParams, 'show') === 'create' ? 'create' : undefined; return { ...state, @@ -61,7 +69,9 @@ const userChangedUrl: CaseReducer = (state, action) => { index: paginationParams.page_index, size: paginationParams.page_size, }, + show, }, + createView: show ? state.createView : undefined, active: true, }; } else { @@ -69,6 +79,17 @@ const userChangedUrl: CaseReducer = (state, action) => { } }; +const trustedAppsCreateResourceChanged: CaseReducer< + | UserClickedSaveNewTrustedAppButton + | ServerReturnedCreateTrustedAppFailure + | ServerReturnedCreateTrustedAppSuccess +> = (state, action) => { + return { + ...state, + createView: action.payload, + }; +}; + export const initialTrustedAppsPageState: TrustedAppsListPageState = { listView: { currentListResourceState: { type: 'UninitialisedResourceState' }, @@ -76,7 +97,9 @@ export const initialTrustedAppsPageState: TrustedAppsListPageState = { index: MANAGEMENT_DEFAULT_PAGE, size: MANAGEMENT_DEFAULT_PAGE_SIZE, }, + show: undefined, }, + createView: undefined, active: false, }; @@ -90,6 +113,11 @@ export const trustedAppsPageReducer: StateReducer = ( case 'userChangedUrl': return userChangedUrl(state, action); + + case 'userClickedSaveNewTrustedAppButton': + case 'serverReturnedCreateTrustedAppSuccess': + case 'serverReturnedCreateTrustedAppFailure': + return trustedAppsCreateResourceChanged(state, action); } return state; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts index a969e2dee4773..453afa1befa6b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts @@ -30,33 +30,41 @@ import { describe('selectors', () => { describe('needsRefreshOfListData()', () => { it('returns false for outdated resource state and inactive state', () => { - expect(needsRefreshOfListData({ listView: createDefaultListView(), active: false })).toBe( - false - ); + expect( + needsRefreshOfListData({ + listView: createDefaultListView(), + active: false, + createView: undefined, + }) + ).toBe(false); }); it('returns true for outdated resource state and active state', () => { - expect(needsRefreshOfListData({ listView: createDefaultListView(), active: true })).toBe( - true - ); + expect( + needsRefreshOfListData({ + listView: createDefaultListView(), + active: true, + createView: undefined, + }) + ).toBe(true); }); it('returns true when current loaded page index is outdated', () => { const listView = createLoadedListViewWithPagination({ index: 1, size: 20 }); - expect(needsRefreshOfListData({ listView, active: true })).toBe(true); + expect(needsRefreshOfListData({ listView, active: true, createView: undefined })).toBe(true); }); it('returns true when current loaded page size is outdated', () => { const listView = createLoadedListViewWithPagination({ index: 0, size: 50 }); - expect(needsRefreshOfListData({ listView, active: true })).toBe(true); + expect(needsRefreshOfListData({ listView, active: true, createView: undefined })).toBe(true); }); it('returns false when current loaded data is up to date', () => { const listView = createLoadedListViewWithPagination(); - expect(needsRefreshOfListData({ listView, active: true })).toBe(false); + expect(needsRefreshOfListData({ listView, active: true, createView: undefined })).toBe(false); }); }); @@ -64,9 +72,9 @@ describe('selectors', () => { it('returns current list resource state', () => { const listView = createDefaultListView(); - expect(getCurrentListResourceState({ listView, active: false })).toStrictEqual( - createUninitialisedResourceState() - ); + expect( + getCurrentListResourceState({ listView, active: false, createView: undefined }) + ).toStrictEqual(createUninitialisedResourceState()); }); }); @@ -78,17 +86,20 @@ describe('selectors', () => { 200 ), currentPaginationInfo: createDefaultPaginationInfo(), + show: undefined, }; - expect(getLastLoadedListResourceState({ listView, active: false })).toStrictEqual( - createListLoadedResourceState(createDefaultPaginationInfo(), 200) - ); + expect( + getLastLoadedListResourceState({ listView, active: false, createView: undefined }) + ).toStrictEqual(createListLoadedResourceState(createDefaultPaginationInfo(), 200)); }); }); describe('getListItems()', () => { it('returns empty list when no valid data loaded', () => { - expect(getListItems({ listView: createDefaultListView(), active: false })).toStrictEqual([]); + expect( + getListItems({ listView: createDefaultListView(), active: false, createView: undefined }) + ).toStrictEqual([]); }); it('returns last loaded list items', () => { @@ -98,9 +109,10 @@ describe('selectors', () => { 200 ), currentPaginationInfo: createDefaultPaginationInfo(), + show: undefined, }; - expect(getListItems({ listView, active: false })).toStrictEqual( + expect(getListItems({ listView, active: false, createView: undefined })).toStrictEqual( createSampleTrustedApps(createDefaultPaginationInfo()) ); }); @@ -108,7 +120,13 @@ describe('selectors', () => { describe('getListTotalItemsCount()', () => { it('returns 0 when no valid data loaded', () => { - expect(getListTotalItemsCount({ listView: createDefaultListView(), active: false })).toBe(0); + expect( + getListTotalItemsCount({ + listView: createDefaultListView(), + active: false, + createView: undefined, + }) + ).toBe(0); }); it('returns last loaded total items count', () => { @@ -118,21 +136,34 @@ describe('selectors', () => { 200 ), currentPaginationInfo: createDefaultPaginationInfo(), + show: undefined, }; - expect(getListTotalItemsCount({ listView, active: false })).toBe(200); + expect(getListTotalItemsCount({ listView, active: false, createView: undefined })).toBe(200); }); }); describe('getListCurrentPageIndex()', () => { it('returns page index', () => { - expect(getListCurrentPageIndex({ listView: createDefaultListView(), active: false })).toBe(0); + expect( + getListCurrentPageIndex({ + listView: createDefaultListView(), + active: false, + createView: undefined, + }) + ).toBe(0); }); }); describe('getListCurrentPageSize()', () => { it('returns page index', () => { - expect(getListCurrentPageSize({ listView: createDefaultListView(), active: false })).toBe(20); + expect( + getListCurrentPageSize({ + listView: createDefaultListView(), + active: false, + createView: undefined, + }) + ).toBe(20); }); }); @@ -144,24 +175,32 @@ describe('selectors', () => { 200 ), currentPaginationInfo: createDefaultPaginationInfo(), + show: undefined, }; - expect(getListErrorMessage({ listView, active: false })).toBeUndefined(); + expect( + getListErrorMessage({ listView, active: false, createView: undefined }) + ).toBeUndefined(); }); it('returns message when not in failed state', () => { const listView = { currentListResourceState: createListFailedResourceState('Internal Server Error'), currentPaginationInfo: createDefaultPaginationInfo(), + show: undefined, }; - expect(getListErrorMessage({ listView, active: false })).toBe('Internal Server Error'); + expect(getListErrorMessage({ listView, active: false, createView: undefined })).toBe( + 'Internal Server Error' + ); }); }); describe('isListLoading()', () => { it('returns false when no loading is happening', () => { - expect(isListLoading({ listView: createDefaultListView(), active: false })).toBe(false); + expect( + isListLoading({ listView: createDefaultListView(), active: false, createView: undefined }) + ).toBe(false); }); it('returns true when loading is in progress', () => { @@ -171,9 +210,10 @@ describe('selectors', () => { 200 ), currentPaginationInfo: createDefaultPaginationInfo(), + show: undefined, }; - expect(isListLoading({ listView, active: false })).toBe(true); + expect(isListLoading({ listView, active: false, createView: undefined })).toBe(true); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts index 6fde779ac1cce..f074b21f79f4e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Immutable, TrustedApp } from '../../../../../common/endpoint/types'; +import { createSelector } from 'reselect'; +import { Immutable, NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types'; import { AsyncResourceState, @@ -14,9 +15,16 @@ import { isOutdatedResourceState, LoadedResourceState, PaginationInfo, + TrustedAppCreateFailure, TrustedAppsListData, TrustedAppsListPageState, } from '../state'; +import { TrustedAppsUrlParams } from '../types'; +import { + isTrustedAppCreateFailureState, + isTrustedAppCreatePendingState, + isTrustedAppCreateSuccessState, +} from '../state/type_guards'; const pageInfosEqual = (pageInfo1: PaginationInfo, pageInfo2: PaginationInfo): boolean => pageInfo1.index === pageInfo2.index && pageInfo1.size === pageInfo2.size; @@ -65,6 +73,27 @@ export const getListTotalItemsCount = (state: Immutable +) => TrustedAppsListPageState['listView']['show'] = (state) => { + return state.listView.show; +}; + +export const getListUrlSearchParams: ( + state: Immutable +) => TrustedAppsUrlParams = createSelector( + getListCurrentPageIndex, + getListCurrentPageSize, + getListCurrentShowValue, + (pageIndex, pageSize, showValue) => { + return { + page_index: pageIndex, + page_size: pageSize, + show: showValue, + }; + } +); + export const getListErrorMessage = ( state: Immutable ): string | undefined => { @@ -74,3 +103,27 @@ export const getListErrorMessage = ( export const isListLoading = (state: Immutable): boolean => { return isLoadingResourceState(state.listView.currentListResourceState); }; + +export const isCreatePending: (state: Immutable) => boolean = ({ + createView, +}) => { + return isTrustedAppCreatePendingState(createView); +}; + +export const getTrustedAppCreateData: ( + state: Immutable +) => undefined | Immutable = ({ createView }) => { + return (isTrustedAppCreatePendingState(createView) && createView.data) || undefined; +}; + +export const getApiCreateErrors: ( + state: Immutable +) => undefined | TrustedAppCreateFailure['data'] = ({ createView }) => { + return (isTrustedAppCreateFailureState(createView) && createView.data) || undefined; +}; + +export const wasCreateSuccessful: (state: Immutable) => boolean = ({ + createView, +}) => { + return isTrustedAppCreateSuccessState(createView); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts index fab059a422a2a..70e4e1e685b01 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts @@ -21,6 +21,7 @@ import { } from '../state'; import { TrustedAppsListResourceStateChanged } from '../store/action'; +import { initialTrustedAppsPageState } from '../store/reducer'; const OS_LIST: Array = ['windows', 'macos', 'linux']; @@ -93,6 +94,7 @@ export const createListComplexLoadingResourceState = ( export const createDefaultPaginationInfo = () => ({ index: 0, size: 20 }); export const createDefaultListView = () => ({ + ...initialTrustedAppsPageState.listView, currentListResourceState: createUninitialisedResourceState(), currentPaginationInfo: createDefaultPaginationInfo(), }); @@ -101,6 +103,7 @@ export const createLoadingListViewWithPagination = ( currentPaginationInfo: PaginationInfo, previousState: StaleResourceState = createUninitialisedResourceState() ): TrustedAppsListPageState['listView'] => ({ + ...initialTrustedAppsPageState.listView, currentListResourceState: { type: 'LoadingResourceState', previousState }, currentPaginationInfo, }); @@ -110,6 +113,7 @@ export const createLoadedListViewWithPagination = ( currentPaginationInfo: PaginationInfo = createDefaultPaginationInfo(), totalItemsCount: number = 200 ): TrustedAppsListPageState['listView'] => ({ + ...initialTrustedAppsPageState.listView, currentListResourceState: createListLoadedResourceState(paginationInfo, totalItemsCount), currentPaginationInfo, }); @@ -119,6 +123,7 @@ export const createFailedListViewWithPagination = ( error: ServerApiError, lastLoadedState?: LoadedResourceState ): TrustedAppsListPageState['listView'] => ({ + ...initialTrustedAppsPageState.listView, currentListResourceState: { type: 'FailedResourceState', error, lastLoadedState }, currentPaginationInfo, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/types.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/types.ts new file mode 100644 index 0000000000000..4d59cd7913a0c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface TrustedAppsUrlParams { + page_index: number; + page_size: number; + show?: 'create'; +} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap index d6e9aee108cf6..642e86059ed6e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap @@ -1,23 +1,955 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TrustedAppsPage rendering 1`] = ` - +Object { + "asFragment": [Function], + "baseElement": .c0 { + padding: 24px; +} + +.c0.siemWrapperPage--restrictWidthDefault, +.c0.siemWrapperPage--restrictWidthCustom { + box-sizing: content-box; + margin: 0 auto; +} + +.c0.siemWrapperPage--restrictWidthDefault { + max-width: 1000px; +} + +.c0.siemWrapperPage--fullHeight { + height: 100%; +} + +.c0.siemWrapperPage--withTimeline { + padding-right: 70px; +} + +.c0.siemWrapperPage--noPadding { + padding: 0; +} + +.c4 { + margin-top: 8px; +} + +.c4 .siemSubtitle__item { + color: #6a717d; + font-size: 12px; + line-height: 1.5; +} + +.c3 { + vertical-align: middle; +} + +.c1 { + margin-bottom: 24px; +} + +.c2 { + display: block; +} + +.c5 .euiFlyout { + z-index: 4001; +} + +@media only screen and (min-width:575px) { + .c4 .siemSubtitle__item { + display: inline-block; + margin-right: 16px; } - title={ - + + .c4 .siemSubtitle__item:last-child { + margin-right: 0; } +} + + +
+
+
+
+
+

+ Trusted Applications + + + Beta + +

+
+
+ View and configure trusted applications +
+
+
+
+ +
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + +
+
+
+ + Name + +
+
+
+ + OS + +
+
+
+ + Date Created + +
+
+
+ + Created By + +
+
+
+ + No items found + +
+
+
+
+
+
+
+ , + "container":
+
+
+
+
+

+ Trusted Applications + + + Beta + +

+
+
+ View and configure trusted applications +
+
+
+
+ +
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + +
+
+
+ + Name + +
+
+
+ + OS + +
+
+
+ + Date Created + +
+
+
+ + Created By + +
+
+
+ + No items found + +
+
+
+
+
+
+
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "unmount": [Function], +} +`; + +exports[`TrustedAppsPage when the Add Trusted App button is clicked should display create form 1`] = ` +@media only screen and (min-width:575px) { + +} + +
- - +
+
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+ +
+
+ + Select an option: Windows, is selected + + +
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+ + Select an option: Hash, is selected + + +
+ + +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+