From 0f18123b83787e736f84194e50537a2dfbcb26da Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 30 Jul 2020 13:21:12 -0400 Subject: [PATCH 1/6] [SECURITY_SOLUTION][ENDPOINT] Fix Endpoint Host page sometimes incorrectly showing onboarding UI (#73222) * Fix incorrectly showing the onboarding message on hosts * Allow loading of the Details even if list load failed * Some test fixes * Fix missing import * refactor UT for the policy list view * Refactor more UT * remove commented out code * Fix some failing tests following cherry-picks * start work on mock host list apis * Remove extra code that came from cherry-pick * more tests fixed * All test pass * Refactoring ++ cleanup * Remove unused import --- .../common/endpoint/generate_data.ts | 105 +++++++++- .../pages/endpoint_hosts/store/action.ts | 6 + .../pages/endpoint_hosts/store/index.test.ts | 1 + .../pages/endpoint_hosts/store/middleware.ts | 42 +++- .../store/mock_host_result_list.ts | 148 +++++++++++++- .../pages/endpoint_hosts/store/reducer.ts | 7 + .../pages/endpoint_hosts/store/selectors.ts | 6 + .../management/pages/endpoint_hosts/types.ts | 2 + .../pages/endpoint_hosts/view/index.test.tsx | 192 +++++++++++------- .../pages/endpoint_hosts/view/index.tsx | 17 +- .../policy/store/policy_list/index.test.ts | 1 + .../policy/store/policy_list/middleware.ts | 1 + .../policy_list/mock_policy_result_list.ts | 40 ---- .../store/policy_list/services/ingest.test.ts | 6 +- .../store/policy_list/services/ingest.ts | 2 +- .../store/policy_list/test_mock_utils.ts | 162 ++++++--------- .../pages/policy/view/policy_details.test.tsx | 8 +- .../pages/policy/view/policy_list.test.tsx | 46 ++--- 18 files changed, 526 insertions(+), 266 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/mock_policy_result_list.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 9a92270fc9c14..aa3f0bf287fca 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -8,16 +8,26 @@ import seedrandom from 'seedrandom'; import { AlertEvent, EndpointEvent, + EndpointStatus, Host, HostMetadata, - OSFields, HostPolicyResponse, HostPolicyResponseActionStatus, + OSFields, PolicyData, - EndpointStatus, } from './types'; import { factory as policyFactory } from './models/policy_config'; import { parentEntityId } from './models/event'; +import { + GetAgentConfigsResponseItem, + GetPackagesResponse, +} from '../../../ingest_manager/common/types/rest_spec'; +import { + AgentConfigStatus, + EsAssetReference, + InstallationStatus, + KibanaAssetReference, +} from '../../../ingest_manager/common/types/models'; export type Event = AlertEvent | EndpointEvent; /** @@ -1062,6 +1072,97 @@ export class EndpointDocGenerator { }; } + /** + * Generate an Agent Configuration (ingest) + */ + public generateAgentConfig(): GetAgentConfigsResponseItem { + return { + id: this.seededUUIDv4(), + name: 'Agent Config', + status: AgentConfigStatus.Active, + description: 'Some description', + namespace: 'default', + monitoring_enabled: ['logs', 'metrics'], + revision: 2, + updated_at: '2020-07-22T16:36:49.196Z', + updated_by: 'elastic', + package_configs: ['852491f0-cc39-11ea-bac2-cdbf95b4b41a'], + agents: 0, + }; + } + + /** + * Generate an EPM Package for Endpoint + */ + public generateEpmPackage(): GetPackagesResponse['response'][0] { + return { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', + description: 'This is the Elastic Endpoint package.', + type: 'solution', + download: '/epr/endpoint/endpoint-0.5.0.tar.gz', + path: '/package/endpoint/0.5.0', + icons: [ + { + src: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg', + size: '16x16', + type: 'image/svg+xml', + }, + ], + status: 'installed' as InstallationStatus, + savedObject: { + type: 'epm-packages', + id: 'endpoint', + attributes: { + installed_kibana: [ + { id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' }, + { id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' }, + ] as KibanaAssetReference[], + installed_es: [ + { id: 'logs-endpoint.alerts', type: 'index_template' }, + { id: 'events-endpoint', type: 'index_template' }, + { id: 'logs-endpoint.events.file', type: 'index_template' }, + { id: 'logs-endpoint.events.library', type: 'index_template' }, + { id: 'metrics-endpoint.metadata', type: 'index_template' }, + { id: 'metrics-endpoint.metadata_mirror', type: 'index_template' }, + { id: 'logs-endpoint.events.network', type: 'index_template' }, + { id: 'metrics-endpoint.policy', type: 'index_template' }, + { id: 'logs-endpoint.events.process', type: 'index_template' }, + { id: 'logs-endpoint.events.registry', type: 'index_template' }, + { id: 'logs-endpoint.events.security', type: 'index_template' }, + { id: 'metrics-endpoint.telemetry', type: 'index_template' }, + ] as EsAssetReference[], + es_index_patterns: { + alerts: 'logs-endpoint.alerts-*', + events: 'events-endpoint-*', + file: 'logs-endpoint.events.file-*', + library: 'logs-endpoint.events.library-*', + metadata: 'metrics-endpoint.metadata-*', + metadata_mirror: 'metrics-endpoint.metadata_mirror-*', + network: 'logs-endpoint.events.network-*', + policy: 'metrics-endpoint.policy-*', + process: 'logs-endpoint.events.process-*', + registry: 'logs-endpoint.events.registry-*', + security: 'logs-endpoint.events.security-*', + telemetry: 'metrics-endpoint.telemetry-*', + }, + name: 'endpoint', + version: '0.5.0', + internal: false, + removable: false, + }, + references: [], + updated_at: '2020-06-24T14:41:23.098Z', + version: 'Wzc0LDFd', + }, + }; + } + /** * Generates a Host Policy response message */ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts index 621fab2e4ee11..4a4326d5b2919 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts @@ -81,6 +81,11 @@ interface ServerReturnedHostNonExistingPolicies { payload: HostState['nonExistingPolicies']; } +interface ServerReturnedHostExistValue { + type: 'serverReturnedHostExistValue'; + payload: boolean; +} + export type HostAction = | ServerReturnedHostList | ServerFailedToReturnHostList @@ -92,6 +97,7 @@ export type HostAction = | ServerFailedToReturnPoliciesForOnboarding | UserSelectedEndpointPolicy | ServerCancelledHostListLoading + | ServerReturnedHostExistValue | ServerCancelledPolicyItemsLoading | ServerReturnedEndpointPackageInfo | ServerReturnedHostNonExistingPolicies; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index b6e18506b6111..8ff4ad5a043b5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -51,6 +51,7 @@ describe('HostList store concerns', () => { policyItemsLoading: false, endpointPackageInfo: undefined, nonExistingPolicies: {}, + hostsExist: true, }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index edeca5659ee38..74bebf211258a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpSetup } from 'kibana/public'; +import { HttpStart } from 'kibana/public'; import { HostInfo, HostResultList } from '../../../../../common/endpoint/types'; import { GetPolicyListResponse } from '../../policy/types'; import { ImmutableMiddlewareFactory } from '../../../../common/store'; @@ -28,6 +28,8 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor return ({ getState, dispatch }) => (next) => async (action) => { next(action); const state = getState(); + + // Host list if ( action.type === 'userChangedUrl' && isOnHostPage(state) && @@ -89,6 +91,19 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor // No hosts, so we should check to see if there are policies for onboarding if (hostResponse && hostResponse.hosts.length === 0) { const http = coreStart.http; + + // The original query to the list could have had an invalid param (ex. invalid page_size), + // so we check first if hosts actually do exist before pulling in data for the onboarding + // messages. + if (await doHostsExist(http)) { + return; + } + + dispatch({ + type: 'serverReturnedHostExistValue', + payload: false, + }); + try { const policyDataResponse: GetPolicyListResponse = await sendGetEndpointSpecificPackageConfigs( http, @@ -119,6 +134,8 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor }); } } + + // Host Details if (action.type === 'userChangedUrl' && hasSelectedHost(state) === true) { dispatch({ type: 'serverCancelledPolicyItemsLoading', @@ -160,7 +177,6 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor type: 'serverFailedToReturnHostList', payload: error, }); - return; } } else { dispatch({ @@ -217,7 +233,7 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor }; const getNonExistingPoliciesForHostsList = async ( - http: HttpSetup, + http: HttpStart, hosts: HostResultList['hosts'], currentNonExistingPolicies: HostState['nonExistingPolicies'] ): Promise => { @@ -274,3 +290,23 @@ const getNonExistingPoliciesForHostsList = async ( return nonExisting; }; + +const doHostsExist = async (http: HttpStart): Promise => { + try { + return ( + ( + await http.post('/api/endpoint/metadata', { + body: JSON.stringify({ + paging_properties: [{ page_index: 0 }, { page_size: 1 }], + }), + }) + ).hosts.length !== 0 + ); + } catch (error) { + // eslint-disable-next-line no-console + console.error(`error while trying to check if hosts exist`); + // eslint-disable-next-line no-console + console.error(error); + } + return false; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts index 05af1ee062de6..355c2bb5c19fc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_host_result_list.ts @@ -4,8 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostInfo, HostResultList, HostStatus } from '../../../../../common/endpoint/types'; +import { HttpStart } from 'kibana/public'; +import { + GetHostPolicyResponse, + HostInfo, + HostPolicyResponse, + HostResultList, + HostStatus, +} from '../../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; +import { + INGEST_API_AGENT_CONFIGS, + INGEST_API_EPM_PACKAGES, + INGEST_API_PACKAGE_CONFIGS, +} from '../../policy/store/policy_list/services/ingest'; +import { + GetAgentConfigsResponse, + GetPackagesResponse, +} from '../../../../../../ingest_manager/common/types/rest_spec'; +import { GetPolicyListResponse } from '../../policy/types'; + +const generator = new EndpointDocGenerator('seed'); export const mockHostResultList: (options?: { total?: number; @@ -26,7 +45,6 @@ export const mockHostResultList: (options?: { const hosts = []; for (let index = 0; index < actualCountToReturn; index++) { - const generator = new EndpointDocGenerator('seed'); hosts.push({ metadata: generator.generateHostMetadata(), host_status: HostStatus.ERROR, @@ -45,9 +63,133 @@ export const mockHostResultList: (options?: { * returns a mocked API response for retrieving a single host metadata */ export const mockHostDetailsApiResult = (): HostInfo => { - const generator = new EndpointDocGenerator('seed'); return { metadata: generator.generateHostMetadata(), host_status: HostStatus.ERROR, }; }; + +/** + * Mock API handlers used by the Endpoint Host list. It also sets up a list of + * API handlers for Host details based on a list of Host results. + */ +const hostListApiPathHandlerMocks = ({ + hostsResults = mockHostResultList({ total: 3 }).hosts, + epmPackages = [generator.generateEpmPackage()], + endpointPackageConfigs = [], + policyResponse = generator.generatePolicyResponse(), +}: { + /** route handlers will be setup for each individual host in this array */ + hostsResults?: HostResultList['hosts']; + epmPackages?: GetPackagesResponse['response']; + endpointPackageConfigs?: GetPolicyListResponse['items']; + policyResponse?: HostPolicyResponse; +} = {}) => { + const apiHandlers = { + // endpoint package info + [INGEST_API_EPM_PACKAGES]: (): GetPackagesResponse => { + return { + response: epmPackages, + success: true, + }; + }, + + // host list + '/api/endpoint/metadata': (): HostResultList => { + return { + hosts: hostsResults, + request_page_size: 10, + request_page_index: 0, + total: hostsResults?.length || 0, + }; + }, + + // Do policies referenced in host list exist + // just returns 1 single agent config that includes all of the packageConfig IDs provided + [INGEST_API_AGENT_CONFIGS]: (): GetAgentConfigsResponse => { + const agentConfig = generator.generateAgentConfig(); + (agentConfig.package_configs as string[]).push( + ...endpointPackageConfigs.map((packageConfig) => packageConfig.id) + ); + return { + items: [agentConfig], + total: 10, + success: true, + perPage: 10, + page: 1, + }; + }, + + // Policy Response + '/api/endpoint/policy_response': (): GetHostPolicyResponse => { + return { policy_response: policyResponse }; + }, + + // List of Policies (package configs) for onboarding + [INGEST_API_PACKAGE_CONFIGS]: (): GetPolicyListResponse => { + return { + items: endpointPackageConfigs, + page: 1, + perPage: 10, + total: endpointPackageConfigs?.length, + success: true, + }; + }, + }; + + // Build a GET route handler for each host details based on the list of Hosts passed on input + if (hostsResults) { + hostsResults.forEach((host) => { + // @ts-ignore + apiHandlers[`/api/endpoint/metadata/${host.metadata.host.id}`] = () => host; + }); + } + + return apiHandlers; +}; + +/** + * Sets up mock impelementations in support of the Hosts list view + * + * @param mockedHttpService + * @param hostsResults + * @param pathHandlersOptions + */ +export const setHostListApiMockImplementation: ( + mockedHttpService: jest.Mocked, + apiResponses?: Parameters[0] +) => void = ( + mockedHttpService, + { hostsResults = mockHostResultList({ total: 3 }).hosts, ...pathHandlersOptions } = {} +) => { + const apiHandlers = hostListApiPathHandlerMocks({ ...pathHandlersOptions, hostsResults }); + + mockedHttpService.post + .mockImplementation(async (...args) => { + throw new Error(`un-expected call to http.post: ${args}`); + }) + // First time called, return list of hosts + .mockImplementationOnce(async () => { + return apiHandlers['/api/endpoint/metadata'](); + }); + + // If the hosts list results is zero, then mock the second call to `/metadata` to return + // empty list - indicating there are no hosts currently present on the system + if (!hostsResults.length) { + mockedHttpService.post.mockImplementationOnce(async () => { + return apiHandlers['/api/endpoint/metadata'](); + }); + } + + // Setup handling of GET requests + mockedHttpService.get.mockImplementation(async (...args) => { + const [path] = args; + if (typeof path === 'string') { + if (apiHandlers[path]) { + return apiHandlers[path](); + } + } + + throw new Error(`MOCK: api request does not have a mocked handler: ${path}`); + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 7f68baa4b85bd..e54f7df4d4f75 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -29,6 +29,7 @@ export const initialHostListState: Immutable = { policyItemsLoading: false, endpointPackageInfo: undefined, nonExistingPolicies: {}, + hostsExist: true, }; /* eslint-disable-next-line complexity */ @@ -125,6 +126,11 @@ export const hostListReducer: ImmutableReducer = ( ...state, endpointPackageInfo: action.payload, }; + } else if (action.type === 'serverReturnedHostExistValue') { + return { + ...state, + hostsExist: action.payload, + }; } else if (action.type === 'userChangedUrl') { const newState: Immutable = { ...state, @@ -181,6 +187,7 @@ export const hostListReducer: ImmutableReducer = ( error: undefined, detailsError: undefined, policyResponseError: undefined, + hostsExist: true, }; } return state; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index 6e0823a920413..ca006f21c29ac 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -203,3 +203,9 @@ export const policyResponseStatus: (state: Immutable) => string = cre export const nonExistingPolicies: ( state: Immutable ) => Immutable = (state) => state.nonExistingPolicies; + +/** + * Return boolean that indicates whether hosts exist + * @param state + */ +export const hostsExist: (state: Immutable) => boolean = (state) => state.hostsExist; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index 582a59cfd7605..6c949e9700b9a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -52,6 +52,8 @@ export interface HostState { endpointPackageInfo?: GetPackagesResponse['response'][0]; /** tracks the list of policies IDs used in Host metadata that may no longer exist */ nonExistingPolicies: Record; + /** Tracks whether hosts exist and helps control if onboarding should be visible */ + hostsExist: boolean; } /** diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 9d49c8705affe..3e00a5cc33db1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -8,18 +8,22 @@ import React from 'react'; import * as reactTestingLibrary from '@testing-library/react'; import { HostList } from './index'; -import { mockHostDetailsApiResult, mockHostResultList } from '../store/mock_host_result_list'; -import { mockPolicyResultList } from '../../policy/store/policy_list/mock_policy_result_list'; +import { + mockHostDetailsApiResult, + mockHostResultList, + setHostListApiMockImplementation, +} from '../store/mock_host_result_list'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { HostInfo, + HostPolicyResponse, HostPolicyResponseActionStatus, HostPolicyResponseAppliedAction, HostStatus, } from '../../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; -import { AppAction } from '../../../../common/store/actions'; import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants'; +import { mockPolicyResultList } from '../../policy/store/policy_list/test_mock_utils'; jest.mock('../../../../common/components/link_to'); @@ -35,6 +39,9 @@ describe('when on the hosts page', () => { const mockedContext = createAppRootMockRenderer(); ({ history, store, coreStart, middlewareSpy } = mockedContext); render = () => mockedContext.render(); + reactTestingLibrary.act(() => { + history.push('/hosts'); + }); }); it('should NOT display timeline', async () => { @@ -43,35 +50,29 @@ describe('when on the hosts page', () => { expect(timelineFlyout).toBeNull(); }); - it('should show the empty state when there are no hosts or polices', async () => { - const renderResult = render(); - // Initially, there are no hosts or policies, so we prompt to add policies first. - const table = await renderResult.findByTestId('emptyPolicyTable'); - expect(table).not.toBeNull(); - }); - - describe('when there are policies, but no hosts', () => { + describe('when there are no hosts or polices', () => { beforeEach(() => { - reactTestingLibrary.act(() => { - const hostListData = mockHostResultList({ total: 0 }); - coreStart.http.get.mockReturnValue(Promise.resolve(hostListData)); - const hostAction: AppAction = { - type: 'serverReturnedHostList', - payload: hostListData, - }; - store.dispatch(hostAction); + setHostListApiMockImplementation(coreStart.http, { + hostsResults: [], + }); + }); - jest.clearAllMocks(); + it('should show the empty state when there are no hosts or polices', async () => { + const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); + }); + // Initially, there are no hosts or policies, so we prompt to add policies first. + const table = await renderResult.findByTestId('emptyPolicyTable'); + expect(table).not.toBeNull(); + }); + }); - const policyListData = mockPolicyResultList({ total: 3 }); - coreStart.http.get.mockReturnValue(Promise.resolve(policyListData)); - const policyAction: AppAction = { - type: 'serverReturnedPoliciesForOnboarding', - payload: { - policyItems: policyListData.items, - }, - }; - store.dispatch(policyAction); + describe('when there are policies, but no hosts', () => { + beforeEach(async () => { + setHostListApiMockImplementation(coreStart.http, { + hostsResults: [], + endpointPackageConfigs: mockPolicyResultList({ total: 3 }).items, }); }); afterEach(() => { @@ -80,18 +81,27 @@ describe('when on the hosts page', () => { it('should show the no hosts empty state', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); + }); const emptyHostsTable = await renderResult.findByTestId('emptyHostsTable'); expect(emptyHostsTable).not.toBeNull(); }); it('should display the onboarding steps', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); + }); const onboardingSteps = await renderResult.findByTestId('onboardingSteps'); expect(onboardingSteps).not.toBeNull(); }); it('should show policy selection', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedPoliciesForOnboarding'); + }); const onboardingPolicySelect = await renderResult.findByTestId('onboardingPolicySelect'); expect(onboardingPolicySelect).not.toBeNull(); }); @@ -112,39 +122,54 @@ describe('when on the hosts page', () => { let firstPolicyID: string; beforeEach(() => { reactTestingLibrary.act(() => { - const hostListData = mockHostResultList({ total: 4 }); - firstPolicyID = hostListData.hosts[0].metadata.Endpoint.policy.applied.id; + const hostListData = mockHostResultList({ total: 4 }).hosts; + + firstPolicyID = hostListData[0].metadata.Endpoint.policy.applied.id; + [HostStatus.ERROR, HostStatus.ONLINE, HostStatus.OFFLINE, HostStatus.UNENROLLING].forEach( (status, index) => { - hostListData.hosts[index] = { - metadata: hostListData.hosts[index].metadata, + hostListData[index] = { + metadata: hostListData[index].metadata, host_status: status, }; } ); - hostListData.hosts.forEach((item, index) => { + hostListData.forEach((item, index) => { generatedPolicyStatuses[index] = item.metadata.Endpoint.policy.applied.status; }); - const action: AppAction = { - type: 'serverReturnedHostList', - payload: hostListData, - }; - store.dispatch(action); + + // Make sure that the first policy id in the host result is not set as non-existent + const ingestPackageConfigs = mockPolicyResultList({ total: 1 }).items; + ingestPackageConfigs[0].id = firstPolicyID; + + setHostListApiMockImplementation(coreStart.http, { + hostsResults: hostListData, + endpointPackageConfigs: ingestPackageConfigs, + }); }); }); it('should display rows in the table', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const rows = await renderResult.findAllByRole('row'); expect(rows).toHaveLength(5); }); it('should show total', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const total = await renderResult.findByTestId('hostListTableTotal'); expect(total.textContent).toEqual('4 Hosts'); }); it('should display correct status', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const hostStatuses = await renderResult.findAllByTestId('rowHostStatus'); expect(hostStatuses[0].textContent).toEqual('Error'); @@ -168,6 +193,9 @@ describe('when on the hosts page', () => { it('should display correct policy status', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const policyStatuses = await renderResult.findAllByTestId('rowPolicyStatus'); policyStatuses.forEach((status, index) => { @@ -184,6 +212,9 @@ describe('when on the hosts page', () => { it('should display policy name as a link', async () => { const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const firstPolicyName = (await renderResult.findAllByTestId('policyNameCellLink'))[0]; expect(firstPolicyName).not.toBeNull(); expect(firstPolicyName.getAttribute('href')).toContain(`policy/${firstPolicyID}`); @@ -192,17 +223,10 @@ describe('when on the hosts page', () => { describe('when the user clicks the first hostname in the table', () => { let renderResult: reactTestingLibrary.RenderResult; beforeEach(async () => { - const hostDetailsApiResponse = mockHostDetailsApiResult(); - - coreStart.http.get.mockReturnValue(Promise.resolve(hostDetailsApiResponse)); - reactTestingLibrary.act(() => { - store.dispatch({ - type: 'serverReturnedHostDetails', - payload: hostDetailsApiResponse, - }); - }); - renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedHostList'); + }); const hostNameLinks = await renderResult.findAllByTestId('hostnameCellLink'); if (hostNameLinks.length) { reactTestingLibrary.fireEvent.click(hostNameLinks[0]); @@ -221,9 +245,11 @@ describe('when on the hosts page', () => { describe('when there is a selected host in the url', () => { let hostDetails: HostInfo; let agentId: string; - const dispatchServerReturnedHostPolicyResponse = ( + let renderAndWaitForData: () => Promise>; + + const createPolicyResponse = ( overallStatus: HostPolicyResponseActionStatus = HostPolicyResponseActionStatus.success - ) => { + ): HostPolicyResponse => { const policyResponse = docGenerator.generatePolicyResponse(); const malwareResponseConfigurations = policyResponse.Endpoint.policy.applied.response.configurations.malware; @@ -269,21 +295,28 @@ describe('when on the hosts page', () => { policyResponse.Endpoint.policy.applied.actions.push(unknownAction); malwareResponseConfigurations.concerned_actions.push(unknownAction.name); + return policyResponse; + }; + + const dispatchServerReturnedHostPolicyResponse = ( + overallStatus: HostPolicyResponseActionStatus = HostPolicyResponseActionStatus.success + ) => { reactTestingLibrary.act(() => { store.dispatch({ type: 'serverReturnedHostPolicyResponse', payload: { - policy_response: policyResponse, + policy_response: createPolicyResponse(overallStatus), }, }); }); }; - beforeEach(() => { + beforeEach(async () => { const { host_status, metadata: { host, ...details }, } = mockHostDetailsApiResult(); + hostDetails = { host_status, metadata: { @@ -297,34 +330,37 @@ describe('when on the hosts page', () => { agentId = hostDetails.metadata.elastic.agent.id; - coreStart.http.get.mockReturnValue(Promise.resolve(hostDetails)); + const policy = docGenerator.generatePolicyPackageConfig(); + policy.id = hostDetails.metadata.Endpoint.policy.applied.id; - reactTestingLibrary.act(() => { - history.push({ - ...history.location, - search: '?selected_host=1', - }); + setHostListApiMockImplementation(coreStart.http, { + hostsResults: [hostDetails], + endpointPackageConfigs: [policy], }); + reactTestingLibrary.act(() => { - store.dispatch({ - type: 'serverReturnedHostDetails', - payload: hostDetails, - }); + history.push('/hosts?selected_host=1'); }); + + renderAndWaitForData = async () => { + const renderResult = render(); + await middlewareSpy.waitForAction('serverReturnedHostDetails'); + return renderResult; + }; }); afterEach(() => { jest.clearAllMocks(); }); - it('should show the flyout', () => { - const renderResult = render(); + it('should show the flyout', async () => { + const renderResult = await renderAndWaitForData(); return renderResult.findByTestId('hostDetailsFlyout').then((flyout) => { expect(flyout).not.toBeNull(); }); }); it('should display policy name value as a link', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue'); expect(policyDetailsLink).not.toBeNull(); expect(policyDetailsLink.getAttribute('href')).toEqual( @@ -333,10 +369,7 @@ describe('when on the hosts page', () => { }); it('should update the URL when policy name link is clicked', async () => { - const policyItem = mockPolicyResultList({ total: 1 }).items[0]; - coreStart.http.get.mockReturnValue(Promise.resolve({ item: policyItem })); - - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue'); const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { @@ -349,7 +382,7 @@ describe('when on the hosts page', () => { }); it('should display policy status value as a link', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); expect(policyStatusLink).not.toBeNull(); expect(policyStatusLink.getAttribute('href')).toEqual( @@ -358,7 +391,7 @@ describe('when on the hosts page', () => { }); it('should update the URL when policy status link is clicked', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { @@ -371,7 +404,7 @@ describe('when on the hosts page', () => { }); it('should display Success overall policy status', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.success); }); @@ -385,7 +418,7 @@ describe('when on the hosts page', () => { }); it('should display Warning overall policy status', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.warning); }); @@ -399,7 +432,7 @@ describe('when on the hosts page', () => { }); it('should display Failed overall policy status', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.failure); }); @@ -413,7 +446,7 @@ describe('when on the hosts page', () => { }); it('should display Unknown overall policy status', async () => { - const renderResult = render(); + const renderResult = await renderAndWaitForData(); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse('' as HostPolicyResponseActionStatus); }); @@ -428,7 +461,7 @@ describe('when on the hosts page', () => { it('should include the link to reassignment in Ingest', async () => { coreStart.application.getUrlForApp.mockReturnValue('/app/ingestManager'); - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const linkToReassign = await renderResult.findByTestId('hostDetailsLinkToIngest'); expect(linkToReassign).not.toBeNull(); expect(linkToReassign.textContent).toEqual('Reassign Configuration'); @@ -440,7 +473,7 @@ describe('when on the hosts page', () => { describe('when link to reassignment in Ingest is clicked', () => { beforeEach(async () => { coreStart.application.getUrlForApp.mockReturnValue('/app/ingestManager'); - const renderResult = render(); + const renderResult = await renderAndWaitForData(); const linkToReassign = await renderResult.findByTestId('hostDetailsLinkToIngest'); reactTestingLibrary.act(() => { reactTestingLibrary.fireEvent.click(linkToReassign); @@ -461,13 +494,14 @@ describe('when on the hosts page', () => { } throw new Error(`POST to '${requestOptions.path}' does not have a mock response!`); }); - renderResult = render(); + renderResult = await renderAndWaitForData(); const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl'); reactTestingLibrary.act(() => { reactTestingLibrary.fireEvent.click(policyStatusLink); }); await userChangedUrlChecker; + await middlewareSpy.waitForAction('serverReturnedHostPolicyResponse'); reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 2692f7791b7c0..58442ab417b60 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -89,6 +89,7 @@ export const HostList = () => { selectedPolicyId, policyItemsLoading, endpointPackageVersion, + hostsExist, } = useHostSelector(selector); const { formatUrl, search } = useFormatUrl(SecurityPageName.administration); @@ -329,7 +330,7 @@ export const HostList = () => { }, [formatUrl, queryParams, search]); const renderTableOrEmptyState = useMemo(() => { - if (!loading && listData && listData.length > 0) { + if (hostsExist) { return ( { error={listError?.message} pagination={paginationSetup} onChange={onTableChange} + loading={loading} /> ); } else if (!policyItemsLoading && policyItems && policyItems.length > 0) { @@ -356,19 +358,20 @@ export const HostList = () => { ); } }, [ - listData, + loading, + hostsExist, + policyItemsLoading, policyItems, + listData, columns, - loading, + listError?.message, paginationSetup, onTableChange, - listError?.message, - handleCreatePolicyClick, handleDeployEndpointsClick, - handleSelectableOnChange, selectedPolicyId, + handleSelectableOnChange, selectionOptions, - policyItemsLoading, + handleCreatePolicyClick, ]); return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts index 8203aae244f24..6ee6a4232f7cf 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts @@ -143,6 +143,7 @@ describe('policy list store concerns', () => { isLoading: false, isDeleting: false, deleteStatus: undefined, + endpointPackageInfo: undefined, pageIndex: 0, pageSize: 10, total: 0, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts index b4e1da4e43da3..6fe555113617d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts @@ -29,6 +29,7 @@ export const policyListMiddlewareFactory: ImmutableMiddlewareFactory GetPolicyListResponse = (options = {}) => { - const { - total = 1, - request_page_size: requestPageSize = 10, - request_page_index: requestPageIndex = 0, - } = options; - - // Skip any that are before the page we're on - const numberToSkip = requestPageSize * requestPageIndex; - - // total - numberToSkip is the count of non-skipped ones, but return no more than a pageSize, and no less than 0 - const actualCountToReturn = Math.max(Math.min(total - numberToSkip, requestPageSize), 0); - - const policies = []; - for (let index = 0; index < actualCountToReturn; index++) { - const generator = new EndpointDocGenerator('seed'); - policies.push(generator.generatePolicyPackageConfig()); - } - const mock: GetPolicyListResponse = { - items: policies, - total, - page: requestPageIndex, - perPage: requestPageSize, - success: true, - }; - return mock; -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts index 7daa500ef1884..83cd8558306a0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts @@ -12,7 +12,7 @@ import { } from './ingest'; import { httpServiceMock } from '../../../../../../../../../../src/core/public/mocks'; import { PACKAGE_CONFIG_SAVED_OBJECT_TYPE } from '../../../../../../../../ingest_manager/common'; -import { apiPathMockResponseProviders } from '../test_mock_utils'; +import { policyListApiPathHandlers } from '../test_mock_utils'; describe('ingest service', () => { let http: ReturnType; @@ -61,7 +61,9 @@ describe('ingest service', () => { describe('sendGetEndpointSecurityPackage()', () => { it('should query EPM with category=security', async () => { - http.get.mockReturnValue(apiPathMockResponseProviders[INGEST_API_EPM_PACKAGES]()); + http.get.mockReturnValue( + Promise.resolve(policyListApiPathHandlers()[INGEST_API_EPM_PACKAGES]()) + ); await sendGetEndpointSecurityPackage(http); expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/epm/packages', { query: { category: 'security' }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts index c6e6146f4d5e4..266faf9eae32c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts @@ -20,7 +20,7 @@ import { NewPolicyData } from '../../../../../../../common/endpoint/types'; const INGEST_API_ROOT = `/api/ingest_manager`; export const INGEST_API_PACKAGE_CONFIGS = `${INGEST_API_ROOT}/package_configs`; -const INGEST_API_AGENT_CONFIGS = `${INGEST_API_ROOT}/agent_configs`; +export const INGEST_API_AGENT_CONFIGS = `${INGEST_API_ROOT}/agent_configs`; const INGEST_API_FLEET = `${INGEST_API_ROOT}/fleet`; const INGEST_API_FLEET_AGENT_STATUS = `${INGEST_API_FLEET}/agent-status`; export const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts index b5c67cc2c2014..3c9d5fde9b826 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts @@ -5,122 +5,84 @@ */ import { HttpStart } from 'kibana/public'; -import { INGEST_API_PACKAGE_CONFIGS, INGEST_API_EPM_PACKAGES } from './services/ingest'; +import { INGEST_API_EPM_PACKAGES, INGEST_API_PACKAGE_CONFIGS } from './services/ingest'; import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; import { GetPolicyListResponse } from '../../types'; -import { - KibanaAssetReference, - EsAssetReference, - GetPackagesResponse, - InstallationStatus, -} from '../../../../../../../ingest_manager/common'; +import { GetPackagesResponse } from '../../../../../../../ingest_manager/common'; const generator = new EndpointDocGenerator('policy-list'); -/** - * a list of API paths response mock providers - */ -export const apiPathMockResponseProviders = { - [INGEST_API_EPM_PACKAGES]: () => - Promise.resolve({ - response: [ - { - name: 'endpoint', - title: 'Elastic Endpoint', - version: '0.5.0', - description: 'This is the Elastic Endpoint package.', - type: 'solution', - download: '/epr/endpoint/endpoint-0.5.0.tar.gz', - path: '/package/endpoint/0.5.0', - icons: [ - { - src: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg', - size: '16x16', - type: 'image/svg+xml', - }, - ], - status: 'installed' as InstallationStatus, - savedObject: { - type: 'epm-packages', - id: 'endpoint', - attributes: { - installed_kibana: [ - { id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' }, - { id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, - { id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, - { id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, - { id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, - { id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' }, - ] as KibanaAssetReference[], - installed_es: [ - { id: 'logs-endpoint.alerts', type: 'index_template' }, - { id: 'events-endpoint', type: 'index_template' }, - { id: 'logs-endpoint.events.file', type: 'index_template' }, - { id: 'logs-endpoint.events.library', type: 'index_template' }, - { id: 'metrics-endpoint.metadata', type: 'index_template' }, - { id: 'metrics-endpoint.metadata_mirror', type: 'index_template' }, - { id: 'logs-endpoint.events.network', type: 'index_template' }, - { id: 'metrics-endpoint.policy', type: 'index_template' }, - { id: 'logs-endpoint.events.process', type: 'index_template' }, - { id: 'logs-endpoint.events.registry', type: 'index_template' }, - { id: 'logs-endpoint.events.security', type: 'index_template' }, - { id: 'metrics-endpoint.telemetry', type: 'index_template' }, - ] as EsAssetReference[], - es_index_patterns: { - alerts: 'logs-endpoint.alerts-*', - events: 'events-endpoint-*', - file: 'logs-endpoint.events.file-*', - library: 'logs-endpoint.events.library-*', - metadata: 'metrics-endpoint.metadata-*', - metadata_mirror: 'metrics-endpoint.metadata_mirror-*', - network: 'logs-endpoint.events.network-*', - policy: 'metrics-endpoint.policy-*', - process: 'logs-endpoint.events.process-*', - registry: 'logs-endpoint.events.registry-*', - security: 'logs-endpoint.events.security-*', - telemetry: 'metrics-endpoint.telemetry-*', - }, - name: 'endpoint', - version: '0.5.0', - internal: false, - removable: false, - }, - references: [], - updated_at: '2020-06-24T14:41:23.098Z', - version: 'Wzc0LDFd', - }, - }, - ], - success: true, - }), -}; - /** * It sets the mock implementation on the necessary http methods to support the policy list view * @param mockedHttpService - * @param responseItems + * @param totalPolicies */ export const setPolicyListApiMockImplementation = ( mockedHttpService: jest.Mocked, - responseItems: GetPolicyListResponse['items'] = [generator.generatePolicyPackageConfig()] + totalPolicies: number = 1 ): void => { - mockedHttpService.get.mockImplementation((...args) => { + const policyApiHandlers = policyListApiPathHandlers(totalPolicies); + + mockedHttpService.get.mockImplementation(async (...args) => { const [path] = args; if (typeof path === 'string') { - if (path === INGEST_API_PACKAGE_CONFIGS) { - return Promise.resolve({ - items: responseItems, - total: 10, - page: 1, - perPage: 10, - success: true, - }); - } - - if (apiPathMockResponseProviders[path]) { - return apiPathMockResponseProviders[path](); + if (policyApiHandlers[path]) { + return policyApiHandlers[path](); } } return Promise.reject(new Error(`MOCK: unknown policy list api: ${path}`)); }); }; + +/** + * Returns the response body for a call to get the list of Policies + * @param options + */ +export const mockPolicyResultList: (options?: { + total?: number; + request_page_size?: number; + request_page_index?: number; +}) => GetPolicyListResponse = (options = {}) => { + const { + total = 1, + request_page_size: requestPageSize = 10, + request_page_index: requestPageIndex = 0, + } = options; + + // Skip any that are before the page we're on + const numberToSkip = requestPageSize * requestPageIndex; + + // total - numberToSkip is the count of non-skipped ones, but return no more than a pageSize, and no less than 0 + const actualCountToReturn = Math.max(Math.min(total - numberToSkip, requestPageSize), 0); + + const policies = []; + for (let index = 0; index < actualCountToReturn; index++) { + policies.push(generator.generatePolicyPackageConfig()); + } + const mock: GetPolicyListResponse = { + items: policies, + total, + page: requestPageIndex, + perPage: requestPageSize, + success: true, + }; + return mock; +}; + +/** + * Returns an object comprised of the API path as the key along with a function that + * returns that API's result value + */ +export const policyListApiPathHandlers = (totalPolicies: number = 1) => { + return { + [INGEST_API_PACKAGE_CONFIGS]: () => { + return mockPolicyResultList({ total: totalPolicies }); + }, + [INGEST_API_EPM_PACKAGES]: (): GetPackagesResponse => { + return { + response: [generator.generateEpmPackage()], + success: true, + }; + }, + }; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index 03ab32dcb2b66..c81ffb0060c88 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -11,7 +11,7 @@ import { PolicyDetails } from './policy_details'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { getPolicyDetailPath, getHostListPath } from '../../../common/routing'; -import { apiPathMockResponseProviders } from '../store/policy_list/test_mock_utils'; +import { policyListApiPathHandlers } from '../store/policy_list/test_mock_utils'; jest.mock('../../../../common/components/link_to'); @@ -80,6 +80,8 @@ describe('Policy Details', () => { policyPackageConfig = generator.generatePolicyPackageConfig(); policyPackageConfig.id = '1'; + const policyListApiHandlers = policyListApiPathHandlers(); + http.get.mockImplementation((...args) => { const [path] = args; if (typeof path === 'string') { @@ -103,9 +105,9 @@ describe('Policy Details', () => { // Get package data // Used in tests that route back to the list - if (apiPathMockResponseProviders[path]) { + if (policyListApiHandlers[path]) { asyncActions = asyncActions.then(async () => sleep()); - return apiPathMockResponseProviders[path](); + return Promise.resolve(policyListApiHandlers[path]()); } } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx index e35c97698f5cb..97eaceff91e9c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx @@ -5,12 +5,9 @@ */ import React from 'react'; -import * as reactTestingLibrary from '@testing-library/react'; - import { PolicyList } from './index'; -import { mockPolicyResultList } from '../store/policy_list/mock_policy_result_list'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; -import { AppAction } from '../../../../common/store/actions'; +import { setPolicyListApiMockImplementation } from '../store/policy_list/test_mock_utils'; jest.mock('../../../../common/components/link_to'); @@ -18,11 +15,12 @@ jest.mock('../../../../common/components/link_to'); describe.skip('when on the policies page', () => { let render: () => ReturnType; let history: AppContextTestRender['history']; - let store: AppContextTestRender['store']; + let coreStart: AppContextTestRender['coreStart']; + let middlewareSpy: AppContextTestRender['middlewareSpy']; beforeEach(() => { const mockedContext = createAppRootMockRenderer(); - ({ history, store } = mockedContext); + ({ history, coreStart, middlewareSpy } = mockedContext); render = () => mockedContext.render(); }); @@ -46,34 +44,30 @@ describe.skip('when on the policies page', () => { describe('when list data loads', () => { let firstPolicyID: string; - beforeEach(() => { - reactTestingLibrary.act(() => { - history.push('/policy'); - reactTestingLibrary.act(() => { - const policyListData = mockPolicyResultList({ total: 3 }); - firstPolicyID = policyListData.items[0].id; - const action: AppAction = { - type: 'serverReturnedPolicyListData', - payload: { - policyItems: policyListData.items, - total: policyListData.total, - pageSize: policyListData.perPage, - pageIndex: policyListData.page, - }, - }; - store.dispatch(action); - }); - }); + const renderList = async () => { + const renderResult = render(); + history.push('/policy'); + await Promise.all([ + middlewareSpy + .waitForAction('serverReturnedPolicyListData') + .then((action) => (firstPolicyID = action.payload.policyItems[0].id)), + // middlewareSpy.waitForAction('serverReturnedAgentConfigListData'), + ]); + return renderResult; + }; + + beforeEach(async () => { + setPolicyListApiMockImplementation(coreStart.http, 3); }); it('should display rows in the table', async () => { - const renderResult = render(); + const renderResult = await renderList(); const rows = await renderResult.findAllByRole('row'); expect(rows).toHaveLength(4); }); it('should display policy name value as a link', async () => { - const renderResult = render(); + const renderResult = await renderList(); const policyNameLink = (await renderResult.findAllByTestId('policyNameLink'))[0]; expect(policyNameLink).not.toBeNull(); expect(policyNameLink.getAttribute('href')).toContain(`policy/${firstPolicyID}`); From 111e8758907383baea03ccefc647fc7a2c8c6e7f Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 30 Jul 2020 10:53:44 -0700 Subject: [PATCH 2/6] [kbn/optimizer] restore x-pack bundle banner (#73767) Co-authored-by: spalger --- .../mock_repo/x-pack/baz/kibana.json | 4 +++ .../mock_repo/x-pack/baz/public/index.ts | 21 ++++++++++++ .../kbn-optimizer/src/common/bundle.test.ts | 2 ++ packages/kbn-optimizer/src/common/bundle.ts | 14 ++++++++ .../basic_optimization.test.ts.snap | 32 +++++++++++++++++ .../basic_optimization.test.ts | 34 ++++++++++++++----- .../src/optimizer/get_plugin_bundles.test.ts | 23 +++++++++++++ .../src/optimizer/get_plugin_bundles.ts | 6 ++++ .../src/worker/webpack.config.ts | 1 + src/dev/precommit_hook/casing_check_config.js | 1 + 10 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/public/index.ts diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json new file mode 100644 index 0000000000000..10602d2e7981a --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json @@ -0,0 +1,4 @@ +{ + "id": "baz", + "ui": true +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/public/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/public/index.ts new file mode 100644 index 0000000000000..7313de07be04c --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/public/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// eslint-disable-next-line no-console +console.log('plugin in an x-pack dir'); diff --git a/packages/kbn-optimizer/src/common/bundle.test.ts b/packages/kbn-optimizer/src/common/bundle.test.ts index 6197a08485854..b8f9b94379f20 100644 --- a/packages/kbn-optimizer/src/common/bundle.test.ts +++ b/packages/kbn-optimizer/src/common/bundle.test.ts @@ -48,6 +48,7 @@ it('creates cache keys', () => { "/foo/bar/c": 789, }, "spec": Object { + "banner": undefined, "contextDir": "/foo/bar", "id": "bar", "manifestPath": undefined, @@ -80,6 +81,7 @@ it('parses bundles from JSON specs', () => { expect(bundles).toMatchInlineSnapshot(` Array [ Bundle { + "banner": undefined, "cache": BundleCache { "path": "/foo/bar/target/.kbn-optimizer-cache", "state": undefined, diff --git a/packages/kbn-optimizer/src/common/bundle.ts b/packages/kbn-optimizer/src/common/bundle.ts index a354da7a21521..25b37ace09a8f 100644 --- a/packages/kbn-optimizer/src/common/bundle.ts +++ b/packages/kbn-optimizer/src/common/bundle.ts @@ -43,6 +43,8 @@ export interface BundleSpec { readonly sourceRoot: string; /** Absolute path to the directory where output should be written */ readonly outputDir: string; + /** Banner that should be written to all bundle JS files */ + readonly banner?: string; /** Absolute path to a kibana.json manifest file, if omitted we assume there are not dependenices */ readonly manifestPath?: string; } @@ -64,6 +66,8 @@ export class Bundle { public readonly sourceRoot: BundleSpec['sourceRoot']; /** Absolute path to the output directory for this bundle */ public readonly outputDir: BundleSpec['outputDir']; + /** Banner that should be written to all bundle JS files */ + public readonly banner: BundleSpec['banner']; /** * Absolute path to a manifest file with "requiredBundles" which will be * used to allow bundleRefs from this bundle to the exports of another bundle. @@ -81,6 +85,7 @@ export class Bundle { this.sourceRoot = spec.sourceRoot; this.outputDir = spec.outputDir; this.manifestPath = spec.manifestPath; + this.banner = spec.banner; this.cache = new BundleCache(Path.resolve(this.outputDir, '.kbn-optimizer-cache')); } @@ -112,6 +117,7 @@ export class Bundle { sourceRoot: this.sourceRoot, outputDir: this.outputDir, manifestPath: this.manifestPath, + banner: this.banner, }; } @@ -220,6 +226,13 @@ export function parseBundles(json: string) { } } + const { banner } = spec; + if (banner !== undefined) { + if (!(typeof banner === 'string')) { + throw new Error('`bundles[]` must have a string `banner` property'); + } + } + return new Bundle({ type, id, @@ -227,6 +240,7 @@ export function parseBundles(json: string) { contextDir, sourceRoot, outputDir, + banner, manifestPath, }); } diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 109188e163d06..5f44d8068e694 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -4,6 +4,7 @@ exports[`builds expected bundles, saves bundle counts to metadata: OptimizerConf OptimizerConfig { "bundles": Array [ Bundle { + "banner": undefined, "cache": BundleCache { "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public/.kbn-optimizer-cache, "state": undefined, @@ -19,6 +20,7 @@ OptimizerConfig { "type": "plugin", }, Bundle { + "banner": undefined, "cache": BundleCache { "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public/.kbn-optimizer-cache, "state": undefined, @@ -33,6 +35,24 @@ OptimizerConfig { "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, "type": "plugin", }, + Bundle { + "banner": "/*! 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. */ +", + "cache": BundleCache { + "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/target/public/.kbn-optimizer-cache, + "state": undefined, + }, + "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz, + "id": "baz", + "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, + "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/target/public, + "publicDirNames": Array [ + "public", + ], + "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, + "type": "plugin", + }, ], "cache": true, "dist": false, @@ -60,6 +80,13 @@ OptimizerConfig { "isUiPlugin": false, "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/nested/baz/kibana.json, }, + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz, + "extraPublicDirs": Array [], + "id": "baz", + "isUiPlugin": true, + "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, + }, ], "profileWebpack": false, "repoRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, @@ -73,6 +100,11 @@ OptimizerConfig { exports[`prepares assets for distribution: bar bundle 1`] = `"(function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{enumerable:true,get:getter})}};__webpack_require__.r=function(exports){if(typeof Symbol!==\\"undefined\\"&&Symbol.toStringTag){Object.defineProperty(exports,Symbol.toStringTag,{value:\\"Module\\"})}Object.defineProperty(exports,\\"__esModule\\",{value:true})};__webpack_require__.t=function(value,mode){if(mode&1)value=__webpack_require__(value);if(mode&8)return value;if(mode&4&&typeof value===\\"object\\"&&value&&value.__esModule)return value;var ns=Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns,\\"default\\",{enumerable:true,value:value});if(mode&2&&typeof value!=\\"string\\")for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module[\\"default\\"]}:function getModuleExports(){return module};__webpack_require__.d(getter,\\"a\\",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p=\\"\\";return __webpack_require__(__webpack_require__.s=5)})([function(module,exports,__webpack_require__){\\"use strict\\";var isOldIE=function isOldIE(){var memo;return function memorize(){if(typeof memo===\\"undefined\\"){memo=Boolean(window&&document&&document.all&&!window.atob)}return memo}}();var getTarget=function getTarget(){var memo={};return function memorize(target){if(typeof memo[target]===\\"undefined\\"){var styleTarget=document.querySelector(target);if(window.HTMLIFrameElement&&styleTarget instanceof window.HTMLIFrameElement){try{styleTarget=styleTarget.contentDocument.head}catch(e){styleTarget=null}}memo[target]=styleTarget}return memo[target]}}();var stylesInDom=[];function getIndexByIdentifier(identifier){var result=-1;for(var i=0;i { it('builds expected bundles, saves bundle counts to metadata', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, - pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], + pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins'), Path.resolve(MOCK_REPO_DIR, 'x-pack')], maxWorkerCount: 1, dist: false, }); @@ -100,7 +100,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { (msg.event?.type === 'bundle cached' || msg.event?.type === 'bundle not cached') && msg.state.phase === 'initializing' ); - assert('produce two bundle cache events while initializing', bundleCacheStates.length === 2); + assert('produce three bundle cache events while initializing', bundleCacheStates.length === 3); const initializedStates = msgs.filter((msg) => msg.state.phase === 'initialized'); assert('produce at least one initialized event', initializedStates.length >= 1); @@ -110,17 +110,17 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { const runningStates = msgs.filter((msg) => msg.state.phase === 'running'); assert( - 'produce two or three "running" states', - runningStates.length === 2 || runningStates.length === 3 + 'produce three to five "running" states', + runningStates.length >= 3 && runningStates.length <= 5 ); const bundleNotCachedEvents = msgs.filter((msg) => msg.event?.type === 'bundle not cached'); - assert('produce two "bundle not cached" events', bundleNotCachedEvents.length === 2); + assert('produce three "bundle not cached" events', bundleNotCachedEvents.length === 3); const successStates = msgs.filter((msg) => msg.state.phase === 'success'); assert( - 'produce one or two "compiler success" states', - successStates.length === 1 || successStates.length === 2 + 'produce one to three "compiler success" states', + successStates.length >= 1 && successStates.length <= 3 ); const otherStates = msgs.filter( @@ -175,12 +175,26 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); + + const baz = config.bundles.find((b) => b.id === 'baz')!; + expect(baz).toBeTruthy(); + baz.cache.refresh(); + expect(baz.cache.getModuleCount()).toBe(3); + + expect(baz.cache.getReferencedFiles()).toMatchInlineSnapshot(` + Array [ + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json, + /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/public/index.ts, + /packages/kbn-optimizer/target/worker/entry_point_creator.js, + /packages/kbn-ui-shared-deps/public_path_module_creator.js, + ] + `); }); it('uses cache on second run and exist cleanly', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, - pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], + pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins'), Path.resolve(MOCK_REPO_DIR, 'x-pack')], maxWorkerCount: 1, dist: false, }); @@ -202,6 +216,7 @@ it('uses cache on second run and exist cleanly', async () => { "initializing", "initializing", "initializing", + "initializing", "initialized", "success", ] @@ -211,7 +226,7 @@ it('uses cache on second run and exist cleanly', async () => { it('prepares assets for distribution', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, - pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], + pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins'), Path.resolve(MOCK_REPO_DIR, 'x-pack')], maxWorkerCount: 1, dist: true, }); @@ -224,6 +239,7 @@ it('prepares assets for distribution', async () => { 'foo async bundle' ); expectFileMatchesSnapshotWithCompression('plugins/bar/target/public/bar.plugin.js', 'bar bundle'); + expectFileMatchesSnapshotWithCompression('x-pack/baz/target/public/baz.plugin.js', 'baz bundle'); }); /** diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts index a70cfc759dd55..a823f66cf767b 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts @@ -48,12 +48,20 @@ it('returns a bundle for core and each plugin', () => { extraPublicDirs: [], manifestPath: '/outside/of/repo/plugins/baz/kibana.json', }, + { + directory: '/repo/x-pack/plugins/box', + id: 'box', + isUiPlugin: true, + extraPublicDirs: [], + manifestPath: '/repo/x-pack/plugins/box/kibana.json', + }, ], '/repo' ).map((b) => b.toSpec()) ).toMatchInlineSnapshot(` Array [ Object { + "banner": undefined, "contextDir": /plugins/foo, "id": "foo", "manifestPath": /plugins/foo/kibana.json, @@ -65,6 +73,7 @@ it('returns a bundle for core and each plugin', () => { "type": "plugin", }, Object { + "banner": undefined, "contextDir": "/outside/of/repo/plugins/baz", "id": "baz", "manifestPath": "/outside/of/repo/plugins/baz/kibana.json", @@ -75,6 +84,20 @@ it('returns a bundle for core and each plugin', () => { "sourceRoot": , "type": "plugin", }, + Object { + "banner": "/*! 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. */ + ", + "contextDir": /x-pack/plugins/box, + "id": "box", + "manifestPath": /x-pack/plugins/box/kibana.json, + "outputDir": /x-pack/plugins/box/target/public, + "publicDirNames": Array [ + "public", + ], + "sourceRoot": , + "type": "plugin", + }, ] `); }); diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts index 04ab992addeec..9350b9464242a 100644 --- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts @@ -24,6 +24,8 @@ import { Bundle } from '../common'; import { KibanaPlatformPlugin } from './kibana_platform_plugins'; export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: string) { + const xpackDirSlash = Path.resolve(repoRoot, 'x-pack') + Path.sep; + return plugins .filter((p) => p.isUiPlugin) .map( @@ -36,6 +38,10 @@ export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: stri contextDir: p.directory, outputDir: Path.resolve(p.directory, 'target/public'), manifestPath: p.manifestPath, + banner: p.directory.startsWith(xpackDirSlash) + ? `/*! Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one or more contributor license agreements.\n` + + ` * Licensed under the Elastic License; you may not use this file except in compliance with the Elastic License. */\n` + : undefined, }) ); } diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 271ad49aee351..3d62ed1636869 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -72,6 +72,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: new CleanWebpackPlugin(), new DisallowedSyntaxPlugin(), new BundleRefsPlugin(bundle, bundleRefs), + ...(bundle.banner ? [new webpack.BannerPlugin({ banner: bundle.banner, raw: true })] : []), ], module: { diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 864bf7515053c..404ad67174681 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -105,6 +105,7 @@ export const IGNORE_DIRECTORY_GLOBS = [ 'test/functional/fixtures/es_archiver/visualize_source-filters', 'packages/kbn-pm/src/utils/__fixtures__/*', 'x-pack/dev-tools', + 'packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack', ]; /** From 1e067b1608adc89cf647f47a24bcecde2d3e99fe Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 30 Jul 2020 14:01:29 -0400 Subject: [PATCH 3/6] [Security Solution][Resolver] Adding resolver backend docs (#73726) * Adding resolver backend docs * Adding more clarity around ancestry array limit Co-authored-by: Elastic Machine --- .../common/endpoint/types.ts | 2 + .../endpoint/routes/resolver/docs/README.md | 216 ++++++++++++++++++ .../resolver/docs/resolver_tree_ancestry.png | Bin 0 -> 19926 bytes .../docs/resolver_tree_children_loop.png | Bin 0 -> 40224 bytes .../resolver_tree_children_pagination.png | Bin 0 -> 32477 bytes ...er_tree_children_pagination_with_after.png | Bin 0 -> 32398 bytes .../docs/resolver_tree_children_simple.png | Bin 0 -> 22889 bytes .../resolver/utils/ancestry_query_handler.ts | 24 +- .../endpoint/routes/resolver/utils/tree.ts | 33 +-- 9 files changed, 242 insertions(+), 33 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_ancestry.png create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_loop.png create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination.png create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_pagination_with_after.png create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_simple.png diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index a982f9ffe8f21..1c24e1abe5a57 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -104,6 +104,8 @@ export interface ResolverChildNode extends ResolverLifecycleNode { * * string: Indicates this is a leaf node and it can be used to continue querying for additional descendants * using this node's entity_id + * + * For more information see the resolver docs on pagination [here](../../server/endpoint/routes/resolver/docs/README.md#L129) */ nextChild?: string | null; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md new file mode 100644 index 0000000000000..1c0692db344c4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/README.md @@ -0,0 +1,216 @@ +# Resolver Backend + +This readme will describe the backend implementation for resolver. + +## Ancestry Array + +The ancestry array is an array of entity_ids. This array is included with each event sent by the elastic endpoint +and defines the ancestors of a particular process. The array is formatted such that [0] of the array contains the direct +parent of the process. [1] of the array contains the grandparent of the process. For example if Process A spawned process +B which spawned process C. Process C's array would be [B,A]. + +The presence of the ancestry array makes querying ancestors and children for a process more efficient. + +## Ancestry Array Limit + +The ancestry array is currently limited to 20 values. The exact limit should not be relied on though. + +## Ancestors + +To query for ancestors of a process leveraging the ancestry array, we first retrieve the lifecycle events for the event_id +passed in. Once we have the origin node we can check to see if the document has the `process.Ext.ancestry` array. If +it does we can perform a search for the values in the array. This will retrieve all the ancestors for the process of interest +up to the limit of the ancestry array. Since the array is capped at 20, if the request is asking for more than 20 +ancestors we will have to examine the most distant ancestor that has been retrieved and use its ancestry array to retrieve +the next set of results to fulfill the request. + +### Pagination + +After the backend gathers the results for an ancestry query, it will set a pagination cursor depending on the results from ES. + +If the number of ancestors we have gathered is equal to the size in the request we don't know if ES has more results or not. So we will set `nextAncestor` to the entity_id of the most distant ancestor retrieved. + +If the request asked for 10 and we only found 8 from ES, we know for sure that there aren't anymore results. In this case we will set `nextAncestor` to `null`. + +### Code + +The code for handling the ancestor logic is in [here](../utils/ancestry_query_handler.ts) + +### Ancestors Multiple Queries Example + +![alt text](./resolver_tree_ancestry.png 'Retrieve ancestors') + +For this example let's assume that the _ancestry array limit_ is 2. The process of interest is A (the entity_id of a node is the character in the circle). Process A has an ancestry array of `[3,2]`, its parent has an ancestry array of `[2,1]` etc. Here is the execution of a request for 3 ancestors for entity_id A. + +**Request:** `GET /resolver/A/ancestry?ancestors=3` + +1. Retrieve lifecycle events for entity_id `A` +2. Retrieve `A`'s start event's ancestry array + 1. In the event that the node of interest does not have an ancestry array, the entity id of it's parent will be used, essentially an ancestry array of length 1, [3] in the example here +3. `A`'s ancestry array is `[3,2]`, query for the lifecycle events for processes with `entity_id` 3 or 2 +4. Check to see if we have retrieved enough ancestors to fulfill the request (we have not, we only received 2 nodes of the 3 that were requested) +5. We haven't so use the most distant ancestor in our result set (process 2) +6. Use process 2's ancestry array to query for the next set of results to fulfill the request +7. Process 2's ancestry array is `[1]` so repeat the process in steps 3-4 and retrieve process with entity_id 1. This fulfills the request so we can return the results for the lifecycle events of A, 3, 2, and 1. + +If process 2 had an ancestry array of `[1,0]` we know that we only need 1 more process to fulfill the request so we can truncate the array to `[1]` instead of searching for all the entries in the array. + +More generically: In the event where our request stops at the x (non-final) position in an ancestry array, we won't search all items in the array, just those up to the x position. The next-cursor will be set to the last ancestor received since there might be more data. + +The `nextAncestor` cursor will be set to `1` in this scenario because we retrieved all 3 ancestors from ES but we don't know if ES has anymore. + +## Descendants + +We can also leverage the ancestry array to query for the descendants of a process. The basic query for the descendants of a process is: _find all processes where their ancestry array contains a particular entity_id_. The results of this query will be sorted in ascending order by the timestamp field. I will try to outline a couple different scenarios for retrieving descendants using the ancestry array below. + +### Start events vs all lifecycle events + +There are two parts to querying for descendant process nodes. When a request comes in for 7 process nodes we need to communicate to ES that we want all of the lifecycle nodes for 7 processes. We could use a query that retrieves all lifecycle events (start, end, etc) but the issue with this is that we need to indicate a `size` in our ES query. If we set the `size` to 7, we will only get 7 lifecycle events. These events could be start, end, or already_running events. It doesn't guarantee that we get all of the lifecycle events for 7 process nodes. + +Instead we can first query for 7 start events, which guarantees that we will have 7 unique process descendants and then we can gather all those entity_ids and do another query for all the lifecycle events for those 7 processes. The downside here is that you have to do two queries to retrieve all the lifecycle events. Optimizations can be made for the first query for the start events by reducing the `_source` that ES returns to only include the `entity_id` and `ancestry`. This will reduce the amount of data that ES has to send back and speed up the query. + +### Scenario Background + +In the scenarios below let's assume the _ancestry array limit_ is 2. The times next to the nodes are the time the node was spawned. The value in red indicates that the process terminated at the time in red. + +Let's also ignore the fact that retrieving the lifecycle events for a descendant actually takes two queries. Let's assume that it's taken care of, and when we say "query for lifecycle events" we get all the lifecycle events back for the descendants using the algorithm described in the [previous section](#start-events-vs-all-lifecycle-events) + +### Simple Scenario + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> E -> F -> G -> H. + +![alt text](./resolver_tree_children_simple.png 'Descendants Simple Scenario') + +**Request:** `GET /resolver/A/children?children=6` + +For this scenario we will retrieve all the lifecycle events for 6 descendants of the process with entity_id `A`. As shown in the diagram above ES has 6 descendants for A so the response to this request will be: `[B, C, E, F, G, H]` because the results are sorted in ascending ordering based on the `timestamp` field which is when the process was started. + +### Looping Scenario + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> E -> F -> G -> J -> K -> H. + +![alt text](./resolver_tree_children_loop.png 'Descendants Looping Scenario') + +**Request:** `GET /resolver/A/children?children=9` + +In this scenario the request is for more descendants than can be retrieved using a single querying with the entity_id `A`. This is because the ancestry array for the descendants in the red section do not have `A` in their ancestry array. So when we query for all process nodes that have `A` in their ancestry array we won't receive D, J, or K. + +Like in the previous scenario, for the first query we will receive `[B, C, E, F, G, H]`. What we want to do next is use a subset of that response that will get us 3 more descendants to fulfill the request for a total of 9. + +We _could_ use `B` and `G` to do this (mostly `B`) but the problem is that when we query for descendants that have `B` or `G` in their ancestry array we will get back some duplicates that we have already received before. For example if we use `B` and `G` we'd get `[C, D, E, F, J, K, H]` but this isn't efficient because we have already received `[E, F, G, H]` from the previous query. + +What we want to do is use the most distant descendants from `A` to make the next query to retrieve the last 3 process nodes to fulfill the request. Those would be `[C, E, F, H]`. So our next query will be: _find all process nodes where their ancestry array contains C or E or F or H_. This query can be limited to a size of 3 so that we will only receive `[D, J, K]`. + +We have now received all the nodes for the request and we can return the results as `[B, C, E, F, G, H, D, J, K]`. + +### Important Caveats + +#### Ordering + +In the previous example the final results are not sorted based on timestamp in ascending order. This is because we had to perform multiple queries to retrieve all the results. The backend will not return the results in sorted order. + +#### Tie breaks on timestamp + +In the previous example we saw that J and K had the same timestamp of `12:13 pm`. The reason they were returned in the order `[J, K]` is because the `event.id` field is used to break ties like this. The `event.id` field is unique for a particular event and an increasing value per ECS's guidelines. Therefore J comes before K because it has an `event.id` of 1 vs 2. + +#### Finding the most distant descendants + +In the previous scenario we saw that we needed to use the most distant descendants from a particular node. To determine if a node is a most distant descendant we can use the ancestry array. Nodes C, E, F, and H all have `A` as their last entry in the ancestry array. This indicates that they are a distant descendant that should be used in the next query. There's one problem with this approach. In a mostly impossible scenario where the node of interest (A) does not have any ancestors, its direct children will also have `A` as the last entry in their ancestry array. + +This edge case will likely never be encountered but we'll try to solve it anyway. To get around this as we iterate over the results from our first query (`[B, C, E, F, G, H]`) we can bucket the ones that have `A` as the last entry in their ancestry array. We bucket the results based on the length of their ancestry array (basically a `Map>`). So after bucketing our results will look like: + +```javascript +{ + 1: [B, G] + 2: [C, E, F, H] +} +``` + +While we are iterating we also keep track of the largest ancestry array that we have seen. In our scenario that will be a size of 2. Then to determine the distant descendants we simply get the nodes that had the largest ancestry array length. In this scenario that'd be `[C, E, F, H]`. + +### Handling Pagination + +#### Pagination Cursor Values + +There are 3 possible states for the pagination cursor for a child node and 2 possible states for the pagination cursor for the node of interest (the node that we are using in the API request). + +Potential cursors for the node of interest: +**a string cursor:** a cursor that can be used to skip the previous set of results. The cursor is a base64 version of a json object with `event.id` and `timestamp` of the last process's start event that was received. +**null:** indicates that no more results can be received using this process's entity_id + +Potential cursors for descendants of the node of interest (these apply to the results of a request): +**a string cursor:** a cursor that can be used to skip the previous set of results. The cursor is a base64 version of a json object with `event.id` and `timestamp` of the last process's start event that was received. This cursor should be used in conjunction with using this process's entity_id for a query. +**undefined:** the node may contain additional children, but we are not aware. To find out, perform additional queries on the node of interest that original returned these results or move down the tree to a descendant of this node to query for more descendants. +**null:** We have found all possible direct children for this node. There may be more descendants but not direct children for this node. + +#### Pagination Examples + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K. + +![alt text](./resolver_tree_children_pagination.png 'Descendants Pagination Scenario') + +Handling pagination for the children API in a little tricky. Let's consider this scenario: + +**Request:** `GET /resolver/A/children?children=3` + +Let's use the diagram above to show the relationship between processes and the data in ES. The response for the request for 3 children is `[B, C, G]`. More process nodes exist in ES so it would be helpful to indicate in the response that there is more data and a way to skip `[B, C, G]` to get the next set of data. A cursor can be set on the response for `A` to point to the last process node in the response which can be sent in another request to retrieve the next set of data. This cursor will contain information from `G` because it has the latest timestamp (in ascending order). + +If another request was made using the returned cursor like the following: + +**Request:** `GET /resolver/A/children?children=5&after=` + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K (the same as above). + +![alt text](./resolver_tree_children_pagination_with_after.png 'Descendants Pagination Scenario Part 2') + +For this request we will do a query for _find all process nodes where their ancestry array has entity_id `A` and use the cursor to skip old results_. The response for this request is `[F, H, E, K]`. The request actually asked for 5 nodes but there was only 4 in ES so only 4 were returned. + +The odd thing about this response is that it did not receive D and J. The problem is that the backend does not have any concept of C in this second request because it was received in the previous one. It will be skipped based on the pagination cursor returned previously. + +This example highlights a scenario where it is not easy for the backend to go back and continue to get the descendants for C because of the limitation of the ancestry array. + +#### Pagination cursor for descendant nodes + +Let's go back to the first request where we got `[B, C, G]`. How could we go about getting the rest of the children for `B`? We have two ways of solving this. First we could determine what the last descendant we had received of `B` and use that as the cursor when returning all the results for this request. That is actually kind of difficult. + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K (the same as above). + +![alt text](./resolver_tree_children_pagination.png 'Descendants Pagination Scenario') + +Let's imagine for a moment that the _ancestry array limit_ is 3 instead of 2. Taking our previous request we would instead get `[B, C, D]` because D started before G. In this case the last descendant for `B` is actually `D` and not `C`. This gets complicated because we'd have to keep track of which descendant was the last (time wise) one for each intermediate process node. In this example we'd need to find the last descendant for both `B` and `C`. We'd have to track the descendants for each process node and build a map to quickly be able to retrieve the last descendant. + +Instead of doing that we could also continue to get the immediate children of `B` by doing another request for `A` like was shown in previous example when using the after cursor. This would guarantee that all children (first level descendants of a node) had been retrieved. + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> G -> F -> H -> E -> J -> K (the same as above). + +![alt text](./resolver_tree_children_pagination_with_after.png 'Descendants Pagination Scenario Part 2') + +To get to the response shown in the diagram above (the blue nodes is the response) a request was made for 3 nodes which returned `[B, C, D]` and then another request was made using the returned cursor for `A` to get an additional 4 nodes `[F, H, E, K]`. Let's assume that the request looked like: + +**Request:** `GET /resolver/A/children?children=5&after=` + +So 5 nodes were actually asked for. After `[F, H, E]` are returned during the first query to ES, we will use the most distant children (also `[F, H, E]`) and make another request for any nodes that have F or H or E in their ancestry array. Only a single node satisfies that query which returns `K`. Therefore we can know with certainty that E, F, and H have no more children because ES only returned K instead of K and one more node (since we requested a total of 5). With this knowledge we can mark A, E, F, H's pagination cursors in a way to communicate that they have no more descendants. + +The way the backend communicates this is by marking the cursor as null. + +If the request was actually for only 4 children like: + +**Request:** `GET /resolver/A/children?children=4&after=` + +Then we wouldn't know for sure whether ES had more results than K. But what we can know is that `A` does not have any more descendants that we can retrieve in a single query using its ancestry array. We have received all nodes where `A` is in their ancestry array when we made the second query for nodes E, F, and H and received `K`. Therefore at the moment when we received `[F, H, E]` we can mark `A`'s cursor as null. + +When we make the next query for any nodes that have F or H or E in their ancestry array and get back K, this satisfies our size of only needing one more node (4 total). At this point we don't know for sure if E, F, or H have more descendants. Since we don't know we will mark E, F, and H's cursors to point to K. + +#### Undefined Pagination + +For reference the time based order of the being nodes being spawned in this scenario is: A -> B -> C -> D -> E -> F -> G -> J -> K -> H. (Not the same as above). + +For this scenario let's assume ES has the data in the diagram below. Let's say the request looks like: + +**Request:** `GET /resolver/A/children?children=6` + +![alt text](./resolver_tree_children_loop.png 'Descendants Looping Scenario') + +The result for this request will be `[B, C, E, F, G, H]`. Since the request was looking for 6 nodes and we got that amount the cursor for `A` will be set to the last one: `H`. The cursors for the intermediate nodes `B` and `G` will be undefined. This is because we don't know if `B` or `G` have more children but `A` can be used to determine that. We also don't know if C, E, F, or H have more descendants so their cursor will also be marked as undefined. If we wanted to know if C had more descendants we can simply issue a new request like `GET /resolver/C/children` to get its descendants and we won't receive and duplicates because we never received D, J or K. + +If we want to know if `B` has more children we can issue another request using the cursor set for `A`. diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_ancestry.png b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_ancestry.png new file mode 100644 index 0000000000000000000000000000000000000000..a2636c7cd38cb515849ede721f4e4d24a851241a GIT binary patch literal 19926 zcmeFZ2T)X969otc1W|$_NphAvLk<#$A?FMN!Z5&)a~PruNEo6b3L;82AR;IrAP5K| zf|5kZK>`+fiBP_Bc( z2?+@)H~i$1lffrxb(>NoBxGuV8diZ`p)NR2ED5iK`rlW)NO3oxfIwae6fY9#=<6%$ zjB|ALck~Jn^~MImNASIuk2B5%hjspY3{o5^E+Q^1A|Y;uK=MkcO31-4BvMpbLeA#z z@s6%o?|&{RB`OXV5YTtT;JgEU0^BbAJpvo(==%3+Mg}70fzlYP1>Q*x6@m_tR1-J& zdrUCaKLF?B{r510gs6ll;_nxMVZPYEpE_fMaTqM|Qwa^Yp?_u$1LFRfE6x~!3DHLc zWBmfGg8iIi4b_58{$9k}(d(a!7{PQ!{&Q&w4Jiv>XLD)&5VVmv!rR$XPFhvQ%~S#( z;1()tX>Qh1q4`l80dRQNr(El z;&pud+_5N0oVmFaHq1OQR3gYrS0czt-B80@LkEYE4U&`bHMElUa^^*LRz}iYG)E_RXEv09r<`RO#q5@FT-UzsPOLbFYM`IZe)eu)p8-JM) ze4wGRp1X#PwVA4!k+rs~tg#iwBh<-2*T>Y{(jd^%1n;XJq^@Iwx4@XGxnj+Nv?Scb z(Q1;xni_#JL8hjfrZ{hmpOF|e`-F$?vQF)<-7iiMtm zO^^&0;U*c33zYM>adyMwr7dM_B;*2|&@ytKa=J2jX$x6HCn*UdBTZ+#OrW(mgTD$5>A*{TuRL!OIU?ddA&DYw( z3~lCW8sryZ;^U?&jSs_`i-#I%cS7C!1g$BQre{e|M~- zp@qLS0;Ol|tB=$%m9fzl4|Fy+uvFJ^)iTk;2TMqSof+%kHN4PjR#JY55Ca;3tmI@R%$)UP%&|5>#uC9`b!cN# zBRMs3i@+cWoFPIME+FA=VP)VOq-iK)=#SBJ4njIv%HcfC&R&!KGg14&$;B+P2#eFd$+UkaylAgFwKO<|bzqf%j0(L~& z)67IR1RbE`;BKIX(87j-C2bIL7#Xallao2Z+1l9KS4vYB1-k==r6Vh? zi*)z!)|5tgSO)r-t6CXrIQjT!S^2sU?ZHP+T29qd*2q##6Jf0vB&V+-V}Vig5I3}R z({;BNw=(zh3zjz4b%jIKC8TUDZH$fF^!)t7qySyiG&FSa@W&qqzod18TqxWqUnF272f|7E+jPGJoE@9A>km=MyZ;G-d)ZMGol+BZ!&z?YpXH4!611Q zUx$qgFJAG`aY7wa3zd4$RbZJ?keJx(hVs>&X5(@%K&RACU!}Z%|H$)1t|Ou`Pg-L* zl4Gt?KCkl;AY1>vcTGX0RsN!0|Ir9#bHl9uvHr-OJDU%(`%9TZOVn;%^{~I5zVwR4 zFJO*iA-7Xj2B)Y>!99-1dPb<_BJ=^Fms}XHIcjB@iUMR4<1>epV3o{|AnmYQ=4@n6X z^4HaA1=;c8k5gQAb9fhJ9bXzYB*|U){yDNu?hgmPnEa|+ew%!E+vD&pn+t_CJ{h(1 zn(|WIO9|}d&Hm)914HSme@l(F2AH?*Xbq!Tnkv4yvKVh?_HMUD@L*a@h#acmju_%)& z{~Cu9)%VKmbXI0ze3$B*r^YOVk z(^abZm%B3GUM4thTw-WrU2Fd`)hgckIo|fL_!8N}r%(GNAB1MyjQKqgRs4$3qt7h1 zo2E;%oOsK*8ZN+yvD!U2#WMDcO`4wmGJ$i>jQmmG_uKM#Nk+_(m~Bww2@dR*DA-;_a&`t!@$0^Jf|7e>ip$ ze#l18bEJA(rPXqekNzso7WM6VsbQO!D|ycuMWfc6Nho^t>b4uEJ2L7nBXwdb6}?_w z(p^)|y9so^XChN!q(36La!g(huW#ly1-$haZ%>crd(yj%B|Cl<5r3T`dksbt&ixvC zVm3EpPP}jQ6ai7pEyc;nL>n6;$^|zk@P$z<+0^2Fm;my1K;2GN_nm! z;)5s|e36e7ltr2=dKVSNoIBqspN)S(IXe@5`gBIfE*+1&{FkQpr$ZL(PHb8&lfeXG zzOpsZ4!JMRbtZ5eE0SZfqon+v>c3#X?(KdOhG4PZgC!MsP1D^So^uzh?em{)1GEfR zOWjUqN8}qu_1k-*#X)@RTN;0&4w@OSnOQQJ*x8APwLvlb`j{6bSn)RZsaImLKoUT*&U;?Z`-)IW8J zxei+bZX?f&_3xE5rt_*76kgM{zg`*gy<*$*6Ti;yvAc6IhCao%Yf*Sg1 zRdmeIs~0+#l*7qxEPHYe4r=rv92E6x2@|v3$1$fUUTdy_fr4>v@An%T(=lDCXXwE! ztvV7-*jP=GzmStXny~cgGSU3mnf6(a?rr&03u||{r{&x$=5fp{KW$MbE7Z;YrAb-o zU?RHJrz>Bl+Pvo2KjWwP`Hxu8X`q`uCsm4nDBNl~e{myo$Z_Rn5`wtmJ?!!cj{@cR z3;&#VYU!3aFG~Y{e_o_(_HFWIpA9+t;$pRe!a|-qd)b&=;nK#zIj=qo9`36&ce4GN zp2o#-?bP}drllzAYF{0lpuVGeC6rTlwnWI;4DLf=t}v(J+Gl$XCR&t2@l!&ZXfoe_ zUh{q)p@eN2O_JxwTT7C{R+E>)r8=nV-L=y6UeWAI;n*|rs{T_hnqC) zwl7ofbt;bCPo&@Ooz!?VL9r5Gd+NJ3%`!!1MoiIe9ykkedCo`e(n_?aUAuCRZ7U-E z%Xe;audB{H94DCumpQ{-(iC7c<}2_4+=LM;oC(9I1!SC|J-vO|ss(I)RcnuS~+qr3AE(ogGRO z?T%90ILky!8u^fpm*Hcwy`>rpT&5tsw>>|)<$K{cdB8+m+=QN22=YBS)z$S@YQH~+ zk*cU7Eo*Bg{Er2$#ipNk_~jX3M>;Q_OUjsaVZ?dIUu=5i1aJf|$-qmCYZToim`c?y zdw7Z5zslV1@=fc~X$4;VSmMh#XOv`T-f!ddsO<+a!#Md@>=7}2x(q0vqXo;b?fQMT zmX@ogRV5}C7G#kR&DNzkhN&`Xdj*suq>dXK8y{IH`F{6eIK6>Gvqt zarIztg9s}VvIMk?(f#@F?jm-t0;dk1SYxzxbgr5SYM7gwKOWlH-S7zfIc~T9k~22W zxri^IDdz}yoAlxS*{dO**TIh<6ivH)lk4p&5ha@0oAKA?(^}fvSL*8(PMtpehOzar zWrfWPC5JL@(bEie6X$>7=Md35`w>?s z|M5yih0o)n%Fs0@D;t}Z=4O>b0-peh`IZT@9VH{en;fmaX=`D2gglI zU*a0UajrajCZ6&R^XKq@Im=i}i@e?1Hu-SmY>O%^Gep8S09B*O$*K^dZEp#aVF+ha zyKZ*M{_4PchrQi@v>t-oo#}`J+Baiv14uqUQNdobk2>!W0gmBiQ~KuU2{qAd4-{L@ ztqeWB)^tN9+Jg}O*lVy2ZxmhzXiC5uX4~`nU1dc@$09e@QGdoLrZ|{h zTs3PtYgOY*8kH-<0Fj8Zw|6Qveo0t)#-QHfW6%|t{%Pl%5u@3**?>!52PAWPESMNSfLZIy7C`*#H#@?2)Vl%$N#eVcg=l;Ov1G2~SP2*|8m*?zyHb7d?S zu|*r2=-u6dKP$1m>4X84pGT7b@<`%qCB|QC9nUJCH9xL484^@aN9VrRE-iH+iRdr#2&P$gnSM36gbBG?CMfQCglcc6j8|GF$9f&_R?``86<2;~~#Krv|?hpoa(N{{A zrlQZpILm&7cXH+UGV9Ear7QpT_O=^r3Dt(g!ZtWs>L)t|1b#q(G^-%Wv%K8TC9VFi zi6?y8(;ME^V=RB094_Oy$(P$Sun?GWpkUd zjBya-;+_N~s-T8~Q;~sdcHh--0aBPV^)FaFsf z;>z9rQdEUP@5^*T0(~7b^QkdvC7S)4<+Zhp#1jn|6U!tZ9+?$L&T_36o1kK^Q*sro z_Ln+-s-YH~o*#Hp1>c5FNn(-0@l+ZS5e;Y_ItGRlRh;UR`x}-8Ln$;2nY3?m8GQ^R zT%4|k_4JUFlQ%C8RfJKLJ$iKV@bJ+0-o^9zxl|9LP{$tiX`@j>DJlY@^%EHQvf0MWg?87Kavp1twal$%? ziTBT|Bam1YFx+)xhpCufprnfZ&}|NRQh0d5m#qZIlZH>goXx%2H`K0tq#eWt8s0m( z^WFg^Cfln7lYYdthhh9I*c6+KttC(uB!5q;-988@!|x|F5lA?;nw99-M#b>F z34( zrt2qZhbbXqrs`Ie6fR6%`wYNyZ@l)3+0gd_%cM^R;4P*_uS7el3e;Yp2FeH|lRq|& zw0$^a%etUlV440&6b^_Kym8hK_j%nSu}qtZ^hK#>Vlw@7e20Cnkggl-W`U^8=1 z0`@^ADN}89(l@rzG#G7Wg~FjhPoK8&S8OfVeD-r^y8P9*#djt+x9)2`<}G?oAi2LV zrG7~vf(EixwaqS*QKf~H=aVXmNvUngMaO#2Uw2c`LgytjXj6pJu_nSdqw)(W0}i*i z79MNA`>pt{o;?HYY$mhS-=7}cMv1I(ppp5=)H|W~rcQgtwry*#?o0jU(}$N{QhIzo zad|NRfmMUlNoDW)m>2auj)yOGQ3^2#K0FiopjG`MOVnsEX>twPaihfOi8XtSa`tk| z_;C;4rM)`AgQgTnHm7Ih)Mqy-Pp0C3<3AI8Lc6pE8$vf2L>)xl_S|5ydUlbl{#w=K zIpdi7U2kh|?~ptk%IBV$)%`XanR4|Pb3tA+uVW;~e8X7Bfa!B+$>i$S2vq6B*~SMH zjvP!umkRn;rhG${;2V`&woflemEE=ID!adp2)_5BdHfSg5uM`MMpX(&PNtxU%-{Kr zK5kU-P20wFN6soi2bV=iMSYw8IP)WYEL0SK^bByZvfms+p{e>E{ZnVd^i5ymq}@B` zUA~&`S4+^lns#aUh!=Rl#gtl1bt>H~S-i8A`>UgGw%DR8nRvHn%2?`nqZ7xqP$TNd zg}cP1+0Znl%{JzWy-u=v)42KV;%ZtQFTRy#S{1o4daP|!&FI5vKP(+%z|kvyH5xvr z)GleE+d99Dkf(Z$saLvp39R;=W>Ik9!E@70t0TEizW10sZ7Vn&XJ)%!@9^WFM@mexo`0*#kU=5nyk?B?w9o|T7oNP)rT*mZy#ibv^O!!ET)B%XWxXJGqfRk8!7 z_oYOCfSNj255(}x$E8ErRNG~i|5Nj9fBQe|0# zJc@QkdSPqccbl!5I)3Yy7V+*f>AkRL+rPKZ^`6frI{Bkcw=oOq{qPt>;COcPQk5e) zdD`48b_{>zj%f1zi6N3kyzZ2E)Jai?hs)AugAuG24qu#xRHLn^s?ZhVCd5I)4zHnW z&T@;j?0TKJytcz2&g7oxFyws{b4pp{&uRXTd>o3>XZh4c9FYSiMoi~!4qYzvVLqvQ z&&>d>VCcfNA%D>WElJf`hGif_&~7-fE(hkP90 z>g_oi#`tI6H6iT~7rUe&Hg!INlz@P*-UYGVda{3%fUrv2H7p!CNuJiR6s8N*IkdwE zMn1C}d3sVZnkn_Vy@`#*Tx8;Xq|SxVc<`Z|3}YQ{xy|_5ghR|(fJB(b#)3%P!58h( zxyLg3>T{74F0uy+^F@jVsHB}MwAvv4q`7kN7n(;eeVeTqe|DaLxTM$EJHN>#>I%e` zP*BSFb6aFq_)_n@zZy7E1KG-(uUd^3y?CZ_xx0F;F*Mz3NOybmbB`_NcO~#OENROE zrR|b3^T~F`7SWP`-~LGsR}k^MLVqTm%|Z_E+&(4V?khRp&?g`8(_+eL;k58!(8WWn zBngA_(1YKvk1f`)s+a`hu04MdN67q@KOItW^K<9C`_$3Ud?A{H>wMMcGmU>EU!|Kg zvwk1Q^M*Zow%bAO~X?4%1 zd9#RhLvGMb9@o%;HNcXB&9#i?M|&B)e3@UMo>?yc~elLSPBLI4~8dFp)A(f!GHbg*TM_myRuR*cg}c3D9}`95BLwP)|l zc3x`hu*e>4+4D~Snk)iWzu+^O?MHYder-Wac;viTC}-j(b?1B@VOGQ@Q6xN7kTy5? zEY(cueM_GUY6Z^k1n@~3O;12fQJu@o2wFV8zQ=qyq3)ch>!_-~f!q_#gA?*UbdmY5m@4XSF>}rp6n_n57M>_U{A*MrWs|l97Y)cB5Md1|5E-W(351 zgV@wc#06*P&9CC#ha6`EP;FQ#8~UM%8mHO~w{O|zU}4%-W2eFYE>HChdi9|@dKfqn zk8d^+h~Nv*uJI>>V)?pr5p!5^^pu(osam1w`AAQmy&m1WuLF;l|*CgdWP49&X zXuTKp`bCxV@@!TVb^W#Nu~}D=XO~BK@p&{4fSx%%6ymPz>SKPIwI9bG(zbeohz4I+ z?~353i0ntp>PX8>FKjb~<}TQ{%HeTm?*3t}q&!k$L371Q{aJZ0LOQCOy-2C?hEpIUtzj}*bbp;&I~ z`onTqI(Onrb^qDp-+ww+<4XDA{hw>44zcrCp6VM*(}Kd|tfj9AFSmPD+(o8B%KVddI$X+Kl-Y%c-yFgnh~>Y^eBxwFnp$QWlTft{DU#i zjrxLDgjt_@9{iOTtZz+wgF4DC2t*I@^Xvul%5?iJ=*@3QP2OWTQ@@mRxpEO7xcE6G zJP>@=d+e2C#-e<07urqcca0qv6YfkqBjEIm(g`$=6Bx#}rm{L{t3kRuWHF@U)i8A0TBxFYA61Fob>hU*F*{HlBgyQxBM>heJ0s>SB(oG!h?jn@IlGqL!#=fTNT91M`X~zBWkH9dj~!hr9RO zi%m=WF{)vEH+qLAy5qmkS!fv;%<|qZ$-I4=Jo%&W+A=}zZ?2OOkJCZx_u4z(ylaN2}fZCKtbPFmlL$3mJ3v5nfSP)y@(B5@ux2` zxv(blamk7JajW$SA}Br}xl6v=fvn5%Gk~eZk8DU5q49LWOGt4f-^=tztC*SH^nGc}>H6{B+v=Y`L~UIm@sXSio#(ZR z^5(sZ4Q=}{dgRbmG5|zl&{z`;f+_tLk^EjEft{e=FK_& z={4#diKn3_rpf}>*T|`;=zE0Sc{n&osAl*Q|7O<%&E6lfttxj7i*a=!k5~JDO{`X~ zT_(nX2#MC?xZ}$|iJwLOL4@vVOin=P_D#pt;}<(fC&UTCzDBmp zgM)+VI>q9WlCjsXpD?&9OUlj7ZDsx9(5be(oRunHRtSHdmZ%eDtbWnEed(kO<;R8l#f4Z09 zEFFN%5uS>SxxrympVoX&AraT39FBenw2t-QZFybg{bo7~6kKy#MjQ_Jg8Qs9)~~;z zp~0O!q_f8&ypd#bc{znWyPQ5!nS&@Uk-?qabf%-t2HE8ZcV;baZaM%27xocqt}X9# zO1q`B#PIDQ^RqQcDi>A+BA&64{k0(5i?x<4-L}cj=*qG^B;|iYy(>) zdy*LV^}Ake93dctgGOQo8up&CZLr7M>CMgBrtMNcdHqa7bw`2eLTUlT&$|ggjw>{O zG>E-vDQ;n7ia@?Q=3m!f-dC1esF`*ZL3Z8Z?(PMy8=c~A&mR7(2bPHy+MPInl8f22 zl()^wnBdAgV#ItHwpHm;s8G_KLk(_nnIM&JY_KLocXNmXZGWttQ8B{6Z*gxvAAS4B zbFEMKA3wIH&~Bdk-V?7=Dlr`}6hA+|V)S9d;G*z_3s<7@Ul;t@{ExmC0&Q;4X4?6D zqnA1$o7|@2^SHk0sGJAk=1sa6LzJJ!s(VNP^N2ad&bNK^+}n4_zH&2ETXMWLM=Vd~ zC)m3jr=+ilO-n1je53f^UI4?#MVw|0jg4!C%^_zg{6H>eIT2yl8nx~=Md(Yi8u`-F zFQ&FG%>^cp66k&5VF`r^a@TWGCjSemZmC00#mFe#*Tt9{1Sdv`apS*QI7*;$NP#in z?%YXNdZ}s24kotOjs-QRIfX$yAXSgI;XojaW$Rb9E?r{zAp8XNuas{P`?dur*wm?3 zZuRRqQ{TQG>m7-uVI$TC`d^2jot=w(Zy9q=O-&v_YL{U&T&`A_+GeYi~hekRTNs02z zCEAA&6IWW1Gg;|J?d zehfV6C(3lhFeNTgqw-&wuK$6{w)f{tf2%VIlPQ1hbcst<#U^(t9y@kya$|9*;h34Z z`SIxJXxVPR_&aguHn_Qrj0})mHdi`&jbsVAu`6Tst?i$w#>dCWDJZV!@u_QTQ^X9@ zGGmcUYHDgkT*{DsNmS|D2_X>7O$&yud}0HM4J{inEle+Jmtsscv-U9Y!usC&Ta_8r zPe1z^@^fxG-E0jNzVs^RU#Si~)+|DP7Pz_Fx_d#}>@ z@E3Rb8pEmQH*Za=H!6Y3q5e=w>CW=I10cs|$oBZclMnITgag)v#oXX&?phwrP<1{@Op05~VE8 zMf+HKsYQ903Cb!@Y{h_?9PA7QUnm51Zhd}DM!a42)z&=cKb)H{2z&EC+3fyH9JP!x zs1wl7ayU1RZc+xit`~2k)>NT-{l@b|P3|hrXlnQ9OLy zTW-|D*{mu037XowgPO{~6%uifzmyR1h)c+oe6>`p6%n#Zo z={}5W_}qUFTiqc-1UpHHn5&M(G!NDOmE>2Z6yRf>Nj>O2u=49QaR4DX8;iuVq zz%6ij_}nJub({Uq%e{*#M6Forp->@Eb7<~yd>`Vg9AT0euqG!UQ1Rv_uBtL(u!L+8 zmxh#Q!&6+le%o<;|2VML_FNesv++cd`z0l0s;a8Kes?I(no`~+Pd?XZ5KD~5MNbKI zAwA5Pw+qTda%JCG{OtU1?6r+2V-2utPx}jxcxC;cW)nG*Au{C%#RD#J*EjxI*+bGy zysp^=mKEa(NlBj*Vi)E{>$D)UaP!uvt-m@m4NFWkZ6{D1q*oN5^sz!i#EF^pVj;ou z@R^=gAe%QozndTZF055-GTIdFKtp>0%G>dfr*O(6EZ61FdQo?3=>sgECMFgvE-6BB zhBlY%2kSj{u!BU^hM0WxV>9MA2uUBWzDOzeC3ENDF8A_PLTIkPI_pK0^fQZ=OyKfU zPx|!&gMv8ggAM;%m1@Lq(eDpDd3ZA`OV!ZO;or{%CiMQ0LP@>lD>{x?Qty9VTJM<$ zg_+)%nFraIcj~fyZ%xrk1W<10*9qfuX<9%-Wfl)jxYNE%`$BqcZB1MXw4r|r>Ps6Z zTSj*%W2=&x{etgZKhRXy_*;I9JyXciPAsU)R+VMM#E?Mr;`yhberG87aP&#J1fp}4N3q?~@~Gm%ey5Ze( zjVa_%vt7Gi=lA&fZQD1_WxQK1>AMqlFB}@mdxq+=7n~?=60yw|t*8n&a5Q<0dvBvRV=QhMa6Gt_maF>#6tGo($X?O-wyoT$js~*N$B3DNM>dxn1dD9gFRN30R<8(W%q_$p@f^@2=ko z5M8aRdRal+Y}~o@2_N}44H%pb{q-%ePh7CZXTp0!9ykWsY3XzSW*ZS$Gb8)GTVIar zS~!jt`49y|0s;g*b=9zPLX`cQLd_DmGf`(7og2G;1S%i<)*^s1qvR>&+Cf25YU=81 zU1CitTp4cPB+H@NxEr~f^bb8h`Wm?#v@8t((KdQAfL{3e;!oT2@7xC273#j1>gAS8 zMrhe~zUy4|>Hos2u~Jhdk70@D*uupZo29^OZ^N!u{v&`-S-r`UKKC z4BxsH6Kukrzs=5)9X)!q7{|uO2FkOTnBnrqkwEAF&~VZTZlWi*`rnqy40un=oRYl86WsngouZ95D@WL@AGS7g`_a*!EPZM3tC~l>6xuo{x^&sYjNr2#V4uL;F> z-E7ByRc)zJw5?hX567z$luQyNw+CcRMGw{o7>+9=&o1U>8T=D^YERlb_ zV*=%+7OPh?d+D13dnxr2x@sh5n?a8OEM(3wf_y~}5|euC0F8?CoT9b|Ps>CL^F!Wp zZLN6pQtBrp|6if`>2{amH0o!}vU`&b*51`_n5e~5K4d@7RrMs zrZ6K16_b7DubQ>W$S`{ANSftqznQiwB{OG>!J{dMOR6sUcT*bgX^s%EV3t zl*SR|jMrN!|63aO7guDj{EvjZ7?OY4(&*NItLeda8`g}1k5Cw&=1h=Gb`>tQk zz55v)RfT-zw?!enTasvxP*VWo)31CZb6TVL(N?XuF`C48{Suo2e|D0+9I*mOo8l)} zdF*vYn0qp@{@z?*=lAXbEk`0AykpmS>|x89t*-Jj#&Au$!&8f??=>m}h~>(J-6tCc zZ^yzv#@(PLxzr=9q4GiGzZLtaY;UeZckRa-0|n4z09DgtZR7NfeX+ry;?r$Ayo#Ir zqLz}N#mhvh;_Jy7@sT0PK1?kFd=3=0RP>n2J8XBN7MAHkEUMJ=SZ4y1Y2!?@lO+YY z?2$)ens1+9tq(O_Z|4AIcc5;qDUg!WFXtkci7qrqWE)=%(){*Lv}EKkk2ceIj%U%} z+-H0jO};rFRDH`hxk+kG*I1H+J2(~N&;gLv-&t(+eC|2gdqRfcg>zAYkFyf2 z9cOXOJ?x@zIzgVgbLZo;UYXM!Heai32CH?@Bt}-hj@4g7R`T>#^1pHiu%-!)uPC? zOWlqtHfK{hwKA3pPJB5VaZYzsZ05_38mf4r^+jeA%EwJj0_H#kR?8{4%$8~=(BiZu zyF~Fi369UPxJsQ|eLV(hLkeTh&!-Lz!bJU!<3Wg+A^u{JRbmDAoXm9ln=KSEXw3RF#@JeMCk z$Oj*HY5t=|3f1qo-O>&8Mef6T=$omu?QYo*$j=T}2{fo73&_v?fAhrxkAlK^AYutA zDH=gRFVI8fRz7HSJbGy4GbfXA?8#;8Af(?kN{i>rDMoP;!+VOVINXI2gS?Z#3r?{q zGD06_d&+s4oc$C<>K!MFzxI6>I; z1?tGi#B`z4_51rQ9`2LHiJ$J7^9NzgE?qj+&M0d4s;zsXAxwf16S}?XctjWJv}=;AV+> zxJ8~MZ((fgG0)*@ilbzwpzRt$vHsJU&E*kBgetkjvHhLf*$9n`jC($Lfdk_CyL|L*{Qe_*s%j-FtS z*m`sAM7kEwvznTYWCk&2JLUh$8I4f~%x4mh^_S?k!gge&r;~tC3+3~J-5`>f8#m5M z`;4C=A+j^byI%zfUA=nsX6X7nZOrC{!m=`fh}AbIps|f?)c{STNt4&8XPn9_rl_ch#4R6y4kTO^!K?zqd=Nh# z!0+wVR_fly@Q})6^bZwIra^5C!?S$l=u`sXl(Vbr%=fNro!?yC+?7smPi75G*kh|5 zTge)>#>3%ksQ!R}(zrF=${xMX8QlAKCU<20xWL9HzkiPddRtSwKl$dG-^PM@*5~E# z-;alf?*(7rCfOdDI4lXQ9xBwNu1qev2?ugtyvVAV%JODyrUOXw*J&Rs5Nd%cn^{=A ztc_CYlFowRI3*>`mVegFfkpctEa#l;EHjAOTbB9qgwt%W;R?I@O%jrZ=79Rq<0#D zmS4z2%PGZYKHq^(!Qu z>h|_*B$Kc=R@H8_;Hl!?qp~2%UT%q{5V&yRO`bwXE!Jo3)o~#qAruPL42uD_Kt)AW z8@}az<2wtOze{&cv830;>qth$P;p@q5x>c1(%RZu-vc6`AejzG@(ZFbP-dPf@jOy>=L-=Ltl%1Q)GWpaA@YDGtAag>hE&c`qq%oE!hgCBZR`#Zm%RaRcf$e@kd{lTwyPw@_{ z#ihgDMKQg6CA;E9oYy(nBLb_!%g&wI+1Y2+)yItXhOGU%Jr)KY*7|=>>U%h3=@1EZ ztkKa?@t_4W@2^*{9x3jZeA~o?v3eDFA?k3)6|%ChU*Gt;01Ky9R(c{gS62ZFs$4&| z_Z92bZG_?RN6H$V!F>r#63<*evN19;5>XK9dm`R{#v8-InPC~)Qdt!yzkP$s(Ek2D zv)A?a?{#3cARD|4vYu?6%P^Dq;TJ-$_kZaYvsR06WoW@N4}9CFhPKcOlx$1cbQ~h zoUr!$ck6R(mpH`Pm3I%l6@;9N7Klk#ppv8hyfBVA4rV$ z@;~+Tq~620M17|2Yo^EG^f=MlQydkW$ zQviuPLqLFA&DL?nm{!YLX>nbvUE^w(4pQbnzm+|g3e=y6H9P@)%ZQVL>0lY-VFKJiYnxt{gmsXYs`9ifxVe6jZlh!d5RmtqYfp zjEza*o2Qo&iKq!cf61X7783BMT`FK!Pcd|r1mu~~j1jOhT2|K6fRrl(K7i4qI5*&W z^ddH>A%e|&a5x$onr6FCwpA`EeGzY|W@cyOC0u)&-@F0VtQfU#RdmKfw63lW8Wd`y z|Gd6G@=vo!dVapx7inl9`8XE!H2LvkApn%x?+^(fDu=IBO)J09<2z~;Zd1Zor335v z^(zwrG5R{v7WcK?d2(fCyof*oi=3&Tf5d%61F@1dee*h^-44~$)iv|JK)?{Hs3vu} zBd^ukPqMJE)b{c$)Er%1DUiXOg5VRrxpW1T(uWT(#DtW(B&|?5Iy&Bc#`t*o@}pXg zr?rcZZo6K-tWt^-;RP0a>((vUaZipy(5C>Vd?u8xeWJj$tIV6{OkyU%z9Ezpi0^1! z!1dgc+Gn8PC*Hoz*s~wKzjlI1WX4EIW3HW`_K(IwWTZek;qgJl>dV>jL+2`N`(m(KrI#$KL|u zYz4Ar6_6hQ>GWwj;PZUDP#p4e{AzxVRMzw7$oa*^Aq>I7aWb_?3}cV6T0-Sb2;V`j5UT^3jtgZ2-hTAc~85hcxUt3tv$?xnQx7 zO2B6NBTZOB{AaaDNKVKQ|6hPxLf&(q8*MRw%q7%4@^W$%!Id_HcNIdO>aRS1!Izbl z#VjXf$_p_ao-1*`;e`t)j)Fnr_qP!t7jZww8+GPh!;_b+ca<+*%&U)koSwl2ZzHK~ zZQl!siW+#ti&Z{*rUFmre0h7{&HC@j&!0bs$LQXc+tx4(SETm>O4Z@&gO}ZXb4n7v zAdwOh{Z`5apZS!O?uAP}xX2Im78r91I==4ioFn? z&l7NY+1VYP?*LhjA3rV|z9|gEqHer-n1>h)gfG<^tuC*2HiZ4YX<$8~{>{IxZ~oSq zMAVO*2kE#cLz4LetZ4#LQs>CX$OKcq4&LHqI*{2A;}crBE7g8W#8#GRuyGRS?cH7?r;dDs&s&}t?0t)lny*-*=-U&=+WH9EC7$M?Yo;`oA1`lLz zi7!;+`oS5CVZXJ%S2mOY}0wqwdu zj%I_X3et{y-Eq85)1zne5)Z>SyrT!l?FSK(Xao4mrQhfeE1Qy{=Q4x<;ai4U3 z%tLSR!pX(Ok%bKy-|uJy6C-vzNFJ)&9 z4|sTZh-CdzsiuaqPtPZk_8De^-=DsblD0R-V63Rw6s;aV9mycbhwAC+ zLH1(%g6eIdsGOYKsCrIMPfxjMt{&eETbm~}?#Gdrmqh2B2l{R+Zxk}Rv;l3VWDsow zM+a`j{B9noBQ&H#I6eJ&WPpY<>G|jEjI6A4CHlD~m76#(5VsT~ch{k@%8VbP9LNg~ z`W$Q^=)nTKU>kpuo0)lLeR-tDe9^6IHN#jJb2#vE;GroH`8w&yonP2OO?1-oXLg^F zv9Y}KpDoOpv;n6NhG{a5PkdaRYPH>nd@B@;hR2^nLPBcm>sx`dW2kptzKrl$Nl5+l3w}vl+X%@Ij5sZzdi+QYHltp{!QV<`{!- z@tn~vtT0>cvIfq z-tHlu0lvO&ztmn#+gX+GEG{fBk1a8p&^bY#p$o$u`!W3Du>i(PCi~GNL7@2ROHM}BAX6Bmi#6?MhF8VDY@3*9`Wn)2D(?Vd zDMzV6UJHO)n`perrAwEH*-kJ*z=kfW+PTz3OwsUMPQD+ zz*UTia~`0$k)K~7spe?gHayG>S-elbm&vdtTv;+KEbP&QcEYy-W^XXn7U zKMf5rcpB})Cc*>AF$UcNf*`J6NlvExOZO`>Hb7{4^Wg(wCC4}vz9J>z;p1xob_y1u z4i9VFH$`E-o5>>_aN1k`7i`?z2QaBu0-lfe3pFZ}rFx!`j;Xy+&A2Sn!IV_!SXBeIbzxA_!4I_@A&izo?K1=I`~E z_O>qnJWz~Z5FWs-VQGzZ@pAQa;Q4!nx2=aK*45?j1BFERMfipOp0M?@wEue)qvoXG zY~-Vba&=KqLW&#U(YDUSYebYl^nYZ8M`QnyO3@N&DM*m?viBqC>S!aJ72U1Te=nE7 z_^3LHYY3w>tS<-7z;V6IQlpV61+7u zJd8|q5cWPcl6tlx#!Bw$y5c$xzIas~5hooh7rZ9i<)bv4#>U(+_p%lb@oGdY}em;_3BJg}~HC*JkNtVO($ep-UInj$C{ zMNvC@M!uxjOr3+ZvnbdP&-=DX1CNzBZcr8z^7HksS-q~_$JX5jAOrY0zaRC2U+!)p>$bS<$2brCfw69<$QTG$w=Z7-oHhP1L4cJZ`U z78G~3M>*orb|`nGq@SUeuYrlCg0-@;vZ^1}!3u##Sep1?bR`iuXGm42)?q|kjGuzA5=sHqpyK6g=cbHB`r2t3D(MTNwKd=@ z#?Dw0epuQn+Zh|;QECLdq`Rso#zs_7*H#0kC2U}2iE%epbr5q@vJ%v?HMYh1D%{LbgUO7$+wi4=bFw9v11SrGWJFmC{G) zTiI(UAza<1#3c3I32tz`77~Ni7In4qQ9}B7*n6Y!zBqU=oDg!5!YMl-;5P$jCvR&F z6(L7;4T7PE242TfAMLEFg|ZQKmOy$LVo++<1RuCbOG^h~qU7l!B`T
99kI%2SH zLIy}%6%PYj1s{D^MFb9|uYyFQ@mL2Z7leb5vX(vEh?LTGFvMbgUHlM=&ID(I0Xz#Q z;fcTtD!LI|{4@+T{ItESJ&c@@R>lZpl$V{jp_Z2kMq5?f*Ivj&LtDbh0p&qZ*K@IS zgooi2#NmFlh>3x&vIj!lS;z(JBckk~Yvt!4q~_*qVlOPD?doHtuJ2hr-fFu~J8+gMTG{hAJef^Zh z9c&F0eO2IP29kgqNGn@iDQzRPj*zmbHEbzTMN0$kX(glrFozOFYHQp0p?!_DaN^cd z1Z5jrO%H^ol`uxfNz7T%TS!mXOGI2p*~>=3#81^wSW}B&uV|^N?C$M{^px-<`jV!y zxRk3R-qqOIN*Vst))JL6P=jAQoNU2-eK8t#65dui7*AngD^Yb5JWdN~j1mzv5OgyH zMT2jsXo~21YbdKaYI#clOrWeCd_0teM65B6I1gVb2ZFLa(pbmJR?5oI&)vz_8Lwca zWq{LhaIJJD>?y2mr(z_A(QySSrF zv>n|fkva}KL?3ijLc40}Vyq18L|t_p)vRsAtk9D79x4(Fs+OKESYKOJdt0O;(p^hk z(^1^W#KGNH*hy30%1{F*tZ3!v=p=&DAPCy2su)P2v_Wj6B5b#c*y!EOSSpu*4M@D>*r;NEyNPSSd$u zLuaBzVU)#iqQ;Ikp85zsYm6Yu1tF>~rmP|*>|&^@Bjg7cqa>U?wAF2h4~Bn|eqP$5 z+CUcI_kZz)KjZ=a{)hUAD0zL-3?Lz4Cs9#E==(gKO~LA)=>7O=BZjpV)4von+WU;7 z>Ew#LN2~Nx^6(pyW-*O@49XtQl#gdheLAG^?AfP!p06Fb?ze91M2{*y<0hs18Np7a zAtI)C`59+Boja>`f7J%%)PtK8Q+Bz}cLIC-dfw*0$S2Sm&OLllT}HwY!GNUVhAzUbE*C!L3nwol) z5zaWB`g=R^H4)!gh{tq~2%+0!^%+4O=AU{EK z82&{txVgI{a5!A{a7%M@7z3$&TWa9E>!Ttk+s&=5k^I7%x0^d2Z=L#t>l+$OcK13m zmFs$Y%TB2&QoZs1qd8--$*(El45YA_&tJYIJkQC_{xCFj4oNjWG4XhJK2!N#x^CW! zwS*5(Z8eUaK26C%{_1+AUB|`*Dnt9mM04V);51O&&L8dFG7!OaaZ_<SW}cc8xY6k5R2|37IUQuE7A?z?W56}Ie^Srz?vVN-}aAVExvRc6ANEF!4_cMPC=p{1< zvAMaaFFpM2+i`Pqa}w19Uj7I$^SYa~3LYMBJ7!eb0#}bkFgy}-JavHn6Zd{DH zv+%Ad)uG_v;DEJmBL?KpI(qq+#b;Hs86A6HCCa%DmLEe>?M})cgx1#97S~)cEhXU& zJf}$&djGt#6)cXZZIDM*RW)^Ir}x$MaAZbEQIUvY0VYz&`qMS4(4k5~@*q7u>(%$= z?qliN+^=tZAl=;@T{`A_ij)Y{O)EO)TO!iY#{H3Kvw&3@$A2gAqMRu(pKSmzm zh#=lbLc+ttqr3P>#6EVl{|s2~`eC98e;2Z}v?Q)4ARwUqjkR(y_8Z0ZN^hlPacAP6 zppF|oFHNZE=^thX_*JtaZ}ASC3s~(?2B}FGUy?fe9Dd;>DR{LSJS>0py29bwx9o3x zmaMRSg*DqWU%!66*y1+!iI$3k;nb;%8hX5mo8$3XS#6Z`tO;TDt;rG~+jvua_EvOs zGy{@dSa|tet-Sxb6BUQs+Oz?-=H5N6$ZH1c27>bP^0R~Q^tP*T-MmywFPXL1yPtpl zjE3uezCVQBzkTvDd&K*>fifYFaX7fRIHExE9{d_y=Zyl;bb@PiY>Xp%I~dy^9-)lN+Z6l zlaQm8Mh9=g25s$;kX*QM;d`~e#B+`0o8S{_T71p(_6l*vrA}wS6h*NQii0dHET}l* zl9DJ+pMFTA6X|FL3LuE!(;eUsGf*i{8{kqjQDPx!pKy zpO|BhJ~P+)!mvOAB?Ma_lv#Lu_npciH5Be1^ZSZfpz< z4Q1%_UJmGOH_X}%B2lf2Vjv}>q^sZES{a#~G{VL`lJb1M%B#0ET<$hPnxLA!`XoMd ze{V+w&0u9uj1afZ3EF*O^6~dSFDF-H*hBdB3=5LV z>HBNg&xIedUs7aFGspY_iwxLw<32%?DaZsyT++c^%O)%$!Vw{etFv_2xRLG@x4AW$ z2H=Cry#FgFC&zkz@LhTEfm~HpReOd4B{tQ(FbbIg5v~3!F|2*3C3@=eL`%$bS)avg z(~kk6-?y|JX^cG=pBGWm)}3$E@xtJ8UQT-duiqV+ZeCvXF)R|(HhUy)2m65#z7SNQ ziIiDs>sU z{r$UfUVcA7&`+=eHv%CFfCnxQTV3^<>dyH1@#Exvkg+inn3a$<1;2UKE$4w!1O$-& zV(i6@I*<(fwQM-(j%k?-2Opn0^PMrU2@=%-ue;1*G!ia@*AR%%)q1WgS7`6wzYnIA zX#NmW8&thIZD?spj3Hmd0c|;mXq)_NeQtn?jxLEg4Ia_!P?Pai!m)=hA~G_=qBaP& ztEa$JII(;kf?b4eUj5ok@60OU_U+q0fBp=4`t+&vPRvYiq1yF?Bs|1|_L;R#4+w#% zG`Ya6wwpA*NF-@Ur_p0+uixynP7>a;#sCykofk4~H4Mf9VokAeBqRa?0#n_b>0Y&f zacJ9f5-w+fnS@Wy&N}7s-CUj>s_e}jG}x=*K%bfIEo7S?u66a0Cuw7LIKlxj?v2Zk zG?+wrWhI%{OhMb`7@&x{JqH`jVCApX;HC#v3MmBrAU$;im~ z{Vj*yd*KHyKtlPucOwrif<9y@ME#taqP}$LlKNxehTLbS7B+Tv&wy8~Z6`yBi~Ho5 zdwG7SGOE5_5g>>u2CIFJRW9HbSioQfUX9u7>*B*e&w)FStajGtw0RSKmdB|>I?W$} z7e|baS|ubTplM&X+$5_uzXuutZ_lIT@+|jVxyZ=K2&&A+T?wA)xng5)KL$aZM0;ED zl~a3_{{}y+tRGik5HN-=oS3k%u=;#v%R_|9+z8Rz zok${{WE`7Kx?<|KJgzb&@aoQ~D<2aTk_)Q>ZRucNBxLB|H zgkZltM^GIC`yBuEi!|f}7m&$p+i%~#f%7emHD*0a4)(r5!s3rko%QATXOQQ@maT>PPT;So_$$^|jUb@V_bKPM(G z5QK1dVRv+NbgFj^gMw;SMPEM8%F05^@*P8>uM@fc8 zGNv?)5TDv{C4lfikcvc`--;cM-rL!*?#xob@5}rWwERe(*;~a-%9}_7CiFn=GGMm2 zFg`l3Be&HOc&)B~3U~u-@d(7X5`(Ld)ew`4dKd2D#lCihc%Fv>7W{{!-^O^#%F1Tf zt{R@=(oDU|qv7f9F2p=k{t9UCjp_}$wX0#_;VgsYq=yb2I*Iak;YK{0{oF(fl003GX9D&U}096#sgVxB$Y1cV_ng!8DoL}-8b@U&){+?BvdkiPTt?^my08Na@P zNVMiXEA2&9Q&R)ZK*R}BQqnBniST-$mqZH$7$GJ>K>j+bE~q8lfZ#-A4ggH;Wv&+3 z8|B$*4@pU4+2r|CYC+`Ff6}lE3ABtocP$m?pS*SK<>x_vNtLZUrAo0n-F+mAp6yj4 zzFX|^SVV;oK~iBQ|7eOV$GS0AJ5L`ZBVuZ=)7!UiK?UF6SUZ}P0^&gk>-CXc zYgl16&DZCJt$r${=a)!COO$!JTgJxY6!?fP(D@#~jF#rekt_TAY{9#Sz`8SQ-6|_8kQv%P zN9v-)cuf(AL;a=BrkVJzOTWme6)MSNQ0H4A&@~Re^K=KEOf$nO?!*LCuvm*tF2G?q zI%0$XR8Jvuq40X;Dac+gwIIQjAEdfStKApY7E=9mz-H&dd=E-{0CEA(AEEm0t)d9osmte#|BPsWsq=S^kLA12#%VUvj zeB$+;@OTNZ~dv4V1IUH#nc1)gd`Fcs%L3l8?*;?mi>>; z)U*XLF9-Pz=9t$L|uoKlym>;~4@-0nx|Na_K@ZQmd<1JJ)G#?Xq^v3xq zIO;!H5^F;gnm#^~+v{^sjZyC`ve!B*<)Jul1y!XyvOif5g+o{Fu~on7!3M!+<>gEo z$>NEXz-y7yRKzOLn?n?|%n{Z88_m~`o?oBs)3f9whmclE2H_y$I|(J-+4m9D$H)0^ z9wXK&e0<6W8=yv|tNbSeiD`f5Y5!Ep6E$x|LITmTZ;cynk|Zw0>U@2DJ&<`d;;E}w zuQIT)2@uOg5nX>sua&}G775LD^9LX6>LA^@9*2t7TvTF<9K(+~Ow6Op6^tp#;*hJV0E{X;{szzN2S^$zx+$oFCFClD0Ox9f|ml2 z|FN>N&Zb1iss6-2_)hlk$Bl!(02BWRo6Km+R+pyEY}zSmMdi|he`fgM>Y$BB_JG&Gconi>JQ2GqhxNI(r}H2NBVJt-Mk z2biR&bN>lq?gg7|`-2qulFj^5wCww$rwmr&Rfv8%2&A6?;)HIzCjku3gl+rXUve3s z@CboGILahQ;yL*>F)&UB$;|v;*T@wS_hRI~EQX6CLZ7xUzMl1`i6)g0%MW&Hjx5}N zm0N|1!=P@4>kXw((sAUMn-Xa75$69Y3L_HTy(q0Rdnk1xf}v=H_5lO(#m!z7EBZyg zOA(J~UE{#=#r}^XyiJ=>l3@MmggUmaJjwYpgd&;F?&pAp{mdu)N>(^3zWYbUc@#Z;& zW$fLECA!0f@$7FzgjtT1UEt*3iJg^+(Z%d6Q>gG{8??-Dl^C|!41f6O9R2-V zqTZNvZ~+DJ`NpLp)EsY&Y=%`5IxFHO(5gHI81qYcC6xjf*;>3Wj(E_fYIb4bB$E>E zDGeBWw$V~c=&$R9t#M4Po#qOXd3@(}krKPYk~A5HWe1IQH=Tr2Ew=)ww?#WBmGx&iXlZoFdnmk>e?a~fIBnY{@Z5_iD+abbfS?;WZfk#RZ!r58!OcXh(0^R-MZ zgbTe_(X)5IjNIx@&~?BQw`Xc)9N)QJpc!YpScj_W_;9lI$>pC*E~m+(8qq7A0zVpa z-Q!2T_fXRnB>a~6X(5>sK#fUZ&1mdi3@W^2=-N+K$UijFu1Y;@nVH`F&T^Tc`GWt( z$ZZm@{YL-a-iN^5yelB<7QDpjc;LS~kq$%R%XnePILLMR2S1Puk8B2X9*u8lI>u#HoNRiVV^qMET z=e%oNf-a_{N)uCSa5y^RrQ^x4FvFZE(80)_xt7!SQZ^)21gnwSitsWURPkV`55g4t zwk$8{0V6N1)`vo8((Dzv@*CWEUe7dHGxcf>-WcxtZ4)%wlM5B74` z{z_7qV)6Df(coYk&IpE0U;FY7=?&$J&#wEQ2F?uT6lJJN#nJw%`pBo?HI2x(O((fD zpC829R(5sMGfpnMj!nLH>Z2FU?y3;opm-cGe$?t$P*hzF*|CE)FSGtrw=RFEbd%*g zN_eLAOj3#(evJAlGQA>S_}Jhadh5VXnr??q@k`YfT8{bBj<}v`o|G`!7*tH%IuQk> z!lEU;Z0uvB+dz01b&hAHzjoh&oHjopG8^gXFU~P zq93|BWGjB|AkQ)@4{x$)gAC4}zVZCtXrN>%{-aFl56KjdGfQWry%zQkJrOfvU;B8S z2faIYg>Y6jK2k&G<6^VpSD(!A{HC`NXEh+Le9}VSu|Z}LAmr=<;)|fqO6rAJ(AgRY zpf?L7In+476rJ%daqjZFx4!dOj9}^!Cvqb^yp_4kgw7|;?3R1X zVG%sRG=ov!cpfepDr;p<6`d|js;S+%QM}6hfPoa?C5tubDB|PT5P#=xnY7=iwpT^d z+b_HGlwNZ`PD?3WaOg49v3*z!7Ya57$MWs)RlX*VH%tu(ibFTQyZOFu(nkJsg6>Sw z3l^KCb!krdeF>V41qF8uuE5`RSWYo!m&s(Z&-;!pu38xV{bpaJmfP&hQ#kaC&hAP$%DC>_$Id}8!40&Rlaixs z49;;qKR6MdaEIPjfG@jiTNEGuMOFIV=9zt;r}xcWKea9@CrKNv?;&`yCv4+hyN1_U zgVjfWFTVWtVRos3g-^(1mNtD>uG=nG-&YmP`rX&X^)E|&=9{A>%5hIbY|Ul-mSsRI z$ahb*^3%U&0r0|TZBy^BzlzHJRx+m8kEQ8EgoK;CJ^erzmy%SAKn3l(4(eSPbvLww z_Rk34Gr2wE^*|&~wykYkdg<&ZG86Qd&c}3&JDHwuW;T`jj;QdIXtw-dAA4Ac)G_8} zowRAP`|x4!mc=_v=|7^wZLzsWB$+%vc*T8xEq^YG5&!pf%r0?xIo}v$@WQN)MYa-l z%cs$ITH*qSwN>*B8`%-g1GmV}iAUnpDWgNe&)jNzcix|9WqBhMzPM!@Gx<}Ijg!T1 z{jV%@Yig`L{4ckaKdq#qY85=kRWo#g(qKODkC1d_^HO&oVs=j_?Ux_MT%V#9-g4vn zMSt%0!Ct2t)o{g0t*>eELkVn2=cR_j!=+tY83oN5X(XiXJkzvdG~`BN&+EaA6&*hvl6V zE7$oUcwB43=V_kLceyj8se+WidKDgoT)f?CCgnc2D`Ez zSIdPrOnSJDTZFgvzgle?=G;p8dNM~Vg|_5dzBYAemZ55vRvM+Vj!NQ8vsCaA6@ij# zzvdM_%`B&p;VXM%)LFz;*H`Z8VhBX@63-{PV~XfGE@0zv)gcM};ZT(f8gD7C@(0)D z$a}Z?-XW+tQf7^n7~!9Y2X^F+4i{sOUw!w;zLeFqHs-_L_OneS|80TsQ5=rulzJLe z>3Xi1Idn+VehL;L<7qT#8%nynhCuH)aq1(l4b9^-eWyfDU%NoATd{^jy?4n9k|gEj zhkuG#Lj&JLiE9fdWRY_gw?dfo%Zr23)M7~C@$jgx91~G*>~(_M%KkCHfHQa z=a2!=%Q2R;u27m0^})IdGKy2m5=^^*Y`APl>&LJdQ86~Tu%d&?T8gldTIqgEN?1%! z2RWBNLBY`HC}=_SPH%!7)?w(AO&l6GMbg@dzu1ytQbobDK+hY9>!_f8V2%qdUeZgy z;&T)TNO4CH`rQ7RPSg%ZUN%{m>;AdNwMv|GcM9)!bskbFNNDFXyRh?4okuJQEUALH zFeh8Yvle;(Ow+cF`+E9eM&_Ovf;bYM2T1*kYvlNYCm0V;ASDwOo0fmz=tFL!P7POHFIXi6ps< z-~D937al_kk2>_OhX37~hj`HpWZdU>yRTO(11Z_J4)%`oT=&1WQisCUjO3wp)hM&t zHtF)%Hjx*|J9ED9UP&XAb>>_w7+`lUX44m_p-N=XuHNL-Rn`dEE_8*9JRS%%1D33uQwS!=5~}k{kw0!gZBi!FmFhyzA`IC!&FGT zGuID%Fy&z$+?`%ij|t%*c~9n?i@^qSb3!M%nxCX3M2#Qply!!@mU!XiH*$R3teng_ z!`)O^5Bjyz)y1R3?E^JaZ$iO)s{kcgr4OG_lM{OKkeaqSE^0;r>7+3vBxN z$w3W>^I2Mt4<|CTzZ}%xPL3MO516~H%X<&tr3Wxo-%jIjG`1)S8V`k&R~mP1XM{Yy zoX#%XeMuFUbYBE*GcJ$ZSf6tjxp4TxVI#iIx>nli%Ic`9dd2Yz7m>#UX%O-S;m>?M z?=nkwjjkn~F#E8lPWucM^KyM%|0GxEB!Bn|ck5j9fHxRtW_60_2v0Q2T-0p&Y$x{c zy^NribKoQ?PXrEAXAbWb#h|`MR-)JEDb_586MkWnUR_Sf;Q=htVI%drF_>FdL8~ficO8E5<9}}LOJk)tSwz%yg=T!D7)XKMh^BviV2l0OSymv|r3o`~UcU(PpmuBflJ@CoIrPhcZ5F=`mOs-P z)jO;I_i2r4|4ibv#^(Oc_tWJkFDOMOPItUHe-kEo9_>%H-%EGurfD!*&fg6(_|f+D z<eL_S@(Kl>~6sc_-*>>Z8Eft<(E&O<@Ase13Fx(h3+P+SPD z-M>hLCH`>oY++M5^idY6Q^0rP?z>)PAAb|!2^hcJn;mm&h8I0M7)*A9G#c zIqGNRe~c4e%q-ZCIS!(bLx4B^(i~%rS5VBKqCQJM39->~&Ge?dIW-Zk_^HAVn%~Ctfc>eQc8f|}R&QEH#>;d2+eTm3HaA ztZGfQ=jJq*rg;sfOQdBzJX{jQW5q3(hh>oY-_D=^ojzrr%z0cxJV1QfS%oS$#aziy z9W_WXYeE+2Z_EXWx1V_YrU*5qK9-<7B2BxLs=V&lfXAM-e? zvm}LDVJs>NV4nfFT=*_%Db7cC`AU1IX3`r0jO4JXa%hVB)a zdVdabx@^2jZW1+iKPAClwAzU!dR7nnx#L8$g`sMJSzXCWV((L-z;)eB-#P$Hm?Klx z5PVYeGb+ZR_eZv-#gP5+?5(0 zD^sw4v6e#C_&|`*)Q@?0hE*;FIzQ1+F7H*jC8({hPfkfmX_Fcgwf|{NS}dqX0xh+) zd%vJSNiU`VibSS;Y?htN^BE?dky*dmj7VsEl$B53eB*so9NVuC^a^|ZGdm*h3#wuA zeZCsO2R{|N)zV`$BLn$+T+MMmGv_5&wip%V4OZD1(Q64g${X{aCH+rr4+j?>Py6j9 zx!l2UXCwOTbIAM>$BeaAFJnyplsO_8sHkXQ&SoZ$w5{^S*nNi0o9inG<9_nJspc`AUTO?7j7QhBshtGtV z{pym?ejPFppW^Jp`yuC8R}w&YmnPn!`^8ra`xkeifeJNsWblq*L2`wi*n`Zk%{9S1 z+4%fCc6aG5sMZ^=cnU(Wzo?l)%MqcIIkQc}s8?$efr?>X!IGC?!4*mr4)&VtCH+<% z0w*cYb8>PDt{1f}u6~`vte{3s6txQXuX!bO-h?L#IrlKa;MrQ$#G_g#Syb#6DUu4( zcJ?cJ76{Js2gkN|=%RkL{pjW{Q6fH9gfK6Es4&>4m*66(0+XG`j-QOoAuROVezUI^ zcPj0|j@)|uVMrm%<+9%$1C+tRmFF=s zrJ)ZAr;ELv!fR`XjseoYcWo}CqLSv`smUxRlB%w+UjB`lJ)fd;d4z<77N-Z#Xe|uo zzo~fi?UsdJ^`~iAhwDFxtQd(HwhP*TX(~x?!y^T+yu<4&3TiV5wV^&Ek*C`$wdBbU zALbAhJprBe$jC^NkdGgcx%#}&v?5NE{O+z4U>3tg$Su%+)C%3P8NQd$qX^7k?sHn{ ze1lJI^cwT2)W(RVG54RnH?9V{4O_8|TYP%chH4^i&zk)fyO7hkF`&;i)c*O*38;Y( zo20d=^dLxB{nx!qKi6X^FK%ko!Xcs5D}R_wgeidr7<26Cs1_j>>H>pxanaz? z{r9VPXy3z?d@mNElSY@9m-hgR#X{>WR%wP;&CN~l=26zWIrMB8r}0}EpTAa~-`R%> zZklbl;&#IhgwoJsnS1K|JJ0i3`uC*#0yVJls=K3EG?}e}iQtCgFa~$C%X7OMb6>5}U%&VBea4+`W_swRq&`i5Dl)EqMarQxR zt=fvc!1l3oGQMZFw(6(eIxWDIz>Rf_uSb3S{FrH9+qC!go`!yF(dNbNzrzCLT5Pg! zKX3LG#XWZUk^7UlbG86nT592nRnHmd=nC9c9kLpFD_=+kY*Jy$yzBeEPsFZ{nVB=&sZ;D8_1ZQ#CQj}WWm<&Qr>6@` z0~sRS+h0SiRUTE^6!alR7pJ_q6Pu+Mge!A%VO)&v*420(*r8%+z{kqfb6T#zaU$(n zPo4}@B+9iwS<^W=`|%RgTN0i}rm04@rDsUG@liw34@zub7J}lEBZ1> z*9}1N!-bVz?>oCq^GjNf=LFnIPiMd5>IV5$((Z1+-=6)Q-XkQ?DS9O0cxvBgxg2vU zC8qAp!Cj>H59hwN3#xfn{~PSS=SM0A)fzU}fvhj%wvL=Idi$Cenud*{9WS z&h9+woCLBx*I&c7X-LHgApO4Z*qhEtC^!BsloE?s!vq-7*{u>e8}*DhHsl&Lnl(Y+ zXafvO>9Pa|;1Oi0+f9?d<;RDae#)Gcrlt${1VE!re5o;gOdiI$s@;|^e?K_X8Ru%x_u#1Xzjj|-?$ZPrCm@0D;=$L&7+231IR#8dZPffk~2&;Fj)-HT@k}kVzJ?7g9=;HVM z-nQd`8AU~8k67Qvp=<-?Ts>NWVWtJfL{K~7~<^BEAwjHlHVbA4vs@GhqS3s zSeU-wasEVVEUJGyjX~C$qA@x>p(mNH0MorSqn@avuOHDuE;n=@k9Js70u_fJkA4SoeCGAJ>moZ0rNX5roIL1+ z>p2-QD1fBJGomL_4wh0yT^qL9ArX8Ocn*@RH&`Rp0dA*(gF$EKx$mgy-a*Ezva%gH zT5RK!ljr5-+4}nWzLz+jhB;7}C~<~$q^GBoK+mzEZl>2=Et%~4?wN76JILKZDPlaY z=QqDSo?G-bO&~#cXK6Ss;3iC0_WbTQ)FZ~gQN8tFiUmd#)VSU!G>lNO6ND@Y6gd7! zAWOB9hY)cVqzUN@(2hU+V#&4moebz$x4_1OQ+w#}PtT`o^(<{@r2U;)d;!%$u^48FGnngh6w&<-=SSw}V~HsR%xZtH@)dz$WA=y;_`;xJ=CJAjx@zJV z|IXI6iHC@=<~c3X4Xdi*dgkljaLI41FX_)nYheRhh!7G)wtwLdBk!7YHP~)e>YWeF z=$<;J|Gf7icvR72qa|>A>^lHThWIA}1w~G-Y5*(5!nB$sWx*>^G(>5^?I0*!bRNIm z9Hs~X8_?&eiU5#ZmNGV<`O==^-irzj+>S78El8L*g9%OJD)aQ>-sk3fymUv=b6OPC zyk-9i(KV@w2?^H<46T7hd@pllgPBA}uR!vUpFeM`&d4jS2f`&WR7|=oJ_O! zFb4oQd-cB43E*+nIxNwj%V(wMrd>x9T;IT`JM(b+bQnyL1dPZluUn%*86UTwZVl0+ zg4+cp|7UE*FyAT?CZ-Z#qH0<`&>nqv7eYU=^u9S@I=`4!!UUFX)Axh;0RhCflHVBb z{axRwd;Bz!$nDHKm6iT6qzwRf#PNW>w!^9|py9l`PjF7xjVQ>^DW=r~Ua7g?ePPId z^`&9*3EU2}Fz$hl;>>IL4rp>m#J+?cY%?W$M_6{1H!(b0ewBOg!qYkkEhEs~qvE*h z_wk!q(5={%szsn8rL%GoCuhQmeJtzU=iNKvl;n}mTx)|)U2lW|QWBW5f-zwZS=n3*)&o(|Qb?(a5iD6diJq+JK zIrJCj%{GQ%;PaTCF=$W_TZ4e8yk)g5h&>^9#X+&84y5!?p6S%-&fo zl?V$8GFYgO#hc=;15>~$ulokGug?!TF2%g$|6XKI|AX!4<(-<09**#cHS7z$+legR zKYMQ(PkBPnq2dsHPmqMnp!A`mnw{XTJ}#17D0qtkcEQ{hQ_e|(q&geh6qkCt*l{6D z>Q3*C&0J~#k>st*>D`H?7mg!;0*OF5cb>)Y2)R!?5nHQpTHWu1XIaTb=O!50G9JsJ zGx$(y)5T0oSz>p2+}PyUWNN;;cqIw`T6y!G=>+e;gfr=Wp8^2?H}suDUS*NLRJlkE z>70SXw1yeB4Upj1K?lg5E&@@xJ!z507hq&_!!7Kjo0Yc(zSvg@xx@X36M zIb!D-V8@`w+o0#ks>|f)Xjmv2rD_^p`kdd|w5^SzVnjo)>bi?^few+ge<4WYKf$0E zU(Mt0i!jc(__Z8>z2Cd99yUl6^=Ii_qR;F-&kao&Ilx$8dQ4Kkj=+XN1k%mk z^M!Z>)Y{1M#@95`4QR1KV7(}l{peo-#~^=f&c~PVX46j`zUlbq3%DQf*&FUG7s>mU zfv`9AYdJ5CiQ&E&S6!e)2EFb}~Wa{h@g40ao(cD!gNWZjRt zrOsll#-K=v>qAg}WGOFlYXALXhb+@58Fv}oH6C3Li&QI{duYM?C0T~RpN7^>hBBD0 zObOmm`NYYSM3O>(vLr z>VPOUs%OY9-r0sW3R! z*R0b(MPml&uM$ihI~|Xcm{oXD!zi~z|0VcljEa^v^4>jqap!)UrHVes<0YswT)C}% z`NQnHWt~g!cumXlQe&O-ZD+&>iN$YC4sxq+ zuden!>e|KmN>6yKsn_UV$?QIzKarPJh4aU;au!m}CXDPd6E zZLz!pc}6|)-LChOe&v(vh0$ZW`#wqX^v3Qb{JvKmd^N;MUtJ~(qkE<`T!Z(!Qk*a^ z>-S{nVq|9pv7hkt<*i4qRy|_HE^{y$uYmU;J9^}Z^1#EC^wN*Lk9}&4A6{iGuCiz| zHJ^|?RpJbMCcSrNW*p)_N_xG_JET@K) z?Gwbr){2pnr%zkArOGm`Y#IA_t(R`QbN&@g--XzHZflG!OeE1Zy}K2_Cr%OBT;s}p zMF5DDl*zF?_%vtWUeI1tx=Q@T-yKD#VZ81bBV!oME@R(k!_+Q>H+C*Aa!9f^?eC>R zr1c9v5F=D%0cJ`#te~TH3u*0hy*6dhP;MVA!5-IgzdRC-2~!#t;Qfz{o~^{?r~m zLrT*AOz8-hiRHt-grvm3R-o-UzGZbCcF1w%z0k=DuQ^O1$Fl+X$}!`B(%AvcsTCl{ z#I<&v_5EWMUHx}&w|$u54_1OX^qF0;9LQOV9Xi4|!o-<~4%c*>N=!Lbpy$Fp$`Wy4 zT#M5O*n9Oa9UX@HVG22FnoAMB*2>hEIWHd-Wp58-v0bVm zM8*4Gq7S(0S2a7Jn)mJ>qr=5swRow%}p_DSO!)Mr@bgPaU~ zxcT`%fvpjvBZQzbVVg}YTEe<#wIC`&lSoXUEfG>CQ&9(|0ih(+I=R%RGE;_lRAt}u zwe1E+Us-f#KSU9tXZ~kTiYfr`{fW(w{~RuloB8^$Spe9!ctlud^FL1XpG^Y27JK}! z{Qz@GssOa%JrtZAs2EA@{HlmJL!C;pep;Q;2{|1`(s#^Y^u zQ;7%X{iV47qVIn*1|kPH`=7?C0d(Oqz)zW><+-rhvd z{ZqvL@0X#dyS)?gl+)gYERV&V5@vUzp9I%vgB=I|Td6NKmAI?}89L>XiJj9j{~-tS z?JtL+8=K%#9e3n1{Hpiz8y?Vw7hVz6`%wGuycHEyu!2y}J0 zS_Bjdb^=S#4Y^^JpGDN4^l{oPrnqufh zc2C_G@2AhJlUL|>kmoA#3fXS0J-qzBFO44h@$bk8vJsogXu*RPMcA@C)j5YfueKjl_{o z+@w?y`QLlSB9k+bw!ttay}4A&Hy3}Ru?E?>bOB4G|~C|tJ& zE3oGs@V$HwVv{x>qJX-SzzG*b;a(DtN0y(NZtX~_d|604hqA2tTyyI^$LnCOmQ|mM z-nz`q%olOCFmu49UzJ?q`oC(BM-wLOND5VG@!i?LLGykCPHFl$lyFU2OrsQ-ov?Rx z)#INSFPXz$Uz8WVI{*21pu_u2-rJ|r zo}9kKUaUDp=RNm(+%S9Yi){&$l51qx{T=85@9oEB$aVI&L>*M9xUn=uL0C#<0G8d) z4U&+Vy4b3~&xjcMcr@eRx5Xi?Id~`rTl?w1?OK1_JLz{C8d)j@pugd-Qoh=J#SA~6 z#D_T|6-AfdAG;B4Ry^}X8Il+e)^?bRjW}f%-Fb_?VvUJZ1*>DefvW2AZzpwetD$d; zL7$1W}vX&f3y&K z27|oBKnC^A<|%6BM346|wMli+JY9F>UlqjL)K_dtvwTxdP>nTwv)-EVi2hx8IOa8TQKUoy`^R>0u_BsS*6bw! z8+T)U%5V1TfVMiu8|LFZTJxVRtpfU|?RX`MYejT)KCi`@Z$Th}3V3xWh2(39CtIFj zeCT*@qY}L~Rt53Lps5RI2b-JMu&w|KlQp@yv#|Nup01l%W*&Ttp{oCe5!(CW-5yGv zdFjTIEQu{HDU+}|E|;g_n(GhAapSs|%iHjfUU)(=Rw4~V=um|=Zs$XhZ#Yh!tIbz> z1o=ZP6mlp%d5gz{3nAw#WHEN!VsG1DaG;X`+6+R(u#wPg6Hh(CVRm~~gAnLVSR0K%LhoQRX2;lrP4~ch5(wHkLB8gWN4w*SXNJD813iNR;`1w^ z(J1P=b$Qp$QyU%vg(r}$pF02gXpRS)z(g*Wn9i9)I<&3PP3ST%p_Y@wOtms5t&Gqf z4cDcjDK}PE^3cPgrZ@hjG)Ze@0g=?D?DjrMsk`GRkX`D_vS?*x5$nJmPl=Yb2pYaC z9m-Lp=l`>bMwB6}#+>ryTi@x?0^;`y7)iUECp|Q&1wUw9bwZv0+^;uF){&lRJ2bPi zq4(!2O=JnRgVmUE-E^41a6kNMAa~xsNyO^gY+~7G)u?sI=$LKYTmI*mdrcYRCEFeQ zH(B^v=zZ`65@i#FuVnOhwM{aZ2*0qQQAx0%x5Wy)>)cES=yHY)<*dblVI|ak$L!;8 z0qo=GWzvoS8YU;h5LQ?<34QAlLAqkgnsJfSo=!d-P8Q$B~h0HVgMHig`=<@tp5TXNrqI zIyaxV9A2a2zs;uxQ(M1fF9`G8YI7@CA{|c31_|OsXgeOl2jsTH%8)g+W=&Ip0m z7AWfVer4%(_vUJ!m6ON(he@i1xRx^G7U@gmE!sS3=aTE=Ios}H{+z=(M{61q?uWjs zmd1IV&O;`;or*&3ADH<_3Oku zVbVkP5g<;vT0)q2pS-l?JlbCm`sMCgQlo*f*}kj4E8^1`FI-#}#>2U&6>e`?Fk$)B zT(`>e-Clg9(Jzc{+gd62cdE{(F56cGJZ6JYzx`Rhyo$?t^>39k`#^m9+N;(2eyxML zm1vbC9Ch(|eE~7W?Bg_VvDF|Rw$)%XI^Ieay`9@uS*o$ldvgz69UJK@t%n*gW`FwZ zOKncQiSqO^eGhFM5J7sz%Bdc0BeVW1J|0ZW{*1Os$)VP`XYxZO2-`B!B9TlZO?HVb zqfnZZ0Lpt5Azg_LfhXiHg2i7Ba5$b3%>q>RG*5@)m4Yr1&$E;tUj}j;r==VFl9K@XhK{SRM;K zsAH>@9v$zAEEJKZ28G$Kr!lc<6dPh+jjpQH*l8M9FR z)a`ZB-8CPa*32PV;YSh5j41?X&QHHKAIyHPYb7doL&f6>oolVu%~uILT6x+fE7qCw z{$+71uxXZ2Sj;8jl>Cw`F)uDlWHs_^WxP+TbM%DWIdBRVDaO0D`tI00gYEpA(rOFK zC6ldf(RQ#MBeCaU_T1QP7ScEwDtkkIG)-0Nb9t9V77@9X<{X)jO4Svxf3)$=T+ z=9Zb>T?=jfFLq~cYI)j(Vi+Ui;~@QP^&F$NYG-YIPgZ&i(yG0VNvq$A^!V6eT_sCz zb@C)fFTI<`vOMm1&b)kgRcB@MitKO_Yy@}gSfA8ftetdAvZq|M%Dihogljf-a*5k1 zgX8Uc62I#ej%kaRk~w=euRD$)o`=ASZ<(Oh=+L{lFh*UP3D&L9wVw(MkwkXv$c}y* z^@RbT8hZdna{lU#M;ZB#Hk7CAS-~`la%dfB%rSs1+Is&Grh>Wd@2OC2;S8%VyZCN> zf;XU6y&xYyCL8E6wo8_=7G~5ao|W=GW{pASw%t-MIO7X)$$E^aY#TE}))>e|A0q6? zCq8ta78-HExto)Yf+BZM63O6LSXp~VE#|tzpD?)$_S1!iT-I7Z530vg@5NMnZ7$%` zpLQC02%d?}aKf)KavzX{D28F=CANFV4UzW9AGqR@@59gH-48EL=1<|t$9ldlJWxd$ z*b1bg4L#?zYm?94r+*i5niqJ+`h<2jrF43wuh=rp>oU@})$OjdCg!Vn`2&=Fo!zs% z@dIID*FnVANN!aPhqxx4=B}~m7Ct~C6NriER&x4?&p#NVR?01RJ}c7f$L@2UwNGo_ zSS_;gXpxtgkK?n7ykMuiU4j5R* zDq_IWzLD_$o=7g!nfk#`_2okkeH_0Zv3vZU;=by!`(WY}S)PKQo3|G5e)pYN-Wn3)lkRbX?}Y|JE3wclJN9~0#gMEhG5_x~0Jzv-sR4T?a~-cwhG zBw;3Hgo}NAL0gFMyN{+KZIjRFMRp5fY)@lGxBD-qc{OSmobpqI(VgnJ>q<{u6c5wU z^l`t~0)PvgDQgLa1aOC>==xKKspW;C03Mn5BRF7+Xew@cVmG62d*vntCS$&|)I+lB zev-gQtHz!%6jY=?`4@{=?W+=3K4viZ~vqWY2sMyRVmMp}KxkJaU# zG-`R|r{Gj(hM1vCm>T2_djLZlnWM@a&sz)vlo3H`?$;}rmc!DYTzF@~d!M&Xe#L+2 zIl&iT8c383WwK+f8Mws6)a5A(Hb%NieUpqqgP~bkN1U0rcGY>R+*~`iNBlx^Ob(4stbkqLwJX^#~QT>H(mp?tg7&687)zx(Xlm@5#X`7XVe`=l2@RN z?)yQCw0lbFkraIwd?o&#ZT0!H!^)OtDvh#j{E7v4*H0m_L=W%jF@UI4@aVYFH0p1= zrpB=)Jesty3jKQ?GTFy8&IOqlc_rV+d*K^r7{K7cd~7nY=OI<7dtdhFhwo5sIh?*t zTL|P(3pD`GCPcJe=Hnt+zyvTbUl)$ICCd8pbQ%ctu-O2l+L_3M&f(`o#oN%+t0p>Ers1Y*#34Ywex@|qu)Mp?BNhGoMStt9FW$uO5sGXXV-gvv*Aw_MNIc#Dna!*;^m$RfmAC zK0gu2!y#CCqhsprQ?dJ7Gu?(frL(G7vzk)pKa$c9NPU&{@XQQ(9f93Ic__mWj_$&A@me>(}d%GH3ujCipps+ggYyv*M)_J0crFGs3w43^~8x#wI?)cY1CwpC~%3fp~(p{0Tp&>Jme!)ur9+Zj8Ad0GC>f&+FG0PJnoBOh#i-1t7pc zKnd&zRIEBxvk#=bR!=3H=g4pP;cAzB^9fkcVIT8q1k083d>c3Q+;MQgQ{+A^Ijpj% ze2b;kNpe_Y4eZ?bu)KPwMTzBD}iZACf3iUve^qZ|UanLhVM9O`!>iT8WQKPji6I4DyyGLXBb z$rg{~<$W!x_DBT8YAdi>IkAsiu*V$lp1Uj$?#l+9M|~vTus}TWa$)m_IXL)OZlU_p z5TCQ_obB4lz_1woGySNcdzO14WZ~~3s!z8+Mo%?B1RAuzztpcUAeO`|ow@N>gbM?l z(0%cP_yysqVW1ukL51%_e~l-S!|RFW15@L>FQjDQ@02 zraE3DA~z?vqAuzM{>?q`ZyGy9lkdw3#ahEKx)N*N?L5T#wb-9AZ@aysK9H@B1t45B zNMJeNbK$Yq50TsUWZZ+&_mcKyk zzH#N!Rn+?FK&_vF48~8#FY4j#_&o)1iyHifYVK+_pEp(RgBJ6RI^3g!bj#l)ne! z+9I{HH%2U3bm8VO?F4cjH@L{bmcJ%w|L|BrH|u&R&|FFu>A#s6gx>q=Wdrg~0fFRm3*`{iUcCS%4} zaajN@iR;i$&`*{#XW6VkuaIZD5-$zI+Mdeix4CdIui`@NI*J1UCOM;VAIE1|4GF1- z(MQJ;#sU8Rb%j4Tnbx3X;gux3w69AK{b5EvZ^*PsQ{rdVw<2 zhtY)GeaK`e(D{!n_COX|2c~5r&5Cg*D;pu4#ERXR^D#xkPe@&dOm=y0oYqjli4B

4`oWx}~*OCKJJQu-z@fymRHVO4eQDX6)yMD@#kaPs=#DjDW)y zdoWu)96{76s`Z6ok%)+vZic)5dNB{fSI$4>m4y<`F+3HVyMNt44*;qp zje}ED)>2LOh&5b$$ma>zu|^J2o4TJR)+V8xvce&Yb7qmLeTo2ZV=gL__H7$8cKP7_ zl~HhC0&+{J%*eHI++_ERe3S?;p@sNB1_*pnui;4=yOlmli?XBoG*cba3qs@&L|HRq z{d|V8`$hULY{SLXxU5t7`|*_)I{ zyo0%oV1liYljz`?5FSnAVqw9A08<~~+170fOl)k4GCk8oqX+5g(JmLy1n5ud%Vc52k*SC1+v%HoAc=mQ=IxUPiJ~|X`p%C<&u%^LmVN=Q3(5_@`qw4DV9A$B%TI5ik`>2n ztiYPawDWCaUj4*38W>M!*h7jVJ3Hmd-Fme5Txc2wu$1&iQUl@vh3yE;ld?MryN5*cyf2Rc%g)&=g9n8wubF?9M_QX&yz~ zVMBg+^z_!_SJ}E!NL~K+FPG7Xx-G1M!0@W4^T!eZPNz7;R~8RnB2CwOJxc?8?(cJR zb72A$YW_6=3*Ct!sj2kWA`nlTRF3D=Md%SL+wPEwO!M-wQhJhglYq7*0)n>amHnd@ z8BsBJ^b)70c@;+`3y4sbA9tna5(DdrLYzNxxUs+N>A$c51s1@EDE>dfK0C{hZ>l}G z6&IPXe=QhAArah2;{JH{!^9rosG$-fFaU1&dCM;Xbz+{P90xm&|&IG4bG z3=(Fxc@%NzyMkgEwzHD}mOW5gte{1p5m#MCG9F+>9C9?`V}cdX$t4!247)2CS~zg# zq#o)txL75?4Ngl`!g=#6vKZd;3l+Ye--%VX22^)(AKp^5R1taBN@M7Mlc4$Pe@*NG zD1N5yFzNRv+Q#~I98T21&9!8F-%$S!$(d5RWKN3P6S&AJDBz?WuuQ%M5?r9g%zaXU zBJ(>tTqM+EuY2|Mdl+qg7!275SjX=GUfBqxnD0sHSxQ8{{xKchis%M|3ka|T$J?hn z5q^E~iFDyzM#pEfu_}K*}>#WjC*0E)RKI4ZH;HlbY&om*L9fK;_;}< z=FZN*;rRyyLI~N^cxtmY20&FH9o5My9WK>0_pClSOw`GA9!{UA-*T(T_=fUsyElI4 z%}V&P%-mLkeNNjHum(i}hxETCQ*91|=3kI@>gkGB2_`6K* zcL*?dmPY#rrby?tD=RCakTEwnDSx{ZnhAiW+1(nhccJa0_P+ew6b5GRe7ZXfXDqkAIQ7AzsJ8$pH zAEj+=JpDecmXR>DmbLG2%DJDE>&~9@z1{p5;>sE;dBXq4qOGadGBwXtVFOK9iy0Or}mZsSG)P^>-IT zM4?D)925pwaJ_x5eDQsgG}ePpW!67L*w)qtG1((IISC~0_%u>^&UA`z2n*RXR`h$u z;!(F}y@tBI^<5Usyz#FsR_ zj%o#ZfIq~k<4rUe-G8%N?^2}x+dbF9MZ?5;nsce)g9DX+9pw9Z5E}Yl%|^*ekOL2o zgX5K|6m(HP{&3xGg1;7au&eA@Mvx;Px+1tO*6Uq3QqHE&Yg4fxb~%9n{4NgmBd8yg zV&UV5(L!8y7w0vqp78(e=VfdH)gi`dv9njBe4%!87yNg=xfoXA;p3CG`+5<7s%F!3 z4gFMf4>O}cIKutrBfft;E6GIP{atiycnNyI;duY&@*U~DAo?Ht7wFsz^+wnvBqZK` zu;1h$T9xjnqA&H0d+5@2X_W87e?3D+yX(>_hERc!w9}P%QIp3>tK^R=Z7xV086XOE z0V9iji|z~FxOkU2E`jKysA<=ecmI1q6XPEAR${ZjC5T?cDq`L7`hQ)({PVA3+p93h zSVwB(-Y)F_9YcFP<#M_GdB7gc#S34`Ql|$Z7>liLKX18+x9q)J??6jO$9Z{vzU9*w zo+}sZ&;xX57V%^BXpWmdI8a;&6V_yZK@9$oIEKP^KgY(yVsD?VVVx1eFy{ZgV1#s& zSz%$4Nl^MiT_cZ8u@%cdACa9Khmtn0Li<%yG|UZuER07TQSK>O-2aicvgwty1>=og zETH*|+B%=MH28i4{Xr`35Z1UabW$iT_0_AJKRN%%4i9KQWem06OIRgWi228SMBsZK zu|1|#GXLqLffn#3YQy0Bw-=*)3EcmXIFoV2)SbBQsvP~$3$H1Y7DHWTa7P_qpVj|I z9NC#axA%*n$2k%D1)rz$P=7>8rMpHZt}6e7yPy9hnm$~l@0yr|L}~%ouv!{hVZL?f z^c{r-Z}sYUmJDoiTuY4w=>fm3b-YEx|=d&D#1H{znFpl;W5o|@EFrSWbb@AZSu87ME zo2%%U7ck^1-%k&?jXI7H{o_`8x9-lPd4(5z6vR?vf)|SX%ys|!BeZ|Ka`EN#iMpZb zEy{qqn$XxFF_U67aN){GdgFhj+h=MuVD&4=d6x%Y5|iM9^+}^W%V>C$GlQJZe_m~; z7{sCpbg4e7rc?24OCW>%DHnfr^pC$3XR$|`oBwk^rZ$mfGMMzpdy0)YMBZqayR(ig z#DV)JwDa}U#e5%1W`D2w-t`8Dcp9HVC+bopy{irZewe9r%`bE!*Jramn-el2hd~6; z2B4@$@XhSHE2?_*2|=y@?;Di54A1xr)(a#bJ9K10Uz6)%;S20BYua)m!P9#&VO zQ7)1TNoPg{@}1Qsv|_=^psJ#?#s3hA3rn$>uP|-)AD>rI)$>42$LhNS!tmi{)EX)O zcut-2enXy!UWSeFm6f4mqjTq`%(vwD<)|HzX6xf@hsx{JezDh8|i@0Ukr3eH(no7TMgz939l%7 zFPvUr9qQzAOte)mZsXFRZPFR<+h}8q&vhV6u{Nc_F7-CJ4ktuQlh6H1r29q?VTT)s zg*ttDLBO}4JS`kH@)XZ$eL~&ITWt&Va%1ltIkq+59^DW3_n*D%{M|-Q|GKwU4qcH^ zG&}0U_Wh3yG8W}1NCm{mj9akJ2m5SJ|;zrBiNJRbSP8X&40>-Jggr3=5m2`Q@ zlTTLGF9)wa8LuVweREb$eBHNYIZNpdp$V`ajF!D&K}R-K7I_BvFPVn)tJ<(LTt!{A z@P+u9#~J2kO08)7PFpJ$7KdeMD;AE#PL%$1A20hv0CqUS<^|w ziu;0wxK|K4J=Cs6w|;K&Z}+R*Z)*>AU(6!;#|Oe{5=_$PI=5~~M+;!$7U**UDNcxB z@Xtqczi2r0_Tq2JQ}Bm^bsY*im6ebn+)ZrcrX4>3YU1{q zn`*o`6sSEE*?e{jjJdLh2=x`4mK|II-25k!TM~+30rmdtKA|5NoTg_=H1uER;nRO$ z3|jOZB=Y*_gF?@)ds4KiGJF(~esb+v^Jqke>6A4S^?yA^5aHIw%w7YGjRR5o)(IF` z^=~f0o@4Z5q*g-0#4LoX4^POfE(hjP8jtPe{s(vOtP{wm!1kI;ejMfR|9++{PbZX^ zNW~#kI+~Z%@n(bRlqQqNKc8V-PD$l8u#vukL!fB&L+oV9%4ABZ5BHxFQPdbjhBNr1 zY7CyEd7Hhs^ua%6+HAX!kyh@)E#?~9s#ireyuX2R14RD&68kAC&_4y&Q0YWLtXlLd ztuEPp&VoOq-FeSRsrV!mmKhxDo7as8BC6P-0XuPL^o zSq}NH&%Rj10VJ2(FwSV%!?mX;Ht>TmKgPCDvylOxBb5EPo_$vDPY0+N;_bDfmqT** zg~CiftO<2XHoJJdmj?~UFa%byPGo}6Yos6e`{Z?R-3R9uYItAolauuyE zTRjQMYIqB%r_8+cXr&m@`u4wHHR_i>?;Q2s-pEfaZiu1Si0? zx_5C-VQFKX0dW>UcRd&fhb)8?(gP7Qb)hJeEIc^nbjj*;J26u?2iiEGzM7xTQD^Qo zlLImoKkODH0DftgLkQY=@bvN?oBmZkRM!J*Hhw&ihqvnnDmU)4knepA)wZ{n#8J!nh$Vp=Uma?LDA36$q#UygYJ;g66++#|{ry?K z<#_p|cwefqMlziqdzf>suC3mE{>vZ^3+JPl&(YyecUP#ye^VEc6*XGRg`^^k7pQD& z-};)Jx{jVn!5@Eoq{m(!yv`-%k+N&AksO>10)}b^1uX|emCOxqXi*C_{_JsH%uZkw z-d}?`>f!FO@Z^VDfJzATZ2SCKg=uB>VFZ z?%ckdtHUgmxbg*84C5gG39XR+)>1=EG?!h%*v@S(N_a*8am4l7&O|>4whfE>wRQcM zV265{4#Dt@7@uXMMFHY%g|Vb~+KGh$|FfOj)q7(W7(c!$?^)7sgVs{6Sln{r_VSk& zS)t)@>@-`|E7okV!Z#!t`A!KcESe&h9Y5VE(ff+~i`Jk?&OZ;zNd&nYzQ?b-TEP5v&` zxx=SHWJyh~8}(L$GS|{8?epZ(1K=V4qea&^1WRY!tV4GWu0LdenK#(~&@@tNMr-8w zqGc`@J04jP9_P~sUQ(lS-%A!nqN)^3scxPU&x|T=z7z&xPn@SzsKA`h`iiaSmvhR4Pu7@ z`pIO4dNqRWk?AgbK7^fdf@|?3GO;#3n2J(xdO3gvmfQ2oKI4xt1*pu(b5`T0iM56uUWdhHMLdQGP4?ZxjFl?u_bbDSi+}$@RSkolj=G@e4cnh{t+>eMAy9W0Sa@cTvgk$MP1(!CjRj`19!qU%` zcks<@wECV4^hByKZlN)2a2Xws^>6JHQZ|_tlg8_FcZWTtVqhDj^qGIj@5T8|r;pKW zLXZRlG1;*4YA?peuw3zOJQq++V-d#91M_S){RH=sYv4xPF+cMqST(R-9;6)YmYntF zQjt78q^yH^r!q;Md1C#U8$Y1(^?Euzsi{FG*aW4Nx6RW{NpIKADmx*+u_TM}=9Jmv z!e}VVHU{#RJ2)7|~+5Tt0sDYD^jwNh#3L zP|H%ef~d6jh9kJp?E`v+Ky~cGh1<7pzXmu3v^Rj_uwTOZx)UVeHY=*gaSC)MwU+R^l_fg)-Tc2<0FINS!%#G!v^=LfNP;nDeVOLkJ04cBv|zJmZ^yU_}QLv zMK$)LAFi150S!U5qio2U_DRZ4Wjk7c@};8`V?hlRN`a8djui-|%%)0Nn?3F+?Rn79 z(RI|*C>t4!O#Qe9TaZhE)bPCxphVCrRdb0iLVpVyI%hSQJ$h2mkxM)jSOb_VU3WFk z8y{1VWSC&Eg93Ay$>{Sb14UWU(HkXzJM2MqwZucox=z4l$lrvoOiJG^K& z$^7`1l0+{kpnk#m)eMI<&661JH~l(RNl;B)np+QMQqQhi&MTXSgPZqjDg5;py8AyF zhH0|r63aJNN{Xea(YWYr`r2boRwb#dP}3`1L9a(xXr!P4s=SO%*IGd0Shoc-JOjT+ zGI)LlTYk{6kWiSf9<_U_4eg6Zm1u8Qo)36Ni{LV)8KS+_A?lPOkJ_Kv8PXxy{?6wg z5b#DNOTy4FNfMoihzQ7t=%9&biq!i$9UhBhODtm8Kxtg7beM($cR8Pij#M0=gOon(w!_N5Bkl+2**0;so8Mgdz}zK2AA*5cXsP(40arO(+e?$%j1?L6;$@1^Q@En?w@(z%gt^qZM8LXjj7go`G>r*L0i z9~1XoRDWV<)!RF5`HbSg*>`o#>bWNS*8ZC!Gb{qwl+vDZ|3H`&?Np}d>HPB5(KsQa zx*D-npc*^iUTJm6iM_f2IHczHX-Ag>v~h4GMdBX~+X7G5fP>w~e~*{@Zgtd$_uy?t zRvOrX+3dU51zs53{GeL)S?0$v_JoqMFW#{^In56v2}wzW+vTKySh=UxN*~%0lbXCn zbq3x!-ciL~?G-mp^xrkg+1;`YRhoLIG2GM-cY=lLgV(hECiCOfPIAHY0gL$fIFye& z!M`_bPdI;#Af@(*0N5tbmK)x&p;{gZnuV^Oic3f=1c-{6CEob?Qig!@)Xj#CUkSM; zgi`nSR+3A*`#Y2*>C_`302Q7cXbT70fZK#uQCoVNLJ=hA^9DOmm>jBSrk~?Gj&IBP z_aoisF$szUOH+VxMsiuoop_kn5^n<`yC)3Ux9tWR+RGj>yy`L0<<;8N`}um8vSI&Q zo}Jjj#tw1a5DVO&;%j@5q}87;-Sg_)5+B**f-1~&oYvOO@XRMJJ9ihSk$|lOP(R9T z)j4*6BT`_&NM0oPx3fX6*=)EtZPguenhP!I-i?RTGbie!KD=NVXt(I zLRf;LS3|9vlvcvGX6af{wbSNb>N$@i5Z7SxJF?ZXyim>D#N7KTUOj_eXHs)TnVHam zx(BLpSEnwnKxID@oL;EKfGm7P>)&H9ja~-<5gsM;4SWaeLBjRG52BFunT@@K>XOG4k==I+g}0z9bN5ZHpmxAX4Umdi zgyeZWz%7_gO|ATI!OMT2bl3P@?#WvQp8z&VrU~K|nj&bG_orMAlpdt1iO_s*Q)@WC zxC|6H8j|%~h0m5%I-@xnX!Q5qYCly-#7Rty>_C;4if&PJTcG)hDbIK;O;mho_Hlyt zcc?~P*nKTf7ckpBJO0mIde%}xAd{|~#J&&ogEW9`u|;)W)X&ti#e{Z51&YF92+P*O z-DJyqu6ry%p~PH*GZbhzU}GyLDk|!GEDBur&`B81#6$UTd08u~Mx7nFM>3H=>0QYo z`G0t~gJyHPtba2)QnU?R=^pU307woVktUx71MCHTmBCWc%bMIC6tF1R$Oa2=L*88a z5r84*o&8^#W>D3XBiOktFL4 zfE$B~=_sa3#OF54e+bS%gadmYYB7j-3R0F4%*odj0C2=I5Ne-KyW+H#cQs`YpS~!p zjj3nj3V;I!F3DW&`6>Ga11Oy1iIK24IoR4qAc9cl_mg@khGrQNR*N%)&JJubem#6$ z@en$_-CYg>Pn^Sk1nBMXN(N<-l{J)SJP_0s!jgmN*$3r_gUjv&%Lz7|qUVzo*MtNd zUc-5ma8J^hjdsGNsRDi9+L6IpVPWU3%Zds2Q81-Kd*sk;DH$&m->nEai?UmW zk42Nk1BuKQw}jzsWUgT!yG>w%50>gFqHI#%`%TnE4!oF}m0rqdkDBtt7^sSSoH9izTovWXRSy@^I-DGcSL``AXDUNuRv_H1!V^Q%pnAwc+zt)5 z2=&Uq(|j62%IX?MR;4%KKG`)7+`|KzYM~=l)#a=k5ITj}TTJ0VQlz-trMHxXN&1s3 zLh~>2CpR~@?h`-DQrG|gS`5O@D1HeeSAOSLz{*#YS znB7{_ukp`&0xu=~#`sOdncV{$&5=s8H_a}Iw>f*09%7Y8@S7BzMN}!D9%+iW5;eQu zgU)ifN9#Hu47b4)L5N_7GV$Tf>w3RI1k5kMZ&1^s={8Zx19aJB*LU09*Lt(ALp_)I zWImr+8k)tW^HWgrE6v#Z`S^TxT;ON?-FgXW4LH0UZ#sK>W41{(+amrpdxeG!={phH2; z_;10npHP%#Nq)!DB4}EvSxrYb~U&6L4y}uhqrHYF!E4&BZ0sr}i zSVMt}ogXSlrDp^W%V5xPN(|%TR78^$H@NHV_k+^uqtfM}H*XuPW8_r=K$ie)>UV?J zO2s(-H@Ih~R-=IfBfBbFC4f8~+rZo;8%_e}Q|s@S_?b;?Q7QQ}VNnw(vAZuI%ny9G zrSb54J6PDH|C5O55g1Xu6WD!|SZw-*((9cO*!}XP=mmg|Hh$Nu-;cd)FzX ztq!uK96{J6I}-_}O&K*4??rGRYOP*M0rZpSj~I>h9OFFggVX(~KjNP#w@Eo5-j_M4 z)Pi2`8D*0n5Yj=q?6)Ea>8VW9=YGWue!90s4iV_13|=jjKZ2LjWgl`1lIwZ9yEKZZ zy67_aE6{Z~Kh7!C-=AAkF4QTxwK4UZH-rxGHBIyrFNEfhE|Mi(qX658 zqFD{07KxS3a=ADSEjFF1Uh4i)h7YB`Qzls(gS_3bShSZF&?$;C zT>!{>8|T4!Ea7oeAc`}x;qA4EY{Gj`V$Tm-My{iIdY;Sd`_V?h=9lalvR-^%s{k%R zlaZIi7s;TWS%l+EnRy!8ZbfH%1NtWD@ACa!aP2R{cG0*tiP?6zyoemCvDFtrju8TUfbh(#tgLL<3{t_a*pvb{0B=sHm*H~H z_9w=XJ(9sSEC#k-1*&^$D^HFx7>n;h3=*^p($gp7*H%GXsPnpl#l6xDZKr86`dZb0 zuZD3rR$^{$o_5@bCu?LxC-8B^1NIx9FW3Zc?I7NX#V`ylS$O{FmODAoN3{+$vtqwEA z(HmDs7+gI+2@c6_(rNr&1ytTOVrpt1bKRj8WU!6++t#yl67a_u(J)ybdkE%rzRJJ0 zCNC&>-|e8MQQW6E>Ue)+j+)23b&@#p^)8(iLUWskvxJfoMP;mYv~#qJv@U%e%Zi5| z<^~|hIt_%YG4!Yf*ZCs?)jU#MPis1+R^$LV!>Y&+yLX-9a#>fr{8*wc935RebL`Lo zIqSmbB+3-xp>^@|zHeUnw}5~fA>bH^kO#jQQ_d!+bNTZH{*&!QNs;f%FwjhRXqWF@ z39z&|9MYeDFAj^bflL4sux%iY*%S_`|Mgk>AUV+!8wddYbbeS_DC(EbG1LIv1G?25x~pWAj^T5Pwn9kjYhq93_&HvzM5a@7S(U7 zv4R)Xf&q8dPPr35>x$!Oy&yU@|FbRJ8nTY2R+o$rN~5rvlXN>GKSBNvy0*`NXXvsU zFOyae=ANIR(9A%uJ|{6`6h6u6l3+*PPLQ_&{XvfNBzpIH20Vufhh6AF8l9Fg=8|SCBNZ8K*w#Rph4@YYR@LJRR~ANm+RC`cd~sTCQ@*9~OW$Y0_i zskU1t0L-N~`_XPg6vB0`uNPxkHcV7~Y2sN4p%Rjn=7a`(f_XfMSEYMZ%FZ zFH^`)#%OU(b2!}k-F)W^b_nG~i2)tig0I&Z8GJ

1~>f(gVdq^NZINKai89#6pt6sG< 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 literal 0 HcmV?d00001 diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_simple.png b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/docs/resolver_tree_children_simple.png new file mode 100644 index 0000000000000000000000000000000000000000..f05963c0f0b677ab0882628de635bff7714a85c1 GIT binary patch literal 22889 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%0Dcg{&6(YS?=)aT0-r6RgHFfT*uf(W7+h$d9Q`N+ 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 { // bucket the start and end events together for a single node const ancestryNodes = this.toMapOfNodes(results); - // the order of this array is going to be weird, it will look like this - // [furthest grandparent...closer grandparent, next recursive call furthest grandparent...closer grandparent] + /** + * This array (this.ancestry.ancestors) is the accumulated ancestors of the node of interest. This array is different + * from the ancestry array of a specific document. The order of this array is going to be weird, it will look like this + * [most distant ancestor...closer ancestor, next recursive call most distant ancestor...closer ancestor] + * + * Here is an example of why this happens + * Consider the following tree: + * A -> B -> C -> D -> E -> Origin + * Where A was spawn before B, which was before C, etc + * + * Let's assume the ancestry array limit is 2 so Origin's array would be: [E, D] + * E's ancestry array would be: [D, C] etc + * + * If a request comes in to retrieve all the ancestors in this tree, the accumulate results will be: + * [D, E, B, C, A] + * + * The first iteration would retrieve D and E in that order because they are sorted in ascending order by timestamp. + * The next iteration would get the ancestors of D (since that's the most distant ancestor from Origin) which are + * [B, C] + * The next iteration would get the ancestors of B which is A + * Hence: [D, E, B, C, A] + */ this.ancestry.ancestors.push(...ancestryNodes.values()); this.ancestry.nextAncestor = parentEntityId(results[0]) || null; this.levels = this.levels - ancestryNodes.size; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts index 9e47f4eb94485..3f941851a4143 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts @@ -30,38 +30,9 @@ export interface Options { } /** - * This class aids in constructing a tree of process events. It works in the following way: + * This class aids in constructing a tree of process events. * - * 1. We construct a tree structure starting with the root node for the event we're requesting. - * 2. We leverage the ability to pass hashes and arrays by reference to construct a fast cache of - * process identifiers that updates the tree structure as we push values into the cache. - * - * When we query a single level of results for child process events we have a flattened, sorted result - * list that we need to add into a constructed tree. We also need to signal in an API response whether - * or not there are more child processes events that we have not yet retrieved, and, if so, for what parent - * process. So, at the end of our tree construction we have a relational layout of the events with no - * pagination information for the given parent nodes. In order to actually construct both the tree and - * insert the pagination information we basically do the following: - * - * 1. Using a terms aggregation query, we return an approximate roll-up of the number of child process - * "creation" events, this gives us an estimation of the number of associated children per parent - * 2. We feed these child process creation event "unique identifiers" (basically a process.entity_id) - * into a second query to get the current state of the process via its "lifecycle" events. - * 3. We construct the tree above with the "lifecycle" events. - * 4. Using the terms query results, we mark each non-leaf node with the number of expected children, if our - * tree has less children than expected, we create a pagination cursor to indicate "we have a truncated set - * of values". - * 5. We mark each leaf node (the last level of the tree we're constructing) with a "null" for the expected - * number of children to indicate "we have not yet attempted to get any children". - * - * Following this scheme, we use exactly 2 queries per level of children that we return--one for the pagination - * and one for the lifecycle events of the processes. The downside to this is that we need to dynamically expand - * the number of documents we can retrieve per level due to the exponential fanout of child processes, - * what this means is that noisy neighbors for a given level may hide other child process events that occur later - * temporally in the same level--so, while a heavily forking process might get shown, maybe the actually malicious - * event doesn't show up in the tree at the beginning. - * - * This Tree's root/origin could be in the middle of the tree. The origin corresponds to the id passed in when this + * This Tree's root/origin will likely be in the middle of the tree. The origin corresponds to the id passed in when this * Tree object is constructed. The tree can have ancestors and children coming from the origin. */ export class Tree { From 827e91c4474e8790374417d30b433296f909b110 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 30 Jul 2020 11:16:49 -0700 Subject: [PATCH 4/6] move and unify postcss config into `@kbn/optimizer` (#73633) Co-authored-by: spalger --- .eslintrc.js | 1 + package.json | 2 +- packages/kbn-optimizer/package.json | 1 + .../{src/worker => }/postcss.config.js | 0 .../basic_optimization.test.ts | 2 +- .../src/worker/webpack.config.ts | 2 +- .../kbn-storybook/lib/webpack.dll.config.js | 2 +- .../storybook_config/webpack.config.js | 2 +- packages/kbn-ui-framework/Gruntfile.js | 2 +- .../doc_site/postcss.config.js | 22 ------------------- packages/kbn-ui-framework/package.json | 4 ++-- packages/kbn-ui-shared-deps/package.json | 1 + src/optimize/base_optimizer.js | 2 +- x-pack/package.json | 7 +++++- .../shareable_runtime/postcss.config.js | 1 - .../shareable_runtime/webpack.config.js | 2 +- .../canvas/storybook/webpack.config.js | 8 +++++-- .../canvas/storybook/webpack.dll.config.js | 2 +- 18 files changed, 26 insertions(+), 37 deletions(-) rename packages/kbn-optimizer/{src/worker => }/postcss.config.js (100%) delete mode 100644 packages/kbn-ui-framework/doc_site/postcss.config.js diff --git a/.eslintrc.js b/.eslintrc.js index c9f9d96f9ddae..b3d29c9866411 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -529,6 +529,7 @@ module.exports = { 'x-pack/test_utils/**/*', 'x-pack/gulpfile.js', 'x-pack/plugins/apm/public/utils/testHelpers.js', + 'x-pack/plugins/canvas/shareable_runtime/postcss.config.js', ], rules: { 'import/no-extraneous-dependencies': [ diff --git a/package.json b/package.json index 51a41cbbab9ff..880534997cff0 100644 --- a/package.json +++ b/package.json @@ -480,7 +480,7 @@ "pixelmatch": "^5.1.0", "pkg-up": "^2.0.0", "pngjs": "^3.4.0", - "postcss": "^7.0.26", + "postcss": "^7.0.32", "postcss-url": "^8.0.0", "prettier": "^2.0.5", "proxyquire": "1.8.0", diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index c11bd1b646933..4fbbc920c4447 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -36,6 +36,7 @@ "loader-utils": "^1.2.3", "node-sass": "^4.13.0", "normalize-path": "^3.0.0", + "postcss": "^7.0.32", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", "resolve-url-loader": "^3.1.1", diff --git a/packages/kbn-optimizer/src/worker/postcss.config.js b/packages/kbn-optimizer/postcss.config.js similarity index 100% rename from packages/kbn-optimizer/src/worker/postcss.config.js rename to packages/kbn-optimizer/postcss.config.js diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts index 2d0d60da1e4a0..bab47d4a1e412 100644 --- a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts @@ -161,6 +161,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { Array [ /node_modules/css-loader/package.json, /node_modules/style-loader/package.json, + /packages/kbn-optimizer/postcss.config.js, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts, @@ -171,7 +172,6 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/styles/_globals_v7dark.scss, /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/styles/_globals_v7light.scss, /packages/kbn-optimizer/target/worker/entry_point_creator.js, - /packages/kbn-optimizer/target/worker/postcss.config.js, /packages/kbn-ui-shared-deps/public_path_module_creator.js, ] `); diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 3d62ed1636869..ae5d2b5fb3292 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -152,7 +152,7 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: options: { sourceMap: !worker.dist, config: { - path: require.resolve('./postcss.config'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/packages/kbn-storybook/lib/webpack.dll.config.js b/packages/kbn-storybook/lib/webpack.dll.config.js index 740ee3819c36f..661312b9a0581 100644 --- a/packages/kbn-storybook/lib/webpack.dll.config.js +++ b/packages/kbn-storybook/lib/webpack.dll.config.js @@ -127,7 +127,7 @@ module.exports = { loader: 'postcss-loader', options: { config: { - path: path.resolve(REPO_ROOT, 'src/optimize/postcss.config.js'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/packages/kbn-storybook/storybook_config/webpack.config.js b/packages/kbn-storybook/storybook_config/webpack.config.js index b2df4f40d4fbe..0a9977463aee8 100644 --- a/packages/kbn-storybook/storybook_config/webpack.config.js +++ b/packages/kbn-storybook/storybook_config/webpack.config.js @@ -91,7 +91,7 @@ module.exports = async ({ config }) => { loader: 'postcss-loader', options: { config: { - path: resolve(REPO_ROOT, 'src/optimize/'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/packages/kbn-ui-framework/Gruntfile.js b/packages/kbn-ui-framework/Gruntfile.js index b7ba1e87b2f00..bb8e7b72cb7bd 100644 --- a/packages/kbn-ui-framework/Gruntfile.js +++ b/packages/kbn-ui-framework/Gruntfile.js @@ -19,7 +19,7 @@ const sass = require('node-sass'); const postcss = require('postcss'); -const postcssConfig = require('../../src/optimize/postcss.config'); +const postcssConfig = require('@kbn/optimizer/postcss.config.js'); const chokidar = require('chokidar'); const { debounce } = require('lodash'); diff --git a/packages/kbn-ui-framework/doc_site/postcss.config.js b/packages/kbn-ui-framework/doc_site/postcss.config.js deleted file mode 100644 index 571bae86dee37..0000000000000 --- a/packages/kbn-ui-framework/doc_site/postcss.config.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -module.exports = { - plugins: [require('autoprefixer')()], -}; diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index abf64906e0253..7933ce06d6847 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -33,7 +33,7 @@ "@babel/core": "^7.10.2", "@elastic/eui": "0.0.55", "@kbn/babel-preset": "1.0.0", - "autoprefixer": "^9.7.4", + "@kbn/optimizer": "1.0.0", "babel-loader": "^8.0.6", "brace": "0.11.1", "chalk": "^2.4.2", @@ -54,7 +54,7 @@ "keymirror": "0.1.1", "moment": "^2.24.0", "node-sass": "^4.13.1", - "postcss": "^7.0.26", + "postcss": "^7.0.32", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", "react-dom": "^16.12.0", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 8398d1c081da6..3c03a52383f77 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -21,6 +21,7 @@ "custom-event-polyfill": "^0.3.0", "elasticsearch-browser": "^16.7.0", "jquery": "^3.5.0", + "mini-css-extract-plugin": "0.8.0", "moment": "^2.24.0", "moment-timezone": "^0.5.27", "react": "^16.12.0", diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 41628a2264193..74973887ae9c1 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -34,7 +34,7 @@ import { IS_KIBANA_DISTRIBUTABLE } from '../legacy/utils'; import { fromRoot } from '../core/server/utils'; import { PUBLIC_PATH_PLACEHOLDER } from './public_path_placeholder'; -const POSTCSS_CONFIG_PATH = require.resolve('./postcss.config'); +const POSTCSS_CONFIG_PATH = require.resolve('./postcss.config.js'); const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); const EMPTY_MODULE_PATH = require.resolve('./intentionally_empty_module.js'); diff --git a/x-pack/package.json b/x-pack/package.json index 3a9b3ca606de6..2d7cb148c43b0 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -121,8 +121,10 @@ "@types/pretty-ms": "^5.0.0", "@welldone-software/why-did-you-render": "^4.0.0", "abab": "^1.0.4", + "autoprefixer": "^9.7.4", "axios": "^0.19.0", "babel-jest": "^25.5.1", + "babel-loader": "^8.0.6", "babel-plugin-require-context-hook": "npm:babel-plugin-require-context-hook-babel7@1.0.0", "base64-js": "^1.3.1", "base64url": "^3.0.1", @@ -159,6 +161,7 @@ "loader-utils": "^1.2.3", "madge": "3.4.4", "marge": "^1.0.1", + "mini-css-extract-plugin": "0.8.0", "mocha": "^7.1.1", "mocha-junit-reporter": "^1.23.1", "mochawesome": "^4.1.0", @@ -168,6 +171,9 @@ "node-fetch": "^2.6.0", "null-loader": "^3.0.0", "pixelmatch": "^5.1.0", + "postcss": "^7.0.32", + "postcss-loader": "^3.0.0", + "postcss-prefix-selector": "^1.7.2", "proxyquire": "1.8.0", "react-docgen-typescript-loader": "^3.1.1", "react-is": "^16.8.0", @@ -308,7 +314,6 @@ "pluralize": "3.1.0", "pngjs": "3.4.0", "polished": "^1.9.2", - "postcss-prefix-selector": "^1.7.2", "prop-types": "^15.6.0", "proper-lockfile": "^3.2.0", "puid": "1.0.7", diff --git a/x-pack/plugins/canvas/shareable_runtime/postcss.config.js b/x-pack/plugins/canvas/shareable_runtime/postcss.config.js index 10baaddfc9b05..e1db6e4a64f71 100644 --- a/x-pack/plugins/canvas/shareable_runtime/postcss.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/postcss.config.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line const autoprefixer = require('autoprefixer'); const prefixer = require('postcss-prefix-selector'); diff --git a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js index 93dc3dbccd549..43e422a161569 100644 --- a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js @@ -111,7 +111,7 @@ module.exports = { loader: 'postcss-loader', options: { config: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, diff --git a/x-pack/plugins/canvas/storybook/webpack.config.js b/x-pack/plugins/canvas/storybook/webpack.config.js index 927f71b832ba0..982185a731b14 100644 --- a/x-pack/plugins/canvas/storybook/webpack.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.config.js @@ -77,7 +77,9 @@ module.exports = async ({ config }) => { { loader: 'postcss-loader', options: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + config: { + path: require.resolve('@kbn/optimizer/postcss.config.js'), + }, }, }, { @@ -114,7 +116,9 @@ module.exports = async ({ config }) => { { loader: 'postcss-loader', options: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + config: { + path: require.resolve('@kbn/optimizer/postcss.config.js'), + }, }, }, { diff --git a/x-pack/plugins/canvas/storybook/webpack.dll.config.js b/x-pack/plugins/canvas/storybook/webpack.dll.config.js index 0e9371e4cb5e4..81d19c035075f 100644 --- a/x-pack/plugins/canvas/storybook/webpack.dll.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.dll.config.js @@ -114,7 +114,7 @@ module.exports = { loader: 'postcss-loader', options: { config: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + path: require.resolve('@kbn/optimizer/postcss.config.js'), }, }, }, From 70d4eac30c381802d87a5112825699ae6cd8089f Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 30 Jul 2020 14:43:33 -0400 Subject: [PATCH 5/6] [Security Solution] Adding tests for endpoint package pipelines (#73703) * Adding tests for endpoint package pipelines * Removing content type check on types that can change based on docker image version * Skipping ingest tests instead of remove expect * Switching ingest tests over to use application/json * Removing country names Co-authored-by: Elastic Machine --- .../apis/epm/file.ts | 6 +- .../ingest_manager_api_integration/config.ts | 4 +- .../apis/fixtures/package_registry_config.yml | 2 + .../apis/index.ts | 1 + .../apis/package.ts | 140 ++++++++++++++++++ .../apis/resolver/entity_id.ts | 20 +-- .../services/resolver.ts | 25 +++- 7 files changed, 168 insertions(+), 30 deletions(-) create mode 100644 x-pack/test/security_solution_endpoint_api_int/apis/package.ts diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts index 733b8d4fd9bd6..3f99f91394d2c 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts @@ -47,7 +47,7 @@ export default function ({ getService }: FtrProviderContext) { '/api/ingest_manager/epm/packages/filetest/0.1.0/kibana/visualization/sample_visualization.json' ) .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); } else { warnAndSkipTest(this, log); @@ -61,7 +61,7 @@ export default function ({ getService }: FtrProviderContext) { '/api/ingest_manager/epm/packages/filetest/0.1.0/kibana/dashboard/sample_dashboard.json' ) .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); } else { warnAndSkipTest(this, log); @@ -73,7 +73,7 @@ export default function ({ getService }: FtrProviderContext) { await supertest .get('/api/ingest_manager/epm/packages/filetest/0.1.0/kibana/search/sample_search.json') .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); } else { warnAndSkipTest(this, log); diff --git a/x-pack/test/ingest_manager_api_integration/config.ts b/x-pack/test/ingest_manager_api_integration/config.ts index ddb49a09a7afa..85d1c20c7f155 100644 --- a/x-pack/test/ingest_manager_api_integration/config.ts +++ b/x-pack/test/ingest_manager_api_integration/config.ts @@ -10,9 +10,9 @@ import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { defineDockerServersConfig } from '@kbn/test'; // Docker image to use for Ingest Manager API integration tests. -// This hash comes from the commit hash here: https://github.com/elastic/package-storage/commit/48f3935a72b0c5aacc6fec8ef36d559b089a238b +// This hash comes from the commit hash here: https://github.com/elastic/package-storage/commit export const dockerImage = - 'docker.elastic.co/package-registry/distribution:48f3935a72b0c5aacc6fec8ef36d559b089a238b'; + 'docker.elastic.co/package-registry/distribution:80e93ade87f65e18d487b1c407406825915daba8'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml b/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml index 4d93386b4d4e1..00e01fe9ea0fc 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml +++ b/x-pack/test/security_solution_endpoint_api_int/apis/fixtures/package_registry_config.yml @@ -1,2 +1,4 @@ package_paths: - /packages/production + - /packages/staging + - /packages/snapshot diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts index 56adc2382e234..b1317c2d9f1c1 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/index.ts @@ -31,5 +31,6 @@ export default function endpointAPIIntegrationTests(providerContext: FtrProvider loadTestFile(require.resolve('./metadata')); loadTestFile(require.resolve('./policy')); loadTestFile(require.resolve('./artifacts')); + loadTestFile(require.resolve('./package')); }); } diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/package.ts b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts new file mode 100644 index 0000000000000..3b5873d1fe0cd --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts @@ -0,0 +1,140 @@ +/* + * 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 expect from '@kbn/expect'; +import { SearchResponse } from 'elasticsearch'; +import { eventsIndexPattern } from '../../../plugins/security_solution/common/endpoint/constants'; +import { + EndpointDocGenerator, + Event, +} from '../../../plugins/security_solution/common/endpoint/generate_data'; +import { FtrProviderContext } from '../ftr_provider_context'; +import { InsertedEvents, processEventsIndex } from '../services/resolver'; + +interface EventIngested { + event: { + ingested: number; + }; +} + +interface NetworkEvent { + source: { + geo?: { + country_name: string; + }; + }; + destination: { + geo?: { + country_name: string; + }; + }; +} + +const networkIndex = 'logs-endpoint.events.network-default'; + +export default function ({ getService }: FtrProviderContext) { + const resolver = getService('resolverGenerator'); + const es = getService('es'); + const generator = new EndpointDocGenerator('data'); + + const searchForID = async (id: string) => { + return es.search>({ + index: eventsIndexPattern, + body: { + query: { + bool: { + filter: [ + { + ids: { + values: id, + }, + }, + ], + }, + }, + }, + }); + }; + + describe('Endpoint package', () => { + describe('ingested processor', () => { + let event: Event; + let genData: InsertedEvents; + + before(async () => { + event = generator.generateEvent(); + genData = await resolver.insertEvents([event]); + }); + + after(async () => { + await resolver.deleteData(genData); + }); + + it('sets the event.ingested field', async () => { + const resp = await searchForID(genData.eventsInfo[0]._id); + expect(resp.body.hits.hits[0]._source.event.ingested).to.not.be(undefined); + }); + }); + + describe('geoip processor', () => { + let processIndexData: InsertedEvents; + let networkIndexData: InsertedEvents; + + before(async () => { + // 46.239.193.5 should be in Iceland + // 8.8.8.8 should be in the US + const eventWithBothIPs = generator.generateEvent({ + extensions: { source: { ip: '8.8.8.8' }, destination: { ip: '46.239.193.5' } }, + }); + + const eventWithSourceOnly = generator.generateEvent({ + extensions: { source: { ip: '8.8.8.8' } }, + }); + networkIndexData = await resolver.insertEvents( + [eventWithBothIPs, eventWithSourceOnly], + networkIndex + ); + + processIndexData = await resolver.insertEvents([eventWithBothIPs], processEventsIndex); + }); + + after(async () => { + await resolver.deleteData(networkIndexData); + await resolver.deleteData(processIndexData); + }); + + it('sets the geoip fields', async () => { + const eventWithBothIPs = await searchForID( + networkIndexData.eventsInfo[0]._id + ); + // Should be 'United States' + expect(eventWithBothIPs.body.hits.hits[0]._source.source.geo?.country_name).to.not.be( + undefined + ); + // should be 'Iceland' + expect(eventWithBothIPs.body.hits.hits[0]._source.destination.geo?.country_name).to.not.be( + undefined + ); + + const eventWithSourceOnly = await searchForID( + networkIndexData.eventsInfo[1]._id + ); + // Should be 'United States' + expect(eventWithBothIPs.body.hits.hits[0]._source.source.geo?.country_name).to.not.be( + undefined + ); + expect(eventWithSourceOnly.body.hits.hits[0]._source.destination?.geo).to.be(undefined); + }); + + it('does not set geoip fields for events in indices other than the network index', async () => { + const eventWithBothIPs = await searchForID( + processIndexData.eventsInfo[0]._id + ); + expect(eventWithBothIPs.body.hits.hits[0]._source.source.geo).to.be(undefined); + expect(eventWithBothIPs.body.hits.hits[0]._source.destination.geo).to.be(undefined); + }); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts index 4f2a801377204..231871fae3d39 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { SearchResponse } from 'elasticsearch'; import { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants'; import { ResolverTree, @@ -20,7 +19,6 @@ import { InsertedEvents } from '../../services/resolver'; export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const resolver = getService('resolverGenerator'); - const es = getService('es'); const generator = new EndpointDocGenerator('resolver'); describe('Resolver handling of entity ids', () => { @@ -38,26 +36,10 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC }); it('excludes events that have an empty entity_id field', async () => { - // first lets get the _id of the document using the parent.process.entity_id - // then we'll use the API to search for that specific document - const res = await es.search>({ - index: genData.indices[0], - body: { - query: { - bool: { - filter: [ - { - term: { 'process.parent.entity_id': origin.process.parent!.entity_id }, - }, - ], - }, - }, - }, - }); const { body }: { body: ResolverEntityIndex } = await supertest.get( // using the same indices value here twice to force the query parameter to be an array // for some reason using supertest's query() function doesn't construct a parsable array - `/api/endpoint/resolver/entity?_id=${res.body.hits.hits[0]._id}&indices=${eventsIndexPattern}&indices=${eventsIndexPattern}` + `/api/endpoint/resolver/entity?_id=${genData.eventsInfo[0]._id}&indices=${eventsIndexPattern}&indices=${eventsIndexPattern}` ); expect(body).to.be.empty(); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts index 335689b804d5b..7e4d4177affac 100644 --- a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts +++ b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts @@ -11,7 +11,7 @@ import { } from '../../../plugins/security_solution/common/endpoint/generate_data'; import { FtrProviderContext } from '../ftr_provider_context'; -const processIndex = 'logs-endpoint.events.process-default'; +export const processEventsIndex = 'logs-endpoint.events.process-default'; /** * Options for build a resolver tree @@ -36,7 +36,7 @@ export interface GeneratedTrees { * Structure containing the events inserted into ES and the index they live in */ export interface InsertedEvents { - events: Event[]; + eventsInfo: Array<{ _id: string; event: Event }>; indices: string[]; } @@ -46,24 +46,37 @@ interface BulkCreateHeader { }; } +interface BulkResponse { + items: Array<{ + create: { + _id: string; + }; + }>; +} + export function ResolverGeneratorProvider({ getService }: FtrProviderContext) { const client = getService('es'); return { async insertEvents( events: Event[], - eventsIndex: string = processIndex + eventsIndex: string = processEventsIndex ): Promise { const body = events.reduce((array: Array, doc) => { array.push({ create: { _index: eventsIndex } }, doc); return array; }, []); - await client.bulk({ body, refresh: true }); - return { events, indices: [eventsIndex] }; + const bulkResp = await client.bulk({ body, refresh: true }); + + const eventsInfo = events.map((event: Event, i: number) => { + return { event, _id: bulkResp.body.items[i].create._id }; + }); + + return { eventsInfo, indices: [eventsIndex] }; }, async createTrees( options: Options, - eventsIndex: string = processIndex, + eventsIndex: string = processEventsIndex, alertsIndex: string = 'logs-endpoint.alerts-default' ): Promise { const seed = options.seed || 'resolver-seed'; From c21474b4ce029b820072b53f3908075404bccbde Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Thu, 30 Jul 2020 14:51:35 -0400 Subject: [PATCH 6/6] [Security Solution][Detections] Change from sha1 to sha256 (#73741) --- .../exceptions/add_exception_modal/index.tsx | 3 +- .../exceptions/edit_exception_modal/index.tsx | 3 +- .../exceptions/exceptionable_fields.json | 26 +++-------- .../components/exceptions/helpers.test.tsx | 45 +++++++++++++++++++ .../common/components/exceptions/helpers.tsx | 36 +++++++++++++-- .../alerts_table/default_config.tsx | 2 +- 6 files changed, 88 insertions(+), 27 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index bb547f05090b7..e6eaa4947e404 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -40,6 +40,7 @@ import { AddExceptionComments } from '../add_exception_comments'; import { enrichNewExceptionItemsWithComments, enrichExceptionItemsWithOS, + lowercaseHashValues, defaultEndpointExceptionItems, entryHasListType, entryHasNonEcsType, @@ -256,7 +257,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ : exceptionItemsToAdd; if (exceptionListType === 'endpoint') { const osTypes = retrieveAlertOsTypes(); - enriched = enrichExceptionItemsWithOS(enriched, osTypes); + enriched = lowercaseHashValues(enrichExceptionItemsWithOS(enriched, osTypes)); } return enriched; }, [comment, exceptionItemsToAdd, exceptionListType, retrieveAlertOsTypes]); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 341d2f2bab37a..6109b85f2da5a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -40,6 +40,7 @@ import { getOperatingSystems, entryHasListType, entryHasNonEcsType, + lowercaseHashValues, } from '../helpers'; import { Loader } from '../../loader'; @@ -195,7 +196,7 @@ export const EditExceptionModal = memo(function EditExceptionModal({ ]; if (exceptionListType === 'endpoint') { const osTypes = exceptionItem._tags ? getOperatingSystems(exceptionItem._tags) : []; - enriched = enrichExceptionItemsWithOS(enriched, osTypes); + enriched = lowercaseHashValues(enrichExceptionItemsWithOS(enriched, osTypes)); } return enriched; }, [exceptionItemsToAdd, exceptionItem, comment, exceptionListType]); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json index fdf0ea60ecf6a..037e340ee7fa2 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json @@ -6,32 +6,25 @@ "Target.process.Ext.code_signature.valid", "Target.process.Ext.services", "Target.process.Ext.user", - "Target.process.command_line", "Target.process.command_line.text", - "Target.process.executable", "Target.process.executable.text", "Target.process.hash.md5", "Target.process.hash.sha1", "Target.process.hash.sha256", "Target.process.hash.sha512", - "Target.process.name", "Target.process.name.text", "Target.process.parent.Ext.code_signature.status", "Target.process.parent.Ext.code_signature.subject_name", "Target.process.parent.Ext.code_signature.trusted", "Target.process.parent.Ext.code_signature.valid", - "Target.process.parent.command_line", "Target.process.parent.command_line.text", - "Target.process.parent.executable", "Target.process.parent.executable.text", "Target.process.parent.hash.md5", "Target.process.parent.hash.sha1", "Target.process.parent.hash.sha256", "Target.process.parent.hash.sha512", - "Target.process.parent.name", "Target.process.parent.name.text", "Target.process.parent.pgid", - "Target.process.parent.working_directory", "Target.process.parent.working_directory.text", "Target.process.pe.company", "Target.process.pe.description", @@ -39,7 +32,6 @@ "Target.process.pe.original_file_name", "Target.process.pe.product", "Target.process.pgid", - "Target.process.working_directory", "Target.process.working_directory.text", "agent.id", "agent.type", @@ -74,7 +66,6 @@ "file.mode", "file.name", "file.owner", - "file.path", "file.path.text", "file.pe.company", "file.pe.description", @@ -82,7 +73,6 @@ "file.pe.original_file_name", "file.pe.product", "file.size", - "file.target_path", "file.target_path.text", "file.type", "file.uid", @@ -94,10 +84,8 @@ "host.id", "host.os.Ext.variant", "host.os.family", - "host.os.full", "host.os.full.text", "host.os.kernel", - "host.os.name", "host.os.name.text", "host.os.platform", "host.os.version", @@ -108,32 +96,25 @@ "process.Ext.code_signature.valid", "process.Ext.services", "process.Ext.user", - "process.command_line", "process.command_line.text", - "process.executable", "process.executable.text", "process.hash.md5", "process.hash.sha1", "process.hash.sha256", "process.hash.sha512", - "process.name", "process.name.text", "process.parent.Ext.code_signature.status", "process.parent.Ext.code_signature.subject_name", "process.parent.Ext.code_signature.trusted", "process.parent.Ext.code_signature.valid", - "process.parent.command_line", "process.parent.command_line.text", - "process.parent.executable", "process.parent.executable.text", "process.parent.hash.md5", "process.parent.hash.sha1", "process.parent.hash.sha256", "process.parent.hash.sha512", - "process.parent.name", "process.parent.name.text", "process.parent.pgid", - "process.parent.working_directory", "process.parent.working_directory.text", "process.pe.company", "process.pe.description", @@ -141,7 +122,10 @@ "process.pe.original_file_name", "process.pe.product", "process.pgid", - "process.working_directory", "process.working_directory.text", - "rule.uuid" + "rule.uuid", + "user.domain", + "user.email", + "user.hash", + "user.id" ] \ No newline at end of file diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 5cb65ee6db8ff..18b509d16b352 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -24,6 +24,7 @@ import { entryHasListType, entryHasNonEcsType, prepareExceptionItemsForBulkClose, + lowercaseHashValues, } from './helpers'; import { EmptyEntry } from './types'; import { @@ -663,4 +664,48 @@ describe('Exception helpers', () => { expect(result).toEqual(expected); }); }); + + describe('#lowercaseHashValues', () => { + test('it should return an empty array with an empty array', () => { + const payload: ExceptionListItemSchema[] = []; + const result = lowercaseHashValues(payload); + expect(result).toEqual([]); + }); + + test('it should return all list items with entry hashes lowercased', () => { + const payload = [ + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'DDDFFF' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'aaabbb' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [ + { field: 'user.hash', type: 'match_any', value: ['aaabbb', 'DDDFFF'] }, + ] as EntriesArray, + }, + ]; + const result = lowercaseHashValues(payload); + expect(result).toEqual([ + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'dddfff' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [{ field: 'user.hash', type: 'match', value: 'aaabbb' }] as EntriesArray, + }, + { + ...getExceptionListItemSchemaMock(), + entries: [ + { field: 'user.hash', type: 'match_any', value: ['aaabbb', 'dddfff'] }, + ] as EntriesArray, + }, + ]); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 3abb788312ff4..2b526ede12acf 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -335,6 +335,36 @@ export const enrichExceptionItemsWithOS = ( }); }; +/** + * Returns given exceptionItems with all hash-related entries lowercased + */ +export const lowercaseHashValues = ( + exceptionItems: Array +): Array => { + return exceptionItems.map((item) => { + const newEntries = item.entries.map((itemEntry) => { + if (itemEntry.field.includes('.hash')) { + if (itemEntry.type === 'match') { + return { + ...itemEntry, + value: itemEntry.value.toLowerCase(), + }; + } else if (itemEntry.type === 'match_any') { + return { + ...itemEntry, + value: itemEntry.value.map((val) => val.toLowerCase()), + }; + } + } + return itemEntry; + }); + return { + ...item, + entries: newEntries, + }; + }); +}; + /** * Returns the value for the given fieldname within TimelineNonEcsData if it exists */ @@ -413,7 +443,7 @@ export const defaultEndpointExceptionItems = ( data: alertData, fieldName: 'file.Ext.code_signature.trusted', }); - const [sha1Hash] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.hash.sha1' }); + const [sha256Hash] = getMappedNonEcsValue({ data: alertData, fieldName: 'file.hash.sha256' }); const [eventCode] = getMappedNonEcsValue({ data: alertData, fieldName: 'event.code' }); const namespaceType = 'agnostic'; @@ -446,10 +476,10 @@ export const defaultEndpointExceptionItems = ( value: filePath ?? '', }, { - field: 'file.hash.sha1', + field: 'file.hash.sha256', operator: 'included', type: 'match', - value: sha1Hash ?? '', + value: sha256Hash ?? '', }, { field: 'event.code', diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 010129d2d4593..f38a9107afca9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -202,7 +202,7 @@ export const requiredFieldsForActions = [ 'file.path', 'file.Ext.code_signature.subject_name', 'file.Ext.code_signature.trusted', - 'file.hash.sha1', + 'file.hash.sha256', 'host.os.family', 'event.code', ];