[Security Solution][Endpoint] Adds generic functional test for artifac list pages (#131532)

* Adds generic functional test for artifac list pages

* Unify page objects to be generic. Moved test data into a mock file

* Fix wrong blocklist js comments

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
David Sánchez 2022-05-12 17:28:31 +02:00 committed by GitHub
parent 179cf309ec
commit 0b162b6881
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 579 additions and 3 deletions

View file

@ -254,6 +254,7 @@ export const ArtifactListPage = memo<ArtifactListPageProps>(
</EuiButton>
)
}
data-test-subj={getTestId('container')}
>
{isFlyoutOpened && (
<ArtifactFlyout

View file

@ -167,8 +167,11 @@ export const BlockListForm = memo<ArtifactFormComponentProps>(
}, [item?.os_types]);
const selectedValues = useMemo(() => {
return blocklistEntry.value.map((label) => ({ label }));
}, [blocklistEntry.value]);
return blocklistEntry.value.map((label) => ({
label,
'data-test-subj': getTestId(`values-input-${label}`),
}));
}, [blocklistEntry.value, getTestId]);
const osOptions: Array<EuiSuperSelectOption<OperatingSystem>> = useMemo(
() =>
@ -186,17 +189,19 @@ export const BlockListForm = memo<ArtifactFormComponentProps>(
value: field,
inputDisplay: CONDITION_FIELD_TITLE[field],
dropdownDisplay: getDropdownDisplay(field),
'data-test-subj': getTestId(field),
}));
if (selectedOs === OperatingSystem.WINDOWS) {
selectableFields.push({
value: 'file.Ext.code_signature',
inputDisplay: CONDITION_FIELD_TITLE['file.Ext.code_signature'],
dropdownDisplay: getDropdownDisplay('file.Ext.code_signature'),
'data-test-subj': getTestId('file.Ext.code_signature'),
});
}
return selectableFields;
}, [selectedOs]);
}, [selectedOs, getTestId]);
const valueLabel = useMemo(() => {
return (

View file

@ -0,0 +1,191 @@
/*
* 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 { unzip } from 'zlib';
import { promisify } from 'util';
import expect from '@kbn/expect';
import { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data';
import { FtrProviderContext } from '../../ftr_provider_context';
import { ArtifactBodyType, ArtifactResponseType, getArtifactsListTestsData } from './mocks';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const pageObjects = getPageObjects(['common', 'artifactEntriesList']);
const testSubjects = getService('testSubjects');
const browser = getService('browser');
const endpointTestResources = getService('endpointTestResources');
const policyTestResources = getService('policyTestResources');
const retry = getService('retry');
const esClient = getService('es');
const unzipPromisify = promisify(unzip);
describe('For each artifact list under management', function () {
let indexedData: IndexedHostsAndAlertsResponse;
const checkFleetArtifacts = async (
type: string,
identifier: string,
expectedArtifact: ArtifactResponseType,
expectedDecodedBodyArtifact: ArtifactBodyType
) => {
// Check edited artifact is in the list with new values (wait for list to be updated)
let updatedArtifact: ArtifactResponseType | undefined;
await retry.waitForWithTimeout('fleet artifact is updated', 120_000, async () => {
const {
hits: { hits: windowsArtifactResults },
} = await esClient.search({
index: '.fleet-artifacts',
size: 1,
query: {
bool: {
filter: [
{
match: {
type,
},
},
{
match: {
identifier,
},
},
],
},
},
});
const windowsArtifact = windowsArtifactResults[0] as ArtifactResponseType;
const isUpdated = windowsArtifact._source.body === expectedArtifact._source.body;
if (isUpdated) updatedArtifact = windowsArtifact;
return isUpdated;
});
updatedArtifact!._source.created = expectedArtifact._source.created;
const bodyFormBuffer = Buffer.from(updatedArtifact!._source.body, 'base64');
const unzippedBody = await unzipPromisify(bodyFormBuffer);
// Check decoded body first to detect possible body changes
expect(JSON.parse(unzippedBody.toString())).eql(expectedDecodedBodyArtifact);
expect(updatedArtifact).eql(expectedArtifact);
};
for (const testData of getArtifactsListTestsData()) {
describe(`When on the ${testData.title} entries list`, function () {
before(async () => {
const endpointPackage = await policyTestResources.getEndpointPackage();
await endpointTestResources.setMetadataTransformFrequency('1s', endpointPackage.version);
indexedData = await endpointTestResources.loadEndpointData();
await browser.refresh();
await pageObjects.artifactEntriesList.navigateToList(testData.urlPath);
});
after(async () => {
await endpointTestResources.unloadEndpointData(indexedData);
});
it(`should not show page title if there is no ${testData.title} entry`, async () => {
await testSubjects.missingOrFail('header-page-title');
});
it(`should be able to add a new ${testData.title} entry`, async () => {
this.timeout(150_000);
// Opens add flyout
await testSubjects.click(`${testData.pagePrefix}-emptyState-addButton`);
for (const formAction of testData.create.formFields) {
if (formAction.type === 'click') {
await testSubjects.click(formAction.selector);
} else if (formAction.type === 'input') {
await testSubjects.setValue(formAction.selector, formAction.value || '');
}
}
// Submit create artifact form
await testSubjects.click(`${testData.pagePrefix}-flyout-submitButton`);
// Check new artifact is in the list
for (const checkResult of testData.create.checkResults) {
expect(await testSubjects.getVisibleText(checkResult.selector)).to.equal(
checkResult.value
);
}
await pageObjects.common.closeToast();
// Title is shown after adding an item
expect(await testSubjects.getVisibleText('header-page-title')).to.equal(testData.title);
// Checks if fleet artifact has been updated correctly
await checkFleetArtifacts(
testData.fleetArtifact.type,
testData.fleetArtifact.identifier,
testData.fleetArtifact.getExpectedUpdatedtArtifactWhenCreate(),
testData.fleetArtifact.getExpectedUpdatedArtifactBodyWhenCreate()
);
});
it(`should be able to update an existing ${testData.title} entry`, async () => {
this.timeout(150_000);
// Opens edit flyout
await pageObjects.artifactEntriesList.clickCardActionMenu(testData.pagePrefix);
await testSubjects.click(`${testData.pagePrefix}-card-cardEditAction`);
for (const formAction of testData.update.formFields) {
if (formAction.type === 'click') {
await testSubjects.click(formAction.selector);
} else if (formAction.type === 'input') {
await testSubjects.setValue(formAction.selector, formAction.value || '');
} else if (formAction.type === 'clear') {
await (
await (await testSubjects.find(formAction.selector)).findByCssSelector('button')
).click();
}
}
// Submit edit artifact form
await testSubjects.click(`${testData.pagePrefix}-flyout-submitButton`);
// Check edited artifact is in the list with new values (wait for list to be updated)
await retry.waitForWithTimeout('entry is updated in list', 10000, async () => {
const currentValue = await testSubjects.getVisibleText(
`${testData.pagePrefix}-card-criteriaConditions`
);
return currentValue === testData.update.waitForValue;
});
for (const checkResult of testData.update.checkResults) {
expect(await testSubjects.getVisibleText(checkResult.selector)).to.equal(
checkResult.value
);
}
await pageObjects.common.closeToast();
// Title still shown after editing an item
expect(await testSubjects.getVisibleText('header-page-title')).to.equal(testData.title);
// Checks if fleet artifact has been updated correctly
await checkFleetArtifacts(
testData.fleetArtifact.type,
testData.fleetArtifact.identifier,
testData.fleetArtifact.getExpectedUpdatedArtifactWhenUpdate(),
testData.fleetArtifact.getExpectedUpdatedArtifactBodyWhenUpdate()
);
});
it(`should be able to delete the existing ${testData.title} entry`, async () => {
// Remove it
await pageObjects.artifactEntriesList.clickCardActionMenu(testData.pagePrefix);
await testSubjects.click(`${testData.pagePrefix}-card-cardDeleteAction`);
await testSubjects.click(`${testData.pagePrefix}-deleteModal-submitButton`);
await testSubjects.waitForDeleted(testData.delete.confirmSelector);
// We only expect one artifact to have been visible
await testSubjects.missingOrFail(testData.delete.card);
// Header has gone because there is no artifact
await testSubjects.missingOrFail('header-page-title');
});
});
}
});
};

View file

@ -41,5 +41,6 @@ export default function (providerContext: FtrProviderContext) {
loadTestFile(require.resolve('./trusted_apps_list'));
loadTestFile(require.resolve('./fleet_integrations'));
loadTestFile(require.resolve('./endpoint_permissions'));
loadTestFile(require.resolve('./artifact_entries_list'));
});
}

View file

@ -0,0 +1,333 @@
/*
* 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 { ArtifactElasticsearchProperties } from '@kbn/fleet-plugin/server/services/artifacts/types';
import { TranslatedExceptionListItem } from '@kbn/security-solution-plugin/server/endpoint/schemas/artifacts/lists';
export interface ArtifactResponseType {
_index: string;
_id: string;
_score: number;
_source: ArtifactElasticsearchProperties;
}
export interface ArtifactBodyType {
entries: TranslatedExceptionListItem[];
}
export const getArtifactsListTestsData = () => [
{
title: 'Blocklist',
pagePrefix: 'blocklistPage',
create: {
formFields: [
{
type: 'input',
selector: 'blocklist-form-name-input',
value: 'Blocklist name',
},
{
type: 'input',
selector: 'blocklist-form-description-input',
value: 'This is the blocklist description',
},
{
type: 'click',
selector: 'blocklist-form-field-select',
},
{
type: 'click',
selector: 'blocklist-form-file.hash.*',
},
{
type: 'input',
selector: 'blocklist-form-values-input',
value: 'A4370C0CF81686C0B696FA6261c9d3e0d810ae704ab8301839dffd5d5112f476',
},
],
checkResults: [
{
selector: 'blocklistPage-card-criteriaConditions',
value:
'OSIS Windows\nAND file.hash.*IS ONE OF\nA4370C0CF81686C0B696FA6261c9d3e0d810ae704ab8301839dffd5d5112f476',
},
],
},
update: {
formFields: [
{
type: 'input',
selector: 'blocklist-form-name-input',
value: 'Blocklist name edited',
},
{
type: 'input',
selector: 'blocklist-form-description-input',
value: 'This is the blocklist description edited',
},
{
type: 'click',
selector: 'blocklist-form-field-select',
},
{
type: 'click',
selector: 'blocklist-form-file.path',
},
{
type: 'clear',
selector:
'blocklist-form-values-input-A4370C0CF81686C0B696FA6261c9d3e0d810ae704ab8301839dffd5d5112f476',
},
{
type: 'input',
selector: 'blocklist-form-values-input',
value: 'c:\\randomFolder\\randomFile.exe, c:\\randomFolder\\randomFile2.exe',
},
],
checkResults: [
{
selector: 'blocklistPage-card-criteriaConditions',
value:
'OSIS Windows\nAND file.pathIS ONE OF\nc:\\randomFolder\\randomFile.exe\nc:\\randomFolder\\randomFile2.exe',
},
{
selector: 'blocklistPage-card-header-title',
value: 'Blocklist name edited',
},
{
selector: 'blocklistPage-card-description',
value: 'This is the blocklist description edited',
},
],
waitForValue:
'OSIS Windows\nAND file.pathIS ONE OF\nc:\\randomFolder\\randomFile.exe\nc:\\randomFolder\\randomFile2.exe',
},
delete: {
confirmSelector: 'blocklistDeletionConfirm',
card: 'blocklistCard',
},
pageObject: 'blocklist',
urlPath: 'blocklist',
fleetArtifact: {
identifier: 'endpoint-blocklist-windows-v1',
type: 'blocklist',
getExpectedUpdatedtArtifactWhenCreate: (): ArtifactResponseType => ({
_index: '.fleet-artifacts-7',
_id: 'endpoint:endpoint-blocklist-windows-v1-d2b12779ee542a6c4742d505cd0c684b0f55436a97074c62e7de7155344c74bc',
_score: 0,
_source: {
type: 'blocklist',
identifier: 'endpoint-blocklist-windows-v1',
relative_url:
'/api/fleet/artifacts/endpoint-blocklist-windows-v1/d2b12779ee542a6c4742d505cd0c684b0f55436a97074c62e7de7155344c74bc',
body: 'eJxVzEEKgzAUBNC7ZF0kMRqjOyt4CSnym/+DgVTFxFKR3r0pdFNmN2+Yk9EcN0eBNcPJ4rESa1hwj9UTu/yZdeQxoXWesgnClIUJ8lKl2bLSBnHZkrrZ+B0JU/s7oxeYOBoIhCPMR4In+D3JwNpCVrzjXa+F0qrjV1WrvlW5EqZGSRy14EAVL+CuJRda1mgtllgKkduiUuz2/uYDrE49EA==',
encryption_algorithm: 'none',
package_name: 'endpoint',
encoded_size: 160,
encoded_sha256: '8620957e33599029c5f96fa689e0df2206960f582130ccdea64f22403fc05e50',
decoded_size: 196,
decoded_sha256: 'd2b12779ee542a6c4742d505cd0c684b0f55436a97074c62e7de7155344c74bc',
compression_algorithm: 'zlib',
created: '2000-01-01T00:00:00.000Z',
},
}),
getExpectedUpdatedArtifactBodyWhenCreate: (): ArtifactBodyType => ({
entries: [
{
type: 'simple',
entries: [
{
field: 'file.hash.sha256',
operator: 'included',
type: 'exact_cased_any',
value: ['A4370C0CF81686C0B696FA6261c9d3e0d810ae704ab8301839dffd5d5112f476'],
},
],
},
],
}),
getExpectedUpdatedArtifactWhenUpdate: (): ArtifactResponseType => ({
_index: '.fleet-artifacts-7',
_id: 'endpoint:endpoint-blocklist-windows-v1-2df413b3c01b54be7e9106e92c39297ca72d32bcd626c3f7eb7d395db8e905fe',
_score: 0,
_source: {
type: 'blocklist',
identifier: 'endpoint-blocklist-windows-v1',
relative_url:
'/api/fleet/artifacts/endpoint-blocklist-windows-v1/2df413b3c01b54be7e9106e92c39297ca72d32bcd626c3f7eb7d395db8e905fe',
body: 'eJx9jcEKwjAQRH9F9iwePOYD/IlWypKdYmCbhCSVltJ/dysieJE5zbxhZiPEVgIquW6jtmaQoxqmrKDzDxsDVAyOQXHJ3B7GU0bhlorFIXqdBWLpZwUL+zZ4rpCB42rgyTob6ci7vi8cJU23pILydcc2luP69K9zfZfu+6EXorpEbA==',
encryption_algorithm: 'none',
package_name: 'endpoint',
encoded_size: 130,
encoded_sha256: '3fb42b56c16ef38f8ecb62c082a7f3dddf4a52998a83c97d16688e854e15a502',
decoded_size: 194,
decoded_sha256: '2df413b3c01b54be7e9106e92c39297ca72d32bcd626c3f7eb7d395db8e905fe',
compression_algorithm: 'zlib',
created: '2000-01-01T00:00:00.000Z',
},
}),
getExpectedUpdatedArtifactBodyWhenUpdate: (): ArtifactBodyType => ({
entries: [
{
type: 'simple',
entries: [
{
field: 'file.path',
operator: 'included',
type: 'exact_cased_any',
value: ['c:\\randomFolder\\randomFile.exe', ' c:\\randomFolder\\randomFile2.exe'],
},
],
},
],
}),
},
},
{
title: 'Host isolation exceptions',
pagePrefix: 'hostIsolationExceptionsListPage',
create: {
formFields: [
{
type: 'input',
selector: 'hostIsolationExceptions-form-name-input',
value: 'Host Isolation exception name',
},
{
type: 'input',
selector: 'hostIsolationExceptions-form-description-input',
value: 'This is the host isolation exception description',
},
{
type: 'input',
selector: 'hostIsolationExceptions-form-ip-input',
value: '1.1.1.1',
},
],
checkResults: [
{
selector: 'hostIsolationExceptionsListPage-card-criteriaConditions',
value: 'OSIS Windows, Linux, Mac\nAND destination.ipIS 1.1.1.1',
},
],
},
update: {
formFields: [
{
type: 'input',
selector: 'hostIsolationExceptions-form-name-input',
value: 'Host Isolation exception name edited',
},
{
type: 'input',
selector: 'hostIsolationExceptions-form-description-input',
value: 'This is the host isolation exception description edited',
},
{
type: 'input',
selector: 'hostIsolationExceptions-form-ip-input',
value: '2.2.2.2/24',
},
],
checkResults: [
{
selector: 'hostIsolationExceptionsListPage-card-criteriaConditions',
value: 'OSIS Windows, Linux, Mac\nAND destination.ipIS 2.2.2.2/24',
},
{
selector: 'hostIsolationExceptionsListPage-card-header-title',
value: 'Host Isolation exception name edited',
},
{
selector: 'hostIsolationExceptionsListPage-card-description',
value: 'This is the host isolation exception description edited',
},
],
waitForValue: 'OSIS Windows, Linux, Mac\nAND destination.ipIS 2.2.2.2/24',
},
delete: {
confirmSelector: 'hostIsolationExceptionsDeletionConfirm',
card: 'hostIsolationExceptionsCard',
},
pageObject: 'hostIsolationExceptions',
urlPath: 'host_isolation_exceptions',
fleetArtifact: {
identifier: 'endpoint-hostisolationexceptionlist-windows-v1',
type: 'hostisolationexceptionlist',
getExpectedUpdatedtArtifactWhenCreate: (): ArtifactResponseType => ({
_index: '.fleet-artifacts-7',
_id: 'endpoint:endpoint-hostisolationexceptionlist-windows-v1-2c3ee2b5e7f86f8c336a3df7e59a1151b11d7eec382442032e69712d6a6459e0',
_score: 0,
_source: {
type: 'hostisolationexceptionlist',
identifier: 'endpoint-hostisolationexceptionlist-windows-v1',
relative_url:
'/api/fleet/artifacts/endpoint-hostisolationexceptionlist-windows-v1/2c3ee2b5e7f86f8c336a3df7e59a1151b11d7eec382442032e69712d6a6459e0',
body: 'eJxVjEEKgDAMBP+Sswhe/YqIhHaFQG1LG0UR/24ULzK3mWVPQtQiqNQPJ+mRQT1VWXIANb82C4K36FFVIquk2Eq2UcoorKlYk+jC6uHNflfY2enkuL5y47A+tmtf6BqNG647LBE=',
encryption_algorithm: 'none',
package_name: 'endpoint',
encoded_size: 101,
encoded_sha256: 'ee949ea39fe547e06add448956fa7d94ea14d1c30a368dce7058a1cb6ac278f9',
decoded_size: 131,
decoded_sha256: '2c3ee2b5e7f86f8c336a3df7e59a1151b11d7eec382442032e69712d6a6459e0',
compression_algorithm: 'zlib',
created: '2000-01-01T00:00:00.000Z',
},
}),
getExpectedUpdatedArtifactBodyWhenCreate: (): ArtifactBodyType => ({
entries: [
{
type: 'simple',
entries: [
{
field: 'destination.ip',
operator: 'included',
type: 'exact_cased',
value: '1.1.1.1',
},
],
},
],
}),
getExpectedUpdatedArtifactWhenUpdate: (): ArtifactResponseType => ({
_index: '.fleet-artifacts-7',
_id: 'endpoint:endpoint-hostisolationexceptionlist-windows-v1-4b62473b4cf057277b3297896771cc1061c3bea2c4f7ec1ef5c2546f33d5d9e8',
_score: 0,
_source: {
type: 'hostisolationexceptionlist',
identifier: 'endpoint-hostisolationexceptionlist-windows-v1',
relative_url:
'/api/fleet/artifacts/endpoint-hostisolationexceptionlist-windows-v1/4b62473b4cf057277b3297896771cc1061c3bea2c4f7ec1ef5c2546f33d5d9e8',
body: 'eJxVjEEKgzAQRe8ya4kgXeUqIjIkvzAQk5CMYpHevVPpprzde59/EbI2QSc/X6SvCvLUZasJNPy1pyBFixFdJbNKyU6qjUpFYy3NmuSQ9oho9neFk4OugfstD077107uZpwe9F6MDzBbLKo=',
encryption_algorithm: 'none',
package_name: 'endpoint',
encoded_size: 107,
encoded_sha256: 'dbcc8f50044d43453fbffb4edda6aa0cd42075621827986393d625404f2b6b81',
decoded_size: 134,
decoded_sha256: '4b62473b4cf057277b3297896771cc1061c3bea2c4f7ec1ef5c2546f33d5d9e8',
compression_algorithm: 'zlib',
created: '2000-01-01T00:00:00.000Z',
},
}),
getExpectedUpdatedArtifactBodyWhenUpdate: (): ArtifactBodyType => ({
entries: [
{
type: 'simple',
entries: [
{
field: 'destination.ip',
operator: 'included',
type: 'exact_cased',
value: '2.2.2.2/24',
},
],
},
],
}),
},
},
];

View file

@ -46,6 +46,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
// always install Endpoint package by default when Fleet sets up
`--xpack.fleet.packages.0.name=endpoint`,
`--xpack.fleet.packages.0.version=latest`,
// set the packagerTaskInterval to 5s in order to speed up test executions when checking fleet artifacts
'--xpack.securitySolution.packagerTaskInterval=5s',
],
},
layout: {

View file

@ -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 { FtrProviderContext } from '../ftr_provider_context';
export function ArtifactEntriesListPageProvider({
getService,
getPageObjects,
}: FtrProviderContext) {
const pageObjects = getPageObjects(['common', 'header', 'endpointPageUtils']);
const testSubjects = getService('testSubjects');
return {
async navigateToList(artifactType: string, searchParams?: string) {
await pageObjects.common.navigateToUrlWithBrowserHistory(
'securitySolutionManagement',
`/${artifactType}${searchParams ? `?${searchParams}` : ''}`
);
await pageObjects.header.waitUntilLoadingHasFinished();
},
// /**
// * ensures that the ArtifactType page is the currently display view
// */
async ensureIsOnArtifactTypePage(artifactTypePage: string) {
await testSubjects.existOrFail(`${artifactTypePage}-container`);
},
// /**
// * Clicks on the actions menu icon in the (only one) ArtifactType card to show the popup with list of actions
// */
async clickCardActionMenu(artifactTypePage: string) {
await testSubjects.existOrFail(`${artifactTypePage}-container`);
await testSubjects.click(`${artifactTypePage}-card-header-actions-button`);
},
};
}

View file

@ -14,12 +14,14 @@ import { IngestManagerCreatePackagePolicy } from './ingest_manager_create_packag
import { FleetIntegrations } from './fleet_integrations_page';
import { DetectionsPageObject } from '../../security_solution_ftr/page_objects/detections';
import { HostsPageObject } from '../../security_solution_ftr/page_objects/hosts';
import { ArtifactEntriesListPageProvider } from './artifact_entries_list_page';
export const pageObjects = {
...xpackFunctionalPageObjects,
endpoint: EndpointPageProvider,
policy: EndpointPolicyPageProvider,
trustedApps: TrustedAppsPageProvider,
artifactEntriesList: ArtifactEntriesListPageProvider,
endpointPageUtils: EndpointPageUtils,
ingestManagerCreatePackagePolicy: IngestManagerCreatePackagePolicy,
fleetIntegrations: FleetIntegrations,