Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8c95dd8
initial setup for a standalone service to create endpoint host VMs
paul-tavares Apr 17, 2023
931e7a6
Scripts: added `getAgentDownloadUrl()` to `common/fleet_services` (co…
paul-tavares Apr 18, 2023
55fc86c
Scripts: Initial version of reusable Endpoint Host Services service
paul-tavares Apr 18, 2023
03e1b3e
Initial file setup for Endpoint alerts tests
paul-tavares Apr 18, 2023
84be7ed
Added `getLatestAgentDownloadVersion` to scripts fleet services and e…
paul-tavares Apr 18, 2023
740f10b
New `waitForEndpointToStreamData()` to script/common Endpoint Metadat…
paul-tavares Apr 18, 2023
5fe4fbb
Cy endpoint policy task to enable all protections
paul-tavares Apr 18, 2023
86b99cd
Added `dataLoadersForRealEndpoints()` support for endpoint suite
paul-tavares Apr 18, 2023
c0a43a1
Cy data load of real endpoint done in test
paul-tavares Apr 18, 2023
1eaa2f9
enable experimental feature in Endpoint Cy config
paul-tavares Apr 19, 2023
7f61e7d
added `waitForActionToComplete` to response actions tasks
paul-tavares Apr 19, 2023
2ec7f54
Added new Cy `waitUntil()` and refactored `waitForActionToComplete()`…
paul-tavares Apr 19, 2023
06e2418
initial structure for checking that alerts are received from endpoint
paul-tavares Apr 19, 2023
d41e63c
waits for alert to be streamed by endpoint
paul-tavares Apr 19, 2023
7b31879
cy: additional alerts related tasks
paul-tavares Apr 20, 2023
ccb6926
cy: alerts related screen selectors
paul-tavares Apr 20, 2023
edd96e4
cy: complete working test for alerts
paul-tavares Apr 20, 2023
731666b
Destroy VM + unenroll fleet agent common script utilities
paul-tavares Apr 20, 2023
43f0355
added Delete VM + unenroll agent + delete policies to the test
paul-tavares Apr 20, 2023
a1b5add
Initial work for a `deleteAllEndpointData()` utility
paul-tavares Apr 20, 2023
a3ac77e
ability to delete all data associated with an Endpoint ID
paul-tavares Apr 20, 2023
9d5d503
Add deleate all endpoint data task and associated utility function an…
paul-tavares Apr 20, 2023
538a681
Merge branch 'main' into task/olm-6030-test-endpoint-sends-alert-events
paul-tavares Apr 23, 2023
1ea3181
Merge remote-tracking branch 'upstream/main' into task/olm-6030-test-…
paul-tavares Apr 24, 2023
5c4ed33
Merge remote-tracking branch 'origin/task/olm-6030-test-endpoint-send…
paul-tavares Apr 24, 2023
9e863b6
Rename prop of `<EndpointAgentStatusById>`
paul-tavares Apr 24, 2023
74dd339
Merge branch 'main' into task/olm-6030-test-endpoint-sends-alert-events
paul-tavares Apr 24, 2023
745b61c
added code to ignore 404 error only in `waitForEndpointToStreamData`
paul-tavares Apr 24, 2023
7445dce
Merge remote-tracking branch 'origin/task/olm-6030-test-endpoint-send…
paul-tavares Apr 24, 2023
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 @@ -344,7 +344,7 @@ describe('When showing Endpoint Agent Status', () => {
});

it('should keep agent status up to date when autoRefresh is true', async () => {
renderProps.autoFresh = true;
renderProps.autoRefresh = true;
apiMocks.responseProvider.metadataDetails.mockReturnValueOnce(endpointDetails);

const { getByTestId } = render();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export interface EndpointAgentStatusByIdProps {
* If set to `true` (Default), then the endpoint status and isolation/action counts will
* be kept up to date by querying the API periodically
*/
autoFresh?: boolean;
autoRefresh?: boolean;
'data-test-subj'?: string;
}

Expand All @@ -150,9 +150,9 @@ export interface EndpointAgentStatusByIdProps {
* instead in order to avoid duplicate API calls.
*/
export const EndpointAgentStatusById = memo<EndpointAgentStatusByIdProps>(
({ endpointAgentId, autoFresh, 'data-test-subj': dataTestSubj }) => {
({ endpointAgentId, autoRefresh, 'data-test-subj': dataTestSubj }) => {
const { data } = useGetEndpointDetails(endpointAgentId, {
refetchInterval: autoFresh ? DEFAULT_POLL_INTERVAL : false,
refetchInterval: autoRefresh ? DEFAULT_POLL_INTERVAL : false,
});

const emptyValue = (
Expand All @@ -169,7 +169,7 @@ export const EndpointAgentStatusById = memo<EndpointAgentStatusByIdProps>(
<EndpointAgentStatus
endpointHostInfo={data}
data-test-subj={dataTestSubj}
autoRefresh={autoFresh}
autoRefresh={autoRefresh}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import type { CasePostRequest } from '@kbn/cases-plugin/common/api';
import type { DeleteAllEndpointDataResponse } from '../../../scripts/endpoint/common/delete_all_endpoint_data';
import type { IndexedEndpointPolicyResponse } from '../../../common/endpoint/data_loaders/index_endpoint_policy_response';
import type {
HostPolicyResponse,
Expand Down Expand Up @@ -56,6 +57,20 @@ declare global {
...args: Parameters<Cypress.Chainable<E>['find']>
): Chainable<JQuery<E>>;

/**
* Continuously call provided callback function until it either return `true`
* or fail if `timeout` is reached.
* @param fn
* @param options
*/
waitUntil(
fn: (subject?: any) => boolean | Promise<boolean> | Chainable<boolean>,
options?: Partial<{
interval: number;
timeout: number;
}>
): Chainable<Subject>;

task(
name: 'indexFleetEndpointPolicy',
arg: {
Expand Down Expand Up @@ -124,6 +139,12 @@ declare global {
arg: HostActionResponse,
options?: Partial<Loggable & Timeoutable>
): Chainable<LogsEndpointActionResponse>;

task(
name: 'deleteAllEndpointData',
arg: { endpointAgentIds: string[] },
options?: Partial<Loggable & Timeoutable>
): Chainable<DeleteAllEndpointDataResponse>;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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 { deleteAllLoadedEndpointData } from '../../tasks/delete_all_endpoint_data';
import { getAlertsTableRows, navigateToAlertsList } from '../../screens/alerts';
import { waitForEndpointAlerts } from '../../tasks/alerts';
import { request } from '../../tasks/common';
import { getEndpointIntegrationVersion } from '../../tasks/fleet';
import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
import { enableAllPolicyProtections } from '../../tasks/endpoint_policy';
import type { PolicyData, ResponseActionApiResponse } from '../../../../../common/endpoint/types';
import type { CreateAndEnrollEndpointHostResponse } from '../../../../../scripts/endpoint/common/endpoint_host_services';
import { login } from '../../tasks/login';
import { EXECUTE_ROUTE } from '../../../../../common/endpoint/constants';
import { waitForActionToComplete } from '../../tasks/response_actions';

describe('Endpoint generated alerts', () => {
let indexedPolicy: IndexedFleetEndpointPolicyResponse;
let policy: PolicyData;
let createdHost: CreateAndEnrollEndpointHostResponse;

before(() => {
getEndpointIntegrationVersion().then((version) => {
const policyName = `alerts test ${Math.random().toString(36).substring(2, 7)}`;

cy.task<IndexedFleetEndpointPolicyResponse>('indexFleetEndpointPolicy', {
policyName,
endpointPackageVersion: version,
agentPolicyName: policyName,
}).then((data) => {
indexedPolicy = data;
policy = indexedPolicy.integrationPolicies[0];

return enableAllPolicyProtections(policy.id).then(() => {
// Create and enroll a new Endpoint host
return cy
.task(
'createEndpointHost',
{
agentPolicyId: policy.policy_id,
},
{ timeout: 180000 }
)
.then((host) => {
createdHost = host as CreateAndEnrollEndpointHostResponse;
});
});
});
});
});

after(() => {
if (createdHost) {
cy.task('destroyEndpointHost', createdHost).then(() => {});
}

if (indexedPolicy) {
cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy);
}

if (createdHost) {
deleteAllLoadedEndpointData({ endpointAgentIds: [createdHost.agentId] });
}
});

beforeEach(() => {
login();
});

it('should create a Detection Engine alert from an endpoint alert', () => {
// Triggers a Malicious Behaviour alert on Linux system (`grep *` was added only to identify this specific alert)
const executeMaliciousCommand = `bash -c cat /dev/tcp/foo | grep ${Math.random()
.toString(16)
.substring(2)}`;

// Send `execute` command that triggers malicious behaviour using the `execute` response action
request<ResponseActionApiResponse>({
method: 'POST',
url: EXECUTE_ROUTE,
body: {
endpoint_ids: [createdHost.agentId],
parameters: {
command: executeMaliciousCommand,
},
},
})
.then((response) => waitForActionToComplete(response.body.data.id))
.then(() => {
return waitForEndpointAlerts(createdHost.agentId, [
{
term: { 'process.group_leader.args': executeMaliciousCommand },
},
]);
})
.then(() => {
return navigateToAlertsList(
`query=(language:kuery,query:'agent.id: "${createdHost.agentId}" ')`
);
});

getAlertsTableRows().should('have.length.greaterThan', 0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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 { APP_ALERTS_PATH } from '../../../../common/constants';

export const navigateToAlertsList = (urlQueryParams: string = '') => {
cy.visit(`${APP_ALERTS_PATH}${urlQueryParams ? `?${urlQueryParams}` : ''}`);
};

export const clickAlertListRefreshButton = (): Cypress.Chainable => {
return cy.getByTestSubj('querySubmitButton').click().should('be.enabled');
};

/**
* Waits until the Alerts list has alerts data and return the number of rows that are currently displayed
* @param timeout
*/
export const getAlertsTableRows = (timeout?: number): Cypress.Chainable<JQuery<HTMLDivElement>> => {
let $rows: JQuery<HTMLDivElement> = Cypress.$();

return cy
.waitUntil(
() => {
clickAlertListRefreshButton();

return cy
.getByTestSubj('alertsTable')
.find<HTMLDivElement>('.euiDataGridRow')
.then(($rowsFound) => {
$rows = $rowsFound;
return Boolean($rows);
});
},
{ timeout }
)
.then(() => $rows);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { APP_PATH } from '../../../../common/constants';
import { getEndpointDetailsPath } from '../../common/routing';
import { getEndpointDetailsPath, getEndpointListPath } from '../../common/routing';

export const AGENT_HOSTNAME_CELL = 'hostnameCellLink';
export const AGENT_POLICY_CELL = 'policyNameCellLink';
Expand All @@ -21,3 +21,7 @@ export const navigateToEndpointPolicyResponse = (
getEndpointDetailsPath({ name: 'endpointPolicyResponse', selected_endpoint: endpointAgentId })
);
};

export const navigateToEndpointList = (): Cypress.Chainable<Cypress.AUTWindow> => {
return cy.visit(APP_PATH + getEndpointListPath({ name: 'endpointList' }));
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@

import type { CasePostRequest } from '@kbn/cases-plugin/common/api';
import { sendEndpointActionResponse } from '../../../../scripts/endpoint/agent_emulator/services/endpoint_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';
import type {
CreateAndEnrollEndpointHostOptions,
CreateAndEnrollEndpointHostResponse,
} from '../../../../scripts/endpoint/common/endpoint_host_services';
import type { IndexedEndpointPolicyResponse } from '../../../../common/endpoint/data_loaders/index_endpoint_policy_response';
import {
deleteIndexedEndpointPolicyResponse,
Expand Down Expand Up @@ -39,6 +46,10 @@ import {
deleteIndexedEndpointRuleAlerts,
indexEndpointRuleAlerts,
} from '../../../../common/endpoint/data_loaders/index_endpoint_rule_alerts';
import {
createAndEnrollEndpointHost,
destroyEndpointHost,
} from '../../../../scripts/endpoint/common/endpoint_host_services';

/**
* Cypress plugin for adding data loading related `task`s
Expand Down Expand Up @@ -155,5 +166,47 @@ export const dataLoaders = (
const { esClient } = await stackServicesPromise;
return sendEndpointActionResponse(esClient, data.action, { state: data.state.state });
},

deleteAllEndpointData: async ({
endpointAgentIds,
}: {
endpointAgentIds: string[];
}): Promise<DeleteAllEndpointDataResponse> => {
const { esClient } = await stackServicesPromise;
return deleteAllEndpointData(esClient, endpointAgentIds);
},
});
};

export const dataLoadersForRealEndpoints = (
on: Cypress.PluginEvents,
config: Cypress.PluginConfigOptions
): void => {
const stackServicesPromise = createRuntimeServices({
kibanaUrl: config.env.KIBANA_URL,
elasticsearchUrl: config.env.ELASTICSEARCH_URL,
username: config.env.ELASTICSEARCH_USERNAME,
password: config.env.ELASTICSEARCH_PASSWORD,
asSuperuser: true,
});

on('task', {
createEndpointHost: async (
options: Omit<CreateAndEnrollEndpointHostOptions, 'log' | 'kbnClient'>
): Promise<CreateAndEnrollEndpointHostResponse> => {
const { kbnClient, log } = await stackServicesPromise;
return createAndEnrollEndpointHost({ ...options, log, kbnClient }).then((newHost) => {
return waitForEndpointToStreamData(kbnClient, newHost.agentId, 120000).then(() => {
return newHost;
});
});
},

destroyEndpointHost: async (
createdHost: CreateAndEnrollEndpointHostResponse
): Promise<null> => {
const { kbnClient } = await stackServicesPromise;
return destroyEndpointHost(kbnClient, createdHost).then(() => null);
},
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,42 @@ Cypress.Commands.addQuery<'findByTestSubj'>(
}
);

Cypress.Commands.add(
'waitUntil',
{ prevSubject: 'optional' },
(subject, fn, { interval = 500, timeout = 30000 } = {}) => {
let attempts = Math.floor(timeout / interval);
Comment thread
ashokaditya marked this conversation as resolved.

const completeOrRetry = (result: boolean) => {
if (result) {
return result;
}
if (attempts < 1) {
throw new Error(`Timed out while retrying, last result was: {${result}}`);
}
cy.wait(interval, { log: false }).then(() => {
attempts--;
return evaluate();
});
};

const evaluate = () => {
const result = fn(subject);

if (typeof result === 'boolean') {
return completeOrRetry(result);
} else if ('then' in result) {
// @ts-expect-error
return result.then(completeOrRetry);
} else {
throw new Error(
`Unknown return type from callback: ${Object.prototype.toString.call(result)}`
);
}
};

return evaluate();
}
);

Cypress.on('uncaught:exception', () => false);
Loading