Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.8] [Defend Workflows][E2E]Endpoint e2e response console (#155605) #158553

Merged
merged 1 commit into from
May 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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