Skip to content

Commit

Permalink
[Defend Workflows][E2E]Endpoint e2e response console (#155605)
Browse files Browse the repository at this point in the history
Depends on #155519

E2E coverage of `isolate`, `processes`, `kill-process` and
`suspend-process` commands on mocked endpoint.

E2E coverage of the above but on real endpoint is
[here](#155519).

Because these tests are run against mocked data I've decided not to mock
`kill-process` and `suspend-process` outcome (whether process is
actually killed/suspended) because it would mean testing mocks
themselves. What is tested is the outcome the user sees ('Action
completed').

---------

Co-authored-by: Patryk Kopycinski <[email protected]>
  • Loading branch information
szwarckonrad and patrykkopycinski authored May 2, 2023
1 parent ca780c5 commit fd5309f
Show file tree
Hide file tree
Showing 8 changed files with 606 additions and 331 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { getEndpointListPath } from '../../../common/routing';
import {
checkEndpointListForOnlyIsolatedHosts,
checkEndpointIsIsolated,
checkFlyoutEndpointIsolation,
filterOutIsolatedHosts,
interceptActionRequests,
Expand All @@ -32,14 +32,19 @@ describe('Isolate command', () => {
describe('from Manage', () => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts>;
let isolatedEndpointData: ReturnTypeFromChainable<typeof indexEndpointHosts>;

let isolatedEndpointHostnames: [string, string];
let endpointHostnames: [string, string];
before(() => {
indexEndpointHosts({
count: 2,
withResponseActions: false,
isolation: false,
}).then((indexEndpoints) => {
endpointData = indexEndpoints;
endpointHostnames = [
endpointData.data.hosts[0].host.name,
endpointData.data.hosts[1].host.name,
];
});

indexEndpointHosts({
Expand All @@ -48,6 +53,10 @@ describe('Isolate command', () => {
isolation: true,
}).then((indexEndpoints) => {
isolatedEndpointData = indexEndpoints;
isolatedEndpointHostnames = [
isolatedEndpointData.data.hosts[0].host.name,
isolatedEndpointData.data.hosts[1].host.name,
];
});
});

Expand All @@ -67,13 +76,15 @@ describe('Isolate command', () => {
beforeEach(() => {
login();
});
// FLAKY: https://github.com/elastic/security-team/issues/6518
it.skip('should allow filtering endpoint by Isolated status', () => {

it('should allow filtering endpoint by Isolated status', () => {
cy.visit(APP_PATH + getEndpointListPath({ name: 'endpointList' }));
closeAllToasts();
filterOutIsolatedHosts();
cy.contains('Showing 2 endpoints');
checkEndpointListForOnlyIsolatedHosts();
isolatedEndpointHostnames.forEach(checkEndpointIsIsolated);
endpointHostnames.forEach((hostname) => {
cy.contains(hostname).should('not.exist');
});
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { ActionDetails } from '../../../../../common/endpoint/types';
import type { ReturnTypeFromChainable } from '../../types';
import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts';
import {
checkReturnedProcessesTable,
inputConsoleCommand,
openResponseConsoleFromEndpointList,
performCommandInputChecks,
submitCommand,
waitForEndpointListPageToBeLoaded,
} from '../../tasks/response_console';
import {
checkEndpointIsIsolated,
checkEndpointIsNotIsolated,
interceptActionRequests,
sendActionResponse,
} from '../../tasks/isolate';
import { login } from '../../tasks/login';

describe('Response console', () => {
beforeEach(() => {
login();
});

describe('Isolate command', () => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts>;
let endpointHostname: string;
let isolateRequestResponse: ActionDetails;

before(() => {
indexEndpointHosts({ withResponseActions: false, isolation: false }).then(
(indexEndpoints) => {
endpointData = indexEndpoints;
endpointHostname = endpointData.data.hosts[0].host.name;
}
);
});

after(() => {
if (endpointData) {
endpointData.cleanup();
// @ts-expect-error ignore setting to undefined
endpointData = undefined;
}
});

it('should isolate host from response console', () => {
waitForEndpointListPageToBeLoaded(endpointHostname);
checkEndpointIsNotIsolated(endpointHostname);
openResponseConsoleFromEndpointList();
performCommandInputChecks('isolate');
interceptActionRequests((responseBody) => {
isolateRequestResponse = responseBody;
}, 'isolate');

submitCommand();
cy.contains('Action pending.').should('exist');
cy.wait('@isolate').then(() => {
sendActionResponse(isolateRequestResponse);
});
cy.contains('Action completed.', { timeout: 120000 }).should('exist');
waitForEndpointListPageToBeLoaded(endpointHostname);
checkEndpointIsIsolated(endpointHostname);
});
});

describe('Release command', () => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts>;
let endpointHostname: string;
let releaseRequestResponse: ActionDetails;

before(() => {
indexEndpointHosts({ withResponseActions: false, isolation: true }).then((indexEndpoints) => {
endpointData = indexEndpoints;
endpointHostname = endpointData.data.hosts[0].host.name;
});
});

after(() => {
if (endpointData) {
endpointData.cleanup();
// @ts-expect-error ignore setting to undefined
endpointData = undefined;
}
});

it('should release host from response console', () => {
waitForEndpointListPageToBeLoaded(endpointHostname);
checkEndpointIsIsolated(endpointHostname);
openResponseConsoleFromEndpointList();
performCommandInputChecks('release');
interceptActionRequests((responseBody) => {
releaseRequestResponse = responseBody;
}, 'release');
submitCommand();
cy.contains('Action pending.').should('exist');
cy.wait('@release').then(() => {
sendActionResponse(releaseRequestResponse);
});
cy.contains('Action completed.', { timeout: 120000 }).should('exist');
waitForEndpointListPageToBeLoaded(endpointHostname);
checkEndpointIsNotIsolated(endpointHostname);
});
});

describe('Processes command', () => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts>;
let endpointHostname: string;
let processesRequestResponse: ActionDetails;

before(() => {
indexEndpointHosts({ withResponseActions: false, isolation: false }).then(
(indexEndpoints) => {
endpointData = indexEndpoints;
endpointHostname = endpointData.data.hosts[0].host.name;
}
);
});

after(() => {
if (endpointData) {
endpointData.cleanup();
// @ts-expect-error ignore setting to undefined
endpointData = undefined;
}
});

it('should return processes from response console', () => {
waitForEndpointListPageToBeLoaded(endpointHostname);
openResponseConsoleFromEndpointList();
performCommandInputChecks('processes');
interceptActionRequests((responseBody) => {
processesRequestResponse = responseBody;
}, 'processes');
submitCommand();
cy.contains('Action pending.').should('exist');
cy.wait('@processes').then(() => {
sendActionResponse(processesRequestResponse);
});
cy.getByTestSubj('getProcessesSuccessCallout', { timeout: 120000 }).within(() => {
checkReturnedProcessesTable();
});
});
});

describe('Kill process command', () => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts>;
let endpointHostname: string;
let killProcessRequestResponse: ActionDetails;

before(() => {
indexEndpointHosts({ withResponseActions: false, isolation: false }).then(
(indexEndpoints) => {
endpointData = indexEndpoints;
endpointHostname = endpointData.data.hosts[0].host.name;
}
);
});

after(() => {
if (endpointData) {
endpointData.cleanup();
// @ts-expect-error ignore setting to undefined
endpointData = undefined;
}
});

it('should kill process from response console', () => {
waitForEndpointListPageToBeLoaded(endpointHostname);
openResponseConsoleFromEndpointList();
inputConsoleCommand(`kill-process --pid 1`);

interceptActionRequests((responseBody) => {
killProcessRequestResponse = responseBody;
}, 'kill-process');
submitCommand();
cy.contains('Action pending.').should('exist');
cy.wait('@kill-process').then(() => {
sendActionResponse(killProcessRequestResponse);
});
cy.contains('Action completed.', { timeout: 120000 }).should('exist');
});
});

describe('Suspend process command', () => {
let endpointData: ReturnTypeFromChainable<typeof indexEndpointHosts>;
let endpointHostname: string;
let suspendProcessRequestResponse: ActionDetails;

before(() => {
indexEndpointHosts({ withResponseActions: false, isolation: false }).then(
(indexEndpoints) => {
endpointData = indexEndpoints;
endpointHostname = endpointData.data.hosts[0].host.name;
}
);
});

after(() => {
if (endpointData) {
endpointData.cleanup();
// @ts-expect-error ignore setting to undefined
endpointData = undefined;
}
});

it('should suspend process from response console', () => {
waitForEndpointListPageToBeLoaded(endpointHostname);
openResponseConsoleFromEndpointList();
inputConsoleCommand(`suspend-process --pid 1`);

interceptActionRequests((responseBody) => {
suspendProcessRequestResponse = responseBody;
}, 'suspend-process');
submitCommand();
cy.contains('Action pending.').should('exist');
cy.wait('@suspend-process').then(() => {
sendActionResponse(suspendProcessRequestResponse);
});
cy.contains('Action completed.', { timeout: 120000 }).should('exist');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
// / <reference types="cypress" />

import type { CasePostRequest } from '@kbn/cases-plugin/common/api';
import { sendEndpointActionResponse } from '../../../../scripts/endpoint/agent_emulator/services/endpoint_response_actions';
import {
sendEndpointActionResponse,
sendFleetActionResponse,
} from '../../../../scripts/endpoint/common/response_actions';
import type { DeleteAllEndpointDataResponse } from '../../../../scripts/endpoint/common/delete_all_endpoint_data';
import { deleteAllEndpointData } from '../../../../scripts/endpoint/common/delete_all_endpoint_data';
import { waitForEndpointToStreamData } from '../../../../scripts/endpoint/common/endpoint_metadata_services';
Expand All @@ -21,11 +24,7 @@ import {
deleteIndexedEndpointPolicyResponse,
indexEndpointPolicyResponse,
} from '../../../../common/endpoint/data_loaders/index_endpoint_policy_response';
import type {
ActionDetails,
HostPolicyResponse,
LogsEndpointActionResponse,
} from '../../../../common/endpoint/types';
import type { ActionDetails, HostPolicyResponse } from '../../../../common/endpoint/types';
import type { IndexEndpointHostsCyTaskOptions } from '../types';
import type {
IndexedEndpointRuleAlerts,
Expand Down Expand Up @@ -162,9 +161,17 @@ export const dataLoaders = (
sendHostActionResponse: async (data: {
action: ActionDetails;
state: { state?: 'success' | 'failure' };
}): Promise<LogsEndpointActionResponse> => {
}): Promise<null> => {
const { esClient } = await stackServicesPromise;
return sendEndpointActionResponse(esClient, data.action, { state: data.state.state });
const fleetResponse = await sendFleetActionResponse(esClient, data.action, {
state: data.state.state,
});

if (!fleetResponse.error) {
await sendEndpointActionResponse(esClient, data.action, { state: data.state.state });
}

return null;
},

deleteAllEndpointData: async ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,18 @@ export const checkEndpointListForOnlyUnIsolatedHosts = (): void =>
checkEndpointListForIsolatedHosts(false);
export const checkEndpointListForOnlyIsolatedHosts = (): void =>
checkEndpointListForIsolatedHosts(true);

export const checkEndpointIsolationStatus = (
endpointHostname: string,
expectIsolated: boolean
): void => {
const chainer = expectIsolated ? 'contain.text' : 'not.contain.text';

cy.contains(endpointHostname).parents('td').siblings('td').eq(0).should(chainer, 'Isolated');
};

export const checkEndpointIsIsolated = (endpointHostname: string): void =>
checkEndpointIsolationStatus(endpointHostname, true);

export const checkEndpointIsNotIsolated = (endpointHostname: string): void =>
checkEndpointIsolationStatus(endpointHostname, false);
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { closeAllToasts } from './close_all_toasts';
import { APP_ENDPOINTS_PATH } from '../../../../common/constants';
import Chainable = Cypress.Chainable;

export const waitForEndpointListPageToBeLoaded = (endpointHostname: string): void => {
cy.visit(APP_ENDPOINTS_PATH);
Expand Down Expand Up @@ -56,3 +57,16 @@ export const performCommandInputChecks = (command: string) => {
selectCommandFromHelpMenu(command);
checkInputForCommandPresence(command);
};

export const checkReturnedProcessesTable = (): Chainable<JQuery<HTMLTableRowElement>> => {
['USER', 'PID', 'ENTITY ID', 'COMMAND'].forEach((header) => {
cy.contains(header);
});

return cy
.get('tbody')
.find('tr')
.then((rows) => {
expect(rows.length).to.be.greaterThan(0);
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,9 @@ import { set } from '@kbn/safer-lodash-set';
import type { Client } from '@elastic/elasticsearch';
import type { ToolingLog } from '@kbn/tooling-log';
import type { KbnClient } from '@kbn/test';
import { sendEndpointActionResponse, sendFleetActionResponse } from '../../common/response_actions';
import { BaseRunningService } from '../../common/base_running_service';
import {
fetchEndpointActionList,
sendEndpointActionResponse,
sendFleetActionResponse,
} from './endpoint_response_actions';
import { fetchEndpointActionList } from './endpoint_response_actions';
import type { ActionDetails } from '../../../../common/endpoint/types';

/**
Expand Down
Loading

0 comments on commit fd5309f

Please sign in to comment.