[Security Solution][Serverless][Endpoint] Defines set of available kibana privileges for Endpoint Essentials/Complete add-on (#162281)

## Summary

PR defines the set of available app features for serverless Product Line
Items (PLIs) for Endpoint addon. Changes include:

- Adds new Security Solution Serverless config file group:
`xpack.securitySolutionServerless.developer.*`. Used to facilitate
development. Values can be set via the
`config/serverless.security.dev.yml`
- Includes `disableManagementUrlRedirect` config option, which when set
to `true` will disable the redirect currently in place when a user
attempts to access the kibana Management pages.
- Defines the set of Kibana Privileges that goes along with Endpoint
Essentials and Endpoint Complete addons for serverless
- Includes cypress e2e tests for validating Endpoint Management related
access based on Product Tier (see below for list of test per role/per
product tier)

**Changes to e2e test framework:**

- Cypress `parallel` runner now normalizes the set of ENV variable
passed into each of the cypress runs
- Added support to Cypress for defining `productTier` via a Cypress test
file (`*.cy.ts`) top-level `describe(description, config, testFn)`
block. Will be applied when the stack is running in `serverless` mode.
- NOTE: if opening Cypress locally using `cypress:open`, you likely will
have to change the setup (only locally - don't commit) to only pickup
your 1 test file because the current implementation of Cypress
`parallel` only reads the first test file
- Serverless Security folder structure was altered with the following:
- `ftr` folder was created and all existing FTR tests moved under it (we
already had a `cypress` folder, thus those are clearly separated)
- a new folder was was created here `test_serverless/shared/lib`.
Contains code that should be test framework independent (aka: can be
used from both FTR and Cypress).
- It currently has the security solution role/user loader logic, thus it
can be used by both FTR (ex. API integration) and Cypress


### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Paul Tavares 2023-08-01 13:20:46 -04:00 committed by GitHub
parent e2db0b0e66
commit 733e19c5c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 3151 additions and 256 deletions

4
.github/CODEOWNERS vendored
View file

@ -1238,6 +1238,10 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib
/x-pack/plugins/security_solution/scripts/endpoint/trusted_apps/ @elastic/security-defend-workflows
/x-pack/test/security_solution_endpoint/ @elastic/security-defend-workflows
/x-pack/test/security_solution_endpoint_api_int/ @elastic/security-defend-workflows
/x-pack/test_serverless/shared/lib/security/kibana_roles/ @elastic/security-defend-workflows
/x-pack/test_serverless/functional/test_suites/security/cypress/e2e/endpoint_management @elastic/security-defend-workflows
/x-pack/test_serverless/functional/test_suites/security/cypress/screens/endpoint_management @elastic/security-defend-workflows
/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/endpoint_management @elastic/security-defend-workflows
## Security Solution sub teams - security-telemetry (Data Engineering)
x-pack/plugins/security_solution/server/usage/ @elastic/security-data-analytics

View file

@ -10,14 +10,29 @@ export enum AppFeatureSecurityKey {
* Enables Advanced Insights (Entity Risk, GenAI)
*/
advancedInsights = 'advanced_insights',
/**
* Enables Endpoint Response Actions like isolate host, trusted apps, blocklist, etc.
* Enables access to the Endpoint List and associated views that allows management of hosts
* running endpoint security
*/
endpointHostManagement = 'endpoint_host_management',
/**
* Enables endpoint policy views that enables user to manage endpoint security policies
*/
endpointPolicyManagement = 'endpoint_policy_management',
/**
* Enables management of all endpoint related artifacts (ex. Trusted Applications, Event Filters,
* Host Isolation Exceptions, Blocklist.
*/
endpointArtifactManagement = 'endpoint_artifact_management',
/**
* Enables all of endpoint's supported response actions - like host isolation, file operations,
* process operations, command execution, etc.
*/
endpointResponseActions = 'endpoint_response_actions',
/**
* Enables Endpoint Exceptions like isolate host, trusted apps, blocklist, etc.
*/
endpointExceptions = 'endpoint_exceptions',
/**
* Enables Threat Intelligence

View file

@ -32,7 +32,9 @@ export default defineCypressConfig({
ELASTICSEARCH_URL: 'http://localhost:9200',
FLEET_SERVER_URL: 'https://localhost:8220',
// Username/password used for both elastic and kibana
ELASTICSEARCH_USERNAME: 'elastic',
KIBANA_USERNAME: 'elastic',
KIBANA_PASSWORD: 'changeme',
ELASTICSEARCH_USERNAME: 'system_indices_superuser',
ELASTICSEARCH_PASSWORD: 'changeme',
},

View file

@ -41,6 +41,8 @@ changes those defaults and target a run against different instances of the stack
```
CYPRESS_KIBANA_URL
CYPRESS_KIBANA_USERNAME
CYPRESS_KIBANA_PASSWORD
CYPRESS_ELASTICSEARCH_URL
CYPRESS_ELASTICSEARCH_USERNAME
CYPRESS_ELASTICSEARCH_PASSWORD
@ -49,8 +51,8 @@ CYPRESS_BASE_URL
Some notes:
- The `ELASTICSEARCH_USERNAME` and `ELASTICSEARCH_PASSWORD` will be used for both Elasticsearch and Kibana access.
- Both URL variables should not include credentials in the url
- The `ELASTICSEARCH_USERNAME` and `ELASTICSEARCH_PASSWORD` should have sufficient privileges to CRUD on restricted indices.
- Both URL variables should **NOT** include credentials in the url
- `KIBANA_URL` and `BASE_URL` will almost always be the same
Example:
@ -59,7 +61,9 @@ Example:
yarn --cwd x-pack/plugins/security_solution
CYPRESS_BASE_URL=http://localhost:5601 \
CYPRESS_KIBANA_URL=http://localhost:5601 \
CYPRESS_ELASTICSEARCH_USERNAME=elastic \
CYPRESS_KIBANA_USERNAME=elastic \
CYPRESS_KIBANA_PASSWORD=changeme \
CYPRESS_ELASTICSEARCH_USERNAME=system_indices_superuser \
CYPRESS_ELASTICSEARCH_PASSWORD=changeme \
CYPRESS_ELASTICSEARCH_URL=http://localhost:9200 cypress:dw:open
```

View file

@ -10,6 +10,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { CasePostRequest } from '@kbn/cases-plugin/common/api';
import type { SecuritySolutionDescribeBlockFtrConfig } from '../../../scripts/run_cypress/utils';
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 {
@ -34,10 +35,25 @@ import type {
declare global {
namespace Cypress {
interface SuiteConfigOverrides {
env: {
ftrConfig: SecuritySolutionDescribeBlockFtrConfig;
};
}
interface Chainable<Subject = any> {
/**
* Get Elements by `data-test-subj`
* Get Elements by `data-test-subj`. Note that his is a parent query and can only be used
* from `cy`
*
* @param args
*
* @example
* // Correct:
* cy.getByTestSubj('some-subject);
*
* // Incorrect:
* cy.get('someElement').getByTestSubj('some-subject);
*/
getByTestSubj<E extends Node = HTMLElement>(
...args: Parameters<Cypress.Chainable<E>['get']>

View file

@ -25,10 +25,8 @@ export const closeResponder = (): void => {
export const openResponderActionLogFlyout = (): void => {
ensureOnResponder();
cy.getByTestSubj('responderShowActionLogButton')
.click()
.getByTestSubj(TEST_SUBJ.actionLogFlyout)
.should('exist');
cy.getByTestSubj('responderShowActionLogButton').click();
cy.getByTestSubj(TEST_SUBJ.actionLogFlyout).should('exist');
};
export const closeResponderActionLogFlyout = (): void => {

View file

@ -74,8 +74,10 @@ export const dataLoaders = (
kibanaUrl: config.env.KIBANA_URL,
elasticsearchUrl: config.env.ELASTICSEARCH_URL,
fleetServerUrl: config.env.FLEET_SERVER_URL,
username: config.env.ELASTICSEARCH_USERNAME,
password: config.env.ELASTICSEARCH_PASSWORD,
username: config.env.KIBANA_USERNAME,
password: config.env.KIBANA_PASSWORD,
esUsername: config.env.ELASTICSEARCH_USERNAME,
esPassword: config.env.ELASTICSEARCH_PASSWORD,
asSuperuser: true,
});
@ -212,8 +214,10 @@ export const dataLoadersForRealEndpoints = (
kibanaUrl: config.env.KIBANA_URL,
elasticsearchUrl: config.env.ELASTICSEARCH_URL,
fleetServerUrl: config.env.FLEET_SERVER_URL,
username: config.env.ELASTICSEARCH_USERNAME,
password: config.env.ELASTICSEARCH_PASSWORD,
username: config.env.KIBANA_USERNAME,
password: config.env.KIBANA_PASSWORD,
esUsername: config.env.ELASTICSEARCH_USERNAME,
esPassword: config.env.ELASTICSEARCH_PASSWORD,
asSuperuser: true,
});
@ -222,8 +226,8 @@ export const dataLoadersForRealEndpoints = (
kibanaUrl: config.env.KIBANA_URL,
elasticUrl: config.env.ELASTICSEARCH_URL,
fleetServerUrl: config.env.FLEET_SERVER_URL,
username: config.env.ELASTICSEARCH_USERNAME,
password: config.env.ELASTICSEARCH_PASSWORD,
username: config.env.KIBANA_USERNAME,
password: config.env.KIBANA_PASSWORD,
asSuperuser: true,
});
const data = await runFleetServerIfNeeded();

View file

@ -36,7 +36,16 @@ Cypress.Commands.addQuery<'getByTestSubj'>(
subject: Cypress.Chainable<JQuery<HTMLElement>>
) => Cypress.Chainable<JQuery<HTMLElement>>;
return (subject) => getFn(subject);
return (subject) => {
if (subject) {
const errMessage =
'`cy.getByTestSubj()` is a parent query and can not be chained off a existing subject. Did you mean to use `.findByTestSubj()`?';
cy.now('log', errMessage, [selector, subject]);
throw new TypeError(errMessage);
}
return getFn(subject);
};
}
);
@ -45,7 +54,7 @@ Cypress.Commands.addQuery<'findByTestSubj'>(
function findByTestSubj(selector, options) {
return (subject) => {
Cypress.ensure.isElement(subject, this.get('name'), cy);
return subject.find(testSubjSelector(selector), {});
return subject.find(testSubjSelector(selector), options);
};
}
);

View file

@ -24,8 +24,8 @@ export const responseActionTasks = (
kibanaUrl: config.env.KIBANA_URL,
elasticsearchUrl: config.env.ELASTICSEARCH_URL,
fleetServerUrl: config.env.FLEET_SERVER_URL,
username: config.env.ELASTICSEARCH_USERNAME,
password: config.env.ELASTICSEARCH_PASSWORD,
username: config.env.KIBANA_USERNAME,
password: config.env.KIBANA_PASSWORD,
asSuperuser: true,
});

View file

@ -6,11 +6,11 @@
*/
export const API_AUTH = Object.freeze({
user: Cypress.env('ELASTICSEARCH_USERNAME'),
pass: Cypress.env('ELASTICSEARCH_PASSWORD'),
user: Cypress.env('KIBANA_USERNAME') ?? Cypress.env('ELASTICSEARCH_USERNAME'),
pass: Cypress.env('KIBANA_PASSWORD') ?? Cypress.env('ELASTICSEARCH_PASSWORD'),
});
export const COMMON_API_HEADERS = { 'kbn-xsrf': 'cypress' };
export const COMMON_API_HEADERS = Object.freeze({ 'kbn-xsrf': 'cypress' });
export const waitForPageToBeLoaded = () => {
cy.getByTestSubj('globalLoadingIndicator-hidden').should('exist');
@ -28,6 +28,6 @@ export const request = <T = unknown>({
}: Partial<Cypress.RequestOptions>): Cypress.Chainable<Cypress.Response<T>> =>
cy.request<T>({
auth: API_AUTH,
headers: Object.freeze({ ...COMMON_API_HEADERS, ...headers }),
headers: { ...COMMON_API_HEADERS, ...headers },
...options,
});

View file

@ -11,9 +11,10 @@ import * as yaml from 'js-yaml';
import type { UrlObject } from 'url';
import Url from 'url';
import type { Role } from '@kbn/security-plugin/common';
import { isLocalhost } from '../../../../scripts/endpoint/common/is_localhost';
import { getWithResponseActionsRole } from '../../../../scripts/endpoint/common/roles_users/with_response_actions_role';
import { getNoResponseActionsRole } from '../../../../scripts/endpoint/common/roles_users/without_response_actions_role';
import { request, loadPage } from './common';
import { request } from './common';
import { getT1Analyst } from '../../../../scripts/endpoint/common/roles_users/t1_analyst';
import { getT2Analyst } from '../../../../scripts/endpoint/common/roles_users/t2_analyst';
import { getHunter } from '../../../../scripts/endpoint/common/roles_users/hunter';
@ -81,6 +82,9 @@ const ELASTICSEARCH_USERNAME = 'ELASTICSEARCH_USERNAME';
*/
const ELASTICSEARCH_PASSWORD = 'ELASTICSEARCH_PASSWORD';
const KIBANA_USERNAME = 'KIBANA_USERNAME';
const KIBANA_PASSWORD = 'KIBANA_PASSWORD';
/**
* The Kibana server endpoint used for authentication
*/
@ -110,46 +114,6 @@ export const getUrlWithRoute = (role: string, route: string) => {
return theUrl;
};
interface User {
username: string;
password: string;
}
/**
* Builds a URL with basic auth using the passed in user.
*
* @param user the user information to build the basic auth with
* @param route string route to visit
*/
export const constructUrlWithUser = (user: User, route: string) => {
const url = Cypress.config().baseUrl;
const kibana = new URL(String(url));
const hostname = kibana.hostname;
const username = user.username;
const password = user.password;
const protocol = kibana.protocol.replace(':', '');
const port = kibana.port;
const path = `${route.startsWith('/') ? '' : '/'}${route}`;
const strUrl = `${protocol}://${username}:${password}@${hostname}:${port}${path}`;
const builtUrl = new URL(strUrl);
cy.log(`origin: ${builtUrl.href}`);
return builtUrl.href;
};
export const getCurlScriptEnvVars = () => ({
ELASTICSEARCH_URL: Cypress.env('ELASTICSEARCH_URL'),
ELASTICSEARCH_USERNAME: Cypress.env('ELASTICSEARCH_USERNAME'),
ELASTICSEARCH_PASSWORD: Cypress.env('ELASTICSEARCH_PASSWORD'),
KIBANA_URL: Cypress.config().baseUrl,
});
export const createRoleAndUser = (role: ROLE) => {
createCustomRoleAndUser(role, rolesMapping[role]);
};
export const createCustomRoleAndUser = (role: string, rolePrivileges: Omit<Role, 'name'>) => {
// post the role
request({
@ -170,36 +134,6 @@ export const createCustomRoleAndUser = (role: string, rolePrivileges: Omit<Role,
});
};
export const deleteRoleAndUser = (role: ROLE) => {
request({
method: 'DELETE',
url: `/internal/security/users/${role}`,
});
request({
method: 'DELETE',
url: `/api/security/role/${role}`,
});
};
export const loginWithUser = (user: User) => {
const url = Cypress.config().baseUrl;
request({
body: {
providerType: 'basic',
providerName: url && !url.includes('localhost') ? 'cloud-basic' : 'basic',
currentURL: '/',
params: {
username: user.username,
password: user.password,
},
},
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
method: 'POST',
url: constructUrlWithUser(user, LOGIN_API_ENDPOINT),
});
};
export const loginWithRole = async (role: ROLE) => {
loginWithCustomRole(role, rolesMapping[role]);
};
@ -250,7 +184,8 @@ export const login = (role?: ROLE) => {
* via environment variables
*/
const credentialsProvidedByEnvironment = (): boolean =>
Cypress.env(ELASTICSEARCH_USERNAME) != null && Cypress.env(ELASTICSEARCH_PASSWORD) != null;
(Cypress.env(KIBANA_USERNAME) != null && Cypress.env(KIBANA_PASSWORD) != null) ||
(Cypress.env(ELASTICSEARCH_USERNAME) != null && Cypress.env(ELASTICSEARCH_PASSWORD) != null);
/**
* Authenticates with Kibana by reading credentials from the
@ -261,24 +196,47 @@ const credentialsProvidedByEnvironment = (): boolean =>
const loginViaEnvironmentCredentials = () => {
const url = Cypress.config().baseUrl;
if (!url) {
throw Error(`Cypress config baseUrl not set!`);
}
const urlObj = new URL(url);
let username: string;
let password: string;
let usernameEnvVar: string;
let passwordEnvVar: string;
if (Cypress.env(KIBANA_USERNAME) && Cypress.env(KIBANA_PASSWORD)) {
username = Cypress.env(KIBANA_USERNAME);
password = Cypress.env(KIBANA_PASSWORD);
usernameEnvVar = KIBANA_USERNAME;
passwordEnvVar = KIBANA_PASSWORD;
} else {
username = Cypress.env(ELASTICSEARCH_USERNAME);
password = Cypress.env(ELASTICSEARCH_PASSWORD);
usernameEnvVar = ELASTICSEARCH_USERNAME;
passwordEnvVar = ELASTICSEARCH_PASSWORD;
}
cy.log(
`Authenticating via environment credentials from the \`CYPRESS_${ELASTICSEARCH_USERNAME}\` and \`CYPRESS_${ELASTICSEARCH_PASSWORD}\` environment variables`
`Authenticating user [${username}] retrieved via environment credentials from the \`CYPRESS_${usernameEnvVar}\` and \`CYPRESS_${passwordEnvVar}\` environment variables`
);
// programmatically authenticate without interacting with the Kibana login page
request({
body: {
providerType: 'basic',
providerName: url && !url.includes('localhost') ? 'cloud-basic' : 'basic',
providerName: url && !isLocalhost(urlObj.hostname) ? 'cloud-basic' : 'basic',
currentURL: '/',
params: {
username: Cypress.env(ELASTICSEARCH_USERNAME),
password: Cypress.env(ELASTICSEARCH_PASSWORD),
username,
password,
},
},
headers: { 'kbn-xsrf': 'cypress-creds-via-env' },
method: 'POST',
url: `${Cypress.config().baseUrl}${LOGIN_API_ENDPOINT}`,
url: `${url}${LOGIN_API_ENDPOINT}`,
});
};
@ -314,37 +272,6 @@ const loginViaConfig = () => {
});
};
/**
* Get the configured auth details that were used to spawn cypress
*
* @returns the default Elasticsearch username and password for this environment
*/
export const getEnvAuth = (): User => {
if (credentialsProvidedByEnvironment()) {
return {
username: Cypress.env(ELASTICSEARCH_USERNAME),
password: Cypress.env(ELASTICSEARCH_PASSWORD),
};
} else {
let user: User = { username: '', password: '' };
cy.readFile(KIBANA_DEV_YML_PATH).then((devYml) => {
const config = yaml.safeLoad(devYml);
user = { username: config.elasticsearch.username, password: config.elasticsearch.password };
});
return user;
}
};
/**
* Authenticates with Kibana, visits the specified `url`, and waits for the
* Kibana global nav to be displayed before continuing
*/
export const loginAndWaitForPage = (url: string) => {
login();
loadPage(url);
};
export const getRoleWithArtifactReadPrivilege = (privilegePrefix: string) => {
const endpointSecurityPolicyManagerRole = getEndpointSecurityPolicyManager();

View file

@ -0,0 +1,18 @@
/*
* 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.
*/
const POSSIBLE_LOCALHOST_VALUES: readonly string[] = [
'localhost',
'127.0.0.1',
'0.0.0.0',
'::1',
'0000:0000:0000:0000:0000:0000:0000:0000',
];
export const isLocalhost = (hostname: string): boolean => {
return POSSIBLE_LOCALHOST_VALUES.includes(hostname.toLowerCase());
};

View file

@ -7,14 +7,6 @@
import { networkInterfaces } from 'node:os';
const POSSIBLE_LOCALHOST_VALUES: readonly string[] = [
'localhost',
'127.0.0.1',
'0.0.0.0',
'::1',
'0000:0000:0000:0000:0000:0000:0000:0000',
];
export const getLocalhostRealIp = (): string => {
for (const netInterfaceList of Object.values(networkInterfaces())) {
if (netInterfaceList) {
@ -31,7 +23,3 @@ export const getLocalhostRealIp = (): string => {
}
return '0.0.0.0';
};
export const isLocalhost = (hostname: string): boolean => {
return POSSIBLE_LOCALHOST_VALUES.includes(hostname.toLowerCase());
};

View file

@ -9,7 +9,10 @@ import { Client } from '@elastic/elasticsearch';
import { ToolingLog } from '@kbn/tooling-log';
import { KbnClient } from '@kbn/test';
import type { StatusResponse } from '@kbn/core-status-common-internal';
import { getLocalhostRealIp, isLocalhost } from './localhost_services';
import pRetry from 'p-retry';
import nodeFetch from 'node-fetch';
import { isLocalhost } from './is_localhost';
import { getLocalhostRealIp } from './localhost_services';
import { createSecuritySuperuser } from './security_user_services';
export interface RuntimeServices {
@ -47,6 +50,10 @@ interface CreateRuntimeServicesOptions {
fleetServerUrl?: string;
username: string;
password: string;
/** If undefined, ES username defaults to `username` */
esUsername?: string;
/** If undefined, ES password defaults to `password` */
esPassword?: string;
log?: ToolingLog;
asSuperuser?: boolean;
}
@ -57,13 +64,17 @@ export const createRuntimeServices = async ({
fleetServerUrl = 'https://localhost:8220',
username: _username,
password: _password,
log = new ToolingLog(),
esUsername,
esPassword,
log = new ToolingLog({ level: 'info', writeTo: process.stdout }),
asSuperuser = false,
}: CreateRuntimeServicesOptions): Promise<RuntimeServices> => {
let username = _username;
let password = _password;
if (asSuperuser) {
await waitForKibana(kibanaUrl);
const superuserResponse = await createSecuritySuperuser(
createEsClient({
url: elasticsearchUrl,
@ -86,7 +97,12 @@ export const createRuntimeServices = async ({
return {
kbnClient: createKbnClient({ log, url: kibanaUrl, username, password }),
esClient: createEsClient({ log, url: elasticsearchUrl, username, password }),
esClient: createEsClient({
log,
url: elasticsearchUrl,
username: esUsername ?? username,
password: esPassword ?? password,
}),
log,
localhostRealIp: await getLocalhostRealIp(),
user: {
@ -187,3 +203,29 @@ export const fetchStackVersion = async (kbnClient: KbnClient): Promise<string> =
return status.version.number;
};
/**
* Checks to ensure Kibana is up and running
* @param kbnUrl
*/
export const waitForKibana = async (kbnUrl: string): Promise<void> => {
const url = (() => {
const u = new URL(kbnUrl);
// This API seems to be available even if user is not authenticated
u.pathname = '/api/status';
return u.toString();
})();
await pRetry(
async () => {
const response = await nodeFetch(url);
if (response.status !== 200) {
throw new Error(
`Kibana not available. Returned: [${response.status}]: ${response.statusText}`
);
}
},
{ maxTimeout: 10000 }
);
};

View file

@ -36,8 +36,8 @@ import type {
PostFleetServerHostsResponse,
} from '@kbn/fleet-plugin/common/types/rest_spec/fleet_server_hosts';
import chalk from 'chalk';
import { isLocalhost } from '../common/is_localhost';
import { dump } from './utils';
import { isLocalhost } from '../common/localhost_services';
import { fetchFleetServerUrl, waitForHostToEnroll } from '../common/fleet_services';
import { getRuntimeServices } from './runtime';

View file

@ -202,7 +202,6 @@ export const cli = () => {
);
if (
// @ts-expect-error
configFromTestFile?.enableExperimental?.length &&
_.some(vars.kbnTestServer.serverArgs, (value) =>
value.includes('--xpack.securitySolution.enableExperimental')
@ -220,7 +219,13 @@ export const cli = () => {
}
if (configFromTestFile?.license) {
vars.esTestCluster.license = configFromTestFile.license;
if (vars.serverless) {
log.warning(
`'ftrConfig.license' ignored. Value does not apply to kibana when running in serverless.\nFile: ${filePath}`
);
} else {
vars.esTestCluster.license = configFromTestFile.license;
}
}
if (hasFleetServerArgs) {
@ -229,10 +234,44 @@ export const cli = () => {
);
}
// Serverless Specific
if (vars.serverless) {
log.info(`Serverless mode detected`);
if (configFromTestFile?.productTypes) {
vars.kbnTestServer.serverArgs.push(
`--xpack.securitySolutionServerless.productTypes=${JSON.stringify([
...configFromTestFile.productTypes,
// Why spread it twice?
// The `serverless.security.yml` file by default includes two product types as of this change.
// Because it's an array, we need to ensure that existing values are "removed" and the ones
// defined here are added. To do that, we duplicate the `productTypes` passed so that all array
// elements in that YAML file are updated. The Security serverless plugin has code in place to
// dedupe.
...configFromTestFile.productTypes,
])}`
);
}
} else if (configFromTestFile?.productTypes) {
log.warning(
`'ftrConfig.productTypes' ignored. Value applies only when running kibana is serverless.\nFile: ${filePath}`
);
}
return vars;
}
);
log.info(`
----------------------------------------------
Cypress FTR setup for file: ${filePath}:
----------------------------------------------
${JSON.stringify(config.getAll(), null, 2)}
----------------------------------------------
`);
const lifecycle = new Lifecycle(log);
const providers = new ProviderCollection(log, [
@ -280,18 +319,84 @@ export const cli = () => {
EsVersion.getDefault()
);
const customEnv = await pRetry(() => functionalTestRunner.run(abortCtrl.signal), {
const createUrlFromFtrConfig = (
type: 'elasticsearch' | 'kibana' | 'fleetserver',
withAuth: boolean = false
): string => {
const getKeyPath = (path: string = ''): string => {
return `servers.${type}${path ? `.${path}` : ''}`;
};
if (!config.get(getKeyPath())) {
throw new Error(`Unable to create URL for ${type}. Not found in FTR config at `);
}
const url = new URL('http://localhost');
url.port = config.get(getKeyPath('port'));
url.protocol = config.get(getKeyPath('protocol'));
url.hostname = config.get(getKeyPath('hostname'));
if (withAuth) {
url.username = config.get(getKeyPath('username'));
url.password = config.get(getKeyPath('password'));
}
return url.toString().replace(/\/$/, '');
};
const baseUrl = createUrlFromFtrConfig('kibana');
const ftrEnv = await pRetry(() => functionalTestRunner.run(abortCtrl.signal), {
retries: 1,
});
log.debug(
`Env. variables returned by [functionalTestRunner.run()]:\n`,
JSON.stringify(ftrEnv, null, 2)
);
// Normalized the set of available env vars in cypress
const cyCustomEnv = {
...ftrEnv,
// NOTE:
// ELASTICSEARCH_URL needs to be crated here with auth because SIEM cypress setup depends on it. At some
// points we should probably try to refactor that code to use `ELASTICSEARCH_URL_WITH_AUTH` instead
ELASTICSEARCH_URL:
ftrEnv.ELASTICSEARCH_URL ?? createUrlFromFtrConfig('elasticsearch', true),
ELASTICSEARCH_URL_WITH_AUTH: createUrlFromFtrConfig('elasticsearch', true),
ELASTICSEARCH_USERNAME:
ftrEnv.ELASTICSEARCH_USERNAME ?? config.get('servers.elasticsearch.username'),
ELASTICSEARCH_PASSWORD:
ftrEnv.ELASTICSEARCH_PASSWORD ?? config.get('servers.elasticsearch.password'),
FLEET_SERVER_URL: createUrlFromFtrConfig('fleetserver'),
KIBANA_URL: baseUrl,
KIBANA_URL_WITH_AUTH: createUrlFromFtrConfig('kibana', true),
KIBANA_USERNAME: config.get('servers.kibana.username'),
KIBANA_PASSWORD: config.get('servers.kibana.password'),
};
log.info(`
----------------------------------------------
Cypress run ENV for file: ${filePath}:
----------------------------------------------
${JSON.stringify(cyCustomEnv, null, 2)}
----------------------------------------------
`);
if (isOpen) {
await cypress.open({
configFile: cypressConfigFilePath,
config: {
e2e: {
baseUrl: `http://localhost:${kibanaPort}`,
baseUrl,
},
env: customEnv,
env: cyCustomEnv,
},
});
} else {
@ -304,10 +409,10 @@ export const cli = () => {
reporterOptions: argv.reporterOptions,
config: {
e2e: {
baseUrl: `http://localhost:${kibanaPort}`,
baseUrl,
},
numTestsKeptInMemory: 0,
env: customEnv,
env: cyCustomEnv,
},
});
} catch (error) {

View file

@ -8,16 +8,12 @@
import _ from 'lodash';
import * as fs from 'fs';
import * as parser from '@babel/parser';
import type {
ExpressionStatement,
Identifier,
ObjectExpression,
ObjectProperty,
} from '@babel/types';
import generate from '@babel/generator';
import type { ExpressionStatement, ObjectExpression, ObjectProperty } from '@babel/types';
import { schema, type TypeOf } from '@kbn/config-schema';
import { getExperimentalAllowedValues } from '../../common/experimental_features';
export const parseTestFileConfig = (
filePath: string
): Record<string, string | number | Record<string, string | number>> | undefined => {
export const parseTestFileConfig = (filePath: string): SecuritySolutionDescribeBlockFtrConfig => {
const testFile = fs.readFileSync(filePath, { encoding: 'utf8' });
const ast = parser.parse(testFile, {
@ -52,28 +48,56 @@ export const parseTestFileConfig = (
return {};
}
return _.reduce(
ftrConfig.value.properties,
(acc: Record<string, string | number | Record<string, string>>, property) => {
const key = (property.key as Identifier).name;
let value;
if (property.value.type === 'ArrayExpression') {
value = _.map(property.value.elements, (element) => {
if (element.type === 'StringLiteral') {
return element.value as string;
}
return element.value as string;
});
} else if (property.value.type === 'StringLiteral') {
value = property.value.value;
}
if (key && value) {
acc[key] = value;
}
return acc;
},
{}
);
const ftrConfigCode = generate(ftrConfig.value, { jsonCompatibleStrings: true }).code;
try {
// TODO:PT need to assess implication of using this approach to get the JSON back out
const ftrConfigJson = new Function(`return ${ftrConfigCode}`)();
return TestFileFtrConfigSchema.validate(ftrConfigJson);
} catch (err) {
throw new Error(
`Failed to parse 'ftrConfig' value defined in 'describe()' at ${filePath}. ${err.message}\nCode: ${ftrConfigCode}`
);
}
}
return undefined;
return {};
};
const TestFileFtrConfigSchema = schema.object(
{
license: schema.maybe(schema.string()),
enableExperimental: schema.maybe(
schema.arrayOf(
schema.string({
validate: (value) => {
const allowedValues = getExperimentalAllowedValues();
if (!allowedValues.includes(value)) {
return `Invalid [enableExperimental] value {${value}.\nValid values are: [${allowedValues.join(
', '
)}]`;
}
},
})
)
),
productTypes: schema.maybe(
// TODO:PT write validate function to ensure that only the correct combinations are used
schema.arrayOf(
schema.object({
product_line: schema.oneOf([
schema.literal('security'),
schema.literal('endpoint'),
schema.literal('cloud'),
]),
product_tier: schema.oneOf([schema.literal('essentials'), schema.literal('complete')]),
})
)
),
},
{ defaultValue: {}, unknowns: 'forbid' }
);
export type SecuritySolutionDescribeBlockFtrConfig = TypeOf<typeof TestFileFtrConfigSchema>;

View file

@ -6,9 +6,9 @@
*/
import { AppFeatures } from '.';
import type { Logger } from '@kbn/core/server';
import type { AppFeatureKeys, ExperimentalFeatures } from '../../../common';
import type { PluginSetupContract } from '@kbn/features-plugin/server';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
const SECURITY_BASE_CONFIG = {
foo: 'foo',
@ -85,7 +85,7 @@ describe('AppFeatures', () => {
const appFeatureKeys = ['test-base-feature'] as unknown as AppFeatureKeys;
const appFeatures = new AppFeatures(
{} as unknown as Logger,
loggingSystemMock.create().get('mock'),
[] as unknown as ExperimentalFeatures
);
appFeatures.init(featuresSetup);
@ -106,7 +106,7 @@ describe('AppFeatures', () => {
const appFeatureKeys = ['test-cases-feature'] as unknown as AppFeatureKeys;
const appFeatures = new AppFeatures(
{} as unknown as Logger,
loggingSystemMock.create().get('mock'),
[] as unknown as ExperimentalFeatures
);
appFeatures.init(featuresSetup);

View file

@ -73,27 +73,31 @@ export class AppFeatures {
const enabledSecurityAppFeaturesConfigs = this.getEnabledAppFeaturesConfigs(
getSecurityAppFeaturesConfig(this.experimentalFeatures)
);
this.featuresSetup.registerKibanaFeature(
this.securityFeatureConfigMerger.mergeAppFeatureConfigs(
securityBaseKibanaFeature,
securityBaseKibanaSubFeatureIds,
enabledSecurityAppFeaturesConfigs
)
const completeAppFeatureConfig = this.securityFeatureConfigMerger.mergeAppFeatureConfigs(
securityBaseKibanaFeature,
securityBaseKibanaSubFeatureIds,
enabledSecurityAppFeaturesConfigs
);
this.logger.debug(JSON.stringify(completeAppFeatureConfig));
this.featuresSetup.registerKibanaFeature(completeAppFeatureConfig);
// register security cases Kibana features
const securityCasesBaseKibanaFeature = getCasesBaseKibanaFeature();
const securityCasesBaseKibanaSubFeatureIds = getCasesBaseKibanaSubFeatureIds();
const enabledCasesAppFeaturesConfigs = this.getEnabledAppFeaturesConfigs(
getCasesAppFeaturesConfig()
);
this.featuresSetup.registerKibanaFeature(
this.casesFeatureConfigMerger.mergeAppFeatureConfigs(
securityCasesBaseKibanaFeature,
securityCasesBaseKibanaSubFeatureIds,
enabledCasesAppFeaturesConfigs
)
const completeCasesAppFeatureConfig = this.casesFeatureConfigMerger.mergeAppFeatureConfigs(
securityCasesBaseKibanaFeature,
securityCasesBaseKibanaSubFeatureIds,
enabledCasesAppFeaturesConfigs
);
this.logger.info(JSON.stringify(completeCasesAppFeatureConfig));
this.featuresSetup.registerKibanaFeature(completeCasesAppFeatureConfig);
}
private getEnabledAppFeaturesConfigs(

View file

@ -124,10 +124,7 @@ export const getSecurityBaseKibanaFeature = (): BaseKibanaFeatureConfig => ({
export const getSecurityBaseKibanaSubFeatureIds = (
_: ExperimentalFeatures // currently un-used, but left here as a convenience for possible future use
): SecuritySubFeatureId[] => [
SecuritySubFeatureId.hostIsolationExceptions,
SecuritySubFeatureId.hostIsolation,
];
): SecuritySubFeatureId[] => [];
/**
* Maps the AppFeatures keys to Kibana privileges that will be merged
@ -168,29 +165,19 @@ export const getSecurityAppFeaturesConfig = (
},
},
[AppFeatureSecurityKey.endpointResponseActions]: {
subFeatureIds: [
SecuritySubFeatureId.processOperations,
SecuritySubFeatureId.fileOperations,
SecuritySubFeatureId.executeAction,
],
subFeaturesPrivileges: [
{
id: 'host_isolation_all',
api: [`${APP_ID}-writeHostIsolation`],
ui: ['writeHostIsolation'],
},
],
[AppFeatureSecurityKey.endpointHostManagement]: {
subFeatureIds: [SecuritySubFeatureId.endpointList],
},
[AppFeatureSecurityKey.endpointExceptions]: {
[AppFeatureSecurityKey.endpointPolicyManagement]: {
subFeatureIds: [SecuritySubFeatureId.policyManagement],
},
[AppFeatureSecurityKey.endpointArtifactManagement]: {
subFeatureIds: [
SecuritySubFeatureId.trustedApplications,
SecuritySubFeatureId.blocklist,
SecuritySubFeatureId.eventFilters,
SecuritySubFeatureId.policyManagement,
SecuritySubFeatureId.endpointList,
SecuritySubFeatureId.responseActionsHistory,
],
subFeaturesPrivileges: [
{
@ -208,5 +195,24 @@ export const getSecurityAppFeaturesConfig = (
},
],
},
[AppFeatureSecurityKey.endpointResponseActions]: {
subFeatureIds: [
SecuritySubFeatureId.hostIsolationExceptions,
SecuritySubFeatureId.responseActionsHistory,
SecuritySubFeatureId.hostIsolation,
SecuritySubFeatureId.processOperations,
SecuritySubFeatureId.fileOperations,
SecuritySubFeatureId.executeAction,
],
subFeaturesPrivileges: [
{
id: 'host_isolation_all',
api: [`${APP_ID}-writeHostIsolation`],
ui: ['writeHostIsolation'],
},
],
},
};
};

View file

@ -166,5 +166,7 @@
"@kbn/discover-plugin",
"@kbn/data-view-editor-plugin",
"@kbn/navigation-plugin",
"@kbn/alerts-ui-shared",
"@kbn/core-logging-server-mocks",
]
}

View file

@ -32,3 +32,22 @@ export const productTypes = schema.arrayOf<SecurityProductType>(productType, {
defaultValue: [],
});
export type SecurityProductTypes = TypeOf<typeof productTypes>;
/**
* Developer only options that can be set in `serverless.security.dev.yml`
*/
export const developerConfigSchema = schema.object({
/**
* Disables the redirect in the UI for kibana management pages (ex. users, roles, etc).
*
* NOTE: you likely will also need to add the following to your `serverless.security.dev.yml`
* file if wanting to access the user, roles and role mapping pages via URL
*
* xpack.security.ui.userManagementEnabled: true
* xpack.security.ui.roleManagementEnabled: true
* xpack.security.ui.roleMappingManagementEnabled: true
*/
disableManagementUrlRedirect: schema.boolean({ defaultValue: false }),
});
export type DeveloperConfig = TypeOf<typeof developerConfigSchema>;

View file

@ -22,7 +22,11 @@ export const PLI_APP_FEATURES: PliAppFeatures = {
],
},
endpoint: {
essentials: [AppFeatureKey.endpointExceptions],
essentials: [
AppFeatureKey.endpointHostManagement,
AppFeatureKey.endpointPolicyManagement,
AppFeatureKey.endpointArtifactManagement,
],
complete: [AppFeatureKey.endpointResponseActions],
},
cloud: {

View file

@ -6,6 +6,7 @@
*/
import { APP_PATH, MANAGE_PATH } from '@kbn/security-solution-plugin/common';
import type { ServerlessSecurityPublicConfig } from '../types';
import type { Services } from '../common/services';
import { subscribeBreadcrumbs } from './breadcrumbs';
import { setAppLinks } from './links/app_links';
@ -14,10 +15,16 @@ import { getSecuritySideNavComponent } from './side_navigation';
const SECURITY_MANAGE_PATH = `${APP_PATH}${MANAGE_PATH}`;
export const configureNavigation = (services: Services) => {
export const configureNavigation = (
services: Services,
serverConfig: ServerlessSecurityPublicConfig
) => {
const { serverless, securitySolution, management } = services;
securitySolution.setIsSidebarEnabled(false);
management.setLandingPageRedirect(SECURITY_MANAGE_PATH);
if (!serverConfig.developer.disableManagementUrlRedirect) {
management.setLandingPageRedirect(SECURITY_MANAGE_PATH);
}
serverless.setProjectHome(APP_PATH);
serverless.setSideNavComponent(getSecuritySideNavComponent(services));

View file

@ -54,7 +54,7 @@ export class SecuritySolutionServerlessPlugin
securitySolution.setGetStartedPage(getSecurityGetStartedComponent(services, productTypes));
configureNavigation(services);
configureNavigation(services, this.config);
setRoutes(services);
return {};

View file

@ -12,7 +12,7 @@ import type {
} from '@kbn/security-solution-plugin/public';
import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public';
import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public';
import type { SecurityProductTypes } from '../common/config';
import type { SecurityProductTypes, DeveloperConfig } from '../common/config';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SecuritySolutionServerlessPluginSetup {}
@ -36,4 +36,5 @@ export interface SecuritySolutionServerlessPluginStartDeps {
export interface ServerlessSecurityPublicConfig {
productTypes: SecurityProductTypes;
developer: DeveloperConfig;
}

View file

@ -7,10 +7,11 @@
import { schema, type TypeOf } from '@kbn/config-schema';
import type { PluginConfigDescriptor } from '@kbn/core/server';
import { productTypes } from '../common/config';
import { developerConfigSchema, productTypes } from '../common/config';
export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: false }),
developer: developerConfigSchema,
productTypes,
});
export type ServerlessSecurityConfig = TypeOf<typeof configSchema>;
@ -18,6 +19,7 @@ export type ServerlessSecurityConfig = TypeOf<typeof configSchema>;
export const config: PluginConfigDescriptor<ServerlessSecurityConfig> = {
exposeToBrowser: {
productTypes: true,
developer: true,
},
schema: configSchema,
deprecations: ({ renameFromRoot }) => [

View file

@ -5,8 +5,13 @@
* 2.0.
*/
import type { PluginInitializerContext, Plugin, CoreSetup, CoreStart } from '@kbn/core/server';
import type {
PluginInitializerContext,
Plugin,
CoreSetup,
CoreStart,
Logger,
} from '@kbn/core/server';
import { getProductAppFeatures } from '../common/pli/pli_features';
import { METERING_TASK as ENDPOINT_METERING_TASK } from './endpoint/constants/metering';
import { endpointMeteringService } from './endpoint/services';
@ -33,9 +38,11 @@ export class SecuritySolutionServerlessPlugin
private config: ServerlessSecurityConfig;
private cspmUsageReportingTask: SecurityUsageReportingTask | undefined;
private endpointUsageReportingTask: SecurityUsageReportingTask | undefined;
private readonly logger: Logger;
constructor(private readonly initializerContext: PluginInitializerContext) {
this.config = this.initializerContext.config.get<ServerlessSecurityConfig>();
this.logger = this.initializerContext.logger.get();
}
public setup(_coreSetup: CoreSetup, pluginsSetup: SecuritySolutionServerlessPluginSetupDeps) {
@ -45,6 +52,14 @@ export class SecuritySolutionServerlessPlugin
const shouldRegister = pluginsSetup.securitySolutionEss == null;
this.logger.info(
`Security Solution running with product tiers:\n${JSON.stringify(
this.config.productTypes,
null,
2
)}`
);
if (shouldRegister) {
pluginsSetup.securitySolution.setAppFeatures(getProductAppFeatures(this.config.productTypes));
}

View file

@ -14,11 +14,13 @@ export function DefendWorkflowsCypressCliTestRunner(context: FtrProviderContext)
return {
FORCE_COLOR: '1',
ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')),
ELASTICSEARCH_USERNAME: config.get('servers.kibana.username'),
ELASTICSEARCH_PASSWORD: config.get('servers.kibana.password'),
ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'),
ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'),
FLEET_SERVER_URL: config.get('servers.fleetserver')
? Url.format(config.get('servers.fleetserver'))
: undefined,
KIBANA_USERNAME: config.get('servers.kibana.username'),
KIBANA_PASSWORD: config.get('servers.kibana.password'),
KIBANA_URL: Url.format({
protocol: config.get('servers.kibana.protocol'),
hostname: config.get('servers.kibana.hostname'),

View file

@ -6,6 +6,7 @@
*/
import { defineCypressConfig } from '@kbn/cypress-config';
import { setupDataLoaderTasks } from './support/setup_data_loader_tasks';
export default defineCypressConfig({
defaultCommandTimeout: 60000,
@ -23,5 +24,8 @@ export default defineCypressConfig({
experimentalMemoryManagement: true,
supportFile: './support/e2e.js',
specPattern: './e2e/**/*.cy.ts',
setupNodeEvents: (on, config) => {
setupDataLoaderTasks(on, config);
},
},
});

View file

@ -0,0 +1,207 @@
/*
* 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.
*/
// / <reference types="cypress" />
import { SecuritySolutionDescribeBlockFtrConfig } from '@kbn/security-solution-plugin/scripts/run_cypress/utils';
import {
DeleteIndexedFleetEndpointPoliciesResponse,
IndexedFleetEndpointPolicyResponse,
} from '@kbn/security-solution-plugin/common/endpoint/data_loaders/index_fleet_endpoint_policy';
import { CasePostRequest } from '@kbn/cases-plugin/common/api';
import {
DeletedIndexedCase,
IndexedCase,
} from '@kbn/security-solution-plugin/common/endpoint/data_loaders/index_case';
import {
HostActionResponse,
IndexEndpointHostsCyTaskOptions,
} from '@kbn/security-solution-plugin/public/management/cypress/types';
import { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data';
import { DeleteIndexedEndpointHostsResponse } from '@kbn/security-solution-plugin/common/endpoint/data_loaders/index_endpoint_hosts';
import {
DeletedIndexedEndpointRuleAlerts,
IndexedEndpointRuleAlerts,
} from '@kbn/security-solution-plugin/common/endpoint/data_loaders/index_endpoint_rule_alerts';
import {
HostPolicyResponse,
LogsEndpointActionResponse,
} from '@kbn/security-solution-plugin/common/endpoint/types';
import { IndexedEndpointPolicyResponse } from '@kbn/security-solution-plugin/common/endpoint/data_loaders/index_endpoint_policy_response';
import { DeleteAllEndpointDataResponse } from '@kbn/security-solution-plugin/scripts/endpoint/common/delete_all_endpoint_data';
import { LoadedRoleAndUser, ServerlessRoleName } from '../../../../shared/lib';
export interface LoadUserAndRoleCyTaskOptions {
name: ServerlessRoleName;
}
declare global {
namespace Cypress {
interface SuiteConfigOverrides {
env: {
ftrConfig: SecuritySolutionDescribeBlockFtrConfig;
};
}
interface Chainable<Subject = any> {
/**
* Get Elements by `data-test-subj`. Note that his is a parent query and can only be used
* from `cy`
*
* @param args
*
* @example
* // Correct:
* cy.getByTestSubj('some-subject);
*
* // Incorrect:
* cy.get('someElement').getByTestSubj('some-subject);
*/
getByTestSubj<E extends Node = HTMLElement>(
...args: Parameters<Cypress.Chainable<E>['get']>
): Chainable<JQuery<E>>;
/**
* Finds elements by `data-test-subj` from within another. Can not be used directly from `cy`.
*
* @example
* // Correct:
* cy.get('someElement').findByTestSubj('some-subject);
*
* // Incorrect:
* cy.findByTestSubj('some-subject);
*/
findByTestSubj<E extends Node = HTMLElement>(
...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>;
// ----------------------------------------------------
//
// TASKS
//
// ----------------------------------------------------
task(
name: 'loadUserAndRole',
arg: LoadUserAndRoleCyTaskOptions,
options?: Partial<Loggable & Timeoutable>
): Chainable<LoadedRoleAndUser>;
task(
name: 'indexFleetEndpointPolicy',
arg: {
policyName: string;
endpointPackageVersion: string;
},
options?: Partial<Loggable & Timeoutable>
): Chainable<IndexedFleetEndpointPolicyResponse>;
task(
name: 'deleteIndexedFleetEndpointPolicies',
arg: IndexedFleetEndpointPolicyResponse,
options?: Partial<Loggable & Timeoutable>
): Chainable<DeleteIndexedFleetEndpointPoliciesResponse>;
task(
name: 'indexCase',
arg?: Partial<CasePostRequest>,
options?: Partial<Loggable & Timeoutable>
): Chainable<IndexedCase['data']>;
task(
name: 'deleteIndexedCase',
arg: IndexedCase['data'],
options?: Partial<Loggable & Timeoutable>
): Chainable<DeletedIndexedCase>;
task(
name: 'indexEndpointHosts',
arg?: IndexEndpointHostsCyTaskOptions,
options?: Partial<Loggable & Timeoutable>
): Chainable<IndexedHostsAndAlertsResponse>;
task(
name: 'deleteIndexedEndpointHosts',
arg: IndexedHostsAndAlertsResponse,
options?: Partial<Loggable & Timeoutable>
): Chainable<DeleteIndexedEndpointHostsResponse>;
task(
name: 'indexEndpointRuleAlerts',
arg?: { endpointAgentId: string; count?: number },
options?: Partial<Loggable & Timeoutable>
): Chainable<IndexedEndpointRuleAlerts['alerts']>;
task(
name: 'deleteIndexedEndpointRuleAlerts',
arg: IndexedEndpointRuleAlerts['alerts'],
options?: Partial<Loggable & Timeoutable>
): Chainable<DeletedIndexedEndpointRuleAlerts>;
task(
name: 'indexEndpointPolicyResponse',
arg: HostPolicyResponse,
options?: Partial<Loggable & Timeoutable>
): Chainable<IndexedEndpointPolicyResponse>;
task(
name: 'deleteIndexedEndpointPolicyResponse',
arg: IndexedEndpointPolicyResponse,
options?: Partial<Loggable & Timeoutable>
): Chainable<null>;
task(
name: 'sendHostActionResponse',
arg: HostActionResponse,
options?: Partial<Loggable & Timeoutable>
): Chainable<LogsEndpointActionResponse>;
task(
name: 'deleteAllEndpointData',
arg: { endpointAgentIds: string[] },
options?: Partial<Loggable & Timeoutable>
): Chainable<DeleteAllEndpointDataResponse>;
task(
name: 'createFileOnEndpoint',
arg: { hostname: string; path: string; content: string },
options?: Partial<Loggable & Timeoutable>
): Chainable<null>;
task(
name: 'uploadFileToEndpoint',
arg: { hostname: string; srcPath: string; destPath: string },
options?: Partial<Loggable & Timeoutable>
): Chainable<null>;
task(
name: 'installPackagesOnEndpoint',
arg: { hostname: string; packages: string[] },
options?: Partial<Loggable & Timeoutable>
): Chainable<null>;
task(
name: 'readZippedFileContentOnEndpoint',
arg: { hostname: string; path: string; password?: string },
options?: Partial<Loggable & Timeoutable>
): Chainable<string>;
}
}
}

View file

@ -0,0 +1,46 @@
/*
* 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 { RESPONSE_ACTION_API_COMMANDS_NAMES } from '@kbn/security-solution-plugin/common/endpoint/service/response_actions/constants';
import { login } from '../../../tasks/login';
import { getNoPrivilegesPage } from '../../../screens/endpoint_management/common';
import { getEndpointManagementPageList } from '../../../screens/endpoint_management';
import { ensureResponseActionAuthzAccess } from '../../../tasks/endpoint_management';
describe(
'App Features for Complete PLI',
{
env: {
ftrConfig: { productTypes: [{ product_line: 'security', product_tier: 'complete' }] },
},
},
() => {
const pages = getEndpointManagementPageList();
let username: string;
let password: string;
beforeEach(() => {
login('endpoint_operations_analyst').then((response) => {
username = response.username;
password = response.password;
});
});
for (const { url, title } of pages) {
it(`should not allow access to ${title}`, () => {
cy.visit(url);
getNoPrivilegesPage().should('exist');
});
}
for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES) {
it(`should not allow access to Response Action: ${actionName}`, () => {
ensureResponseActionAuthzAccess('none', actionName, username, password);
});
}
}
);

View file

@ -0,0 +1,56 @@
/*
* 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 { RESPONSE_ACTION_API_COMMANDS_NAMES } from '@kbn/security-solution-plugin/common/endpoint/service/response_actions/constants';
import { login } from '../../../tasks/login';
import { getAgentListTable, visitFleetAgentList } from '../../../screens';
import { getEndpointManagementPageList } from '../../../screens/endpoint_management';
import { ensureResponseActionAuthzAccess } from '../../../tasks/endpoint_management';
describe(
'App Features for Complete PLI with Endpoint Complete',
{
env: {
ftrConfig: {
productTypes: [
{ product_line: 'security', product_tier: 'complete' },
{ product_line: 'endpoint', product_tier: 'complete' },
],
},
},
},
() => {
const allPages = getEndpointManagementPageList();
let username: string;
let password: string;
beforeEach(() => {
login('endpoint_operations_analyst').then((response) => {
username = response.username;
password = response.password;
});
});
for (const { url, title, pageTestSubj } of allPages) {
it(`should allow access to ${title}`, () => {
cy.visit(url);
cy.getByTestSubj(pageTestSubj).should('exist');
});
}
for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES) {
it(`should allow access to Response Action: ${actionName}`, () => {
ensureResponseActionAuthzAccess('all', actionName, username, password);
});
}
it(`should have access to Fleet`, () => {
visitFleetAgentList();
getAgentListTable().should('exist');
});
}
);

View file

@ -0,0 +1,48 @@
/*
* 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 { RESPONSE_ACTION_API_COMMANDS_NAMES } from '@kbn/security-solution-plugin/common/endpoint/service/response_actions/constants';
import { login } from '../../../tasks/login';
import { getNoPrivilegesPage } from '../../../screens/endpoint_management/common';
import { getEndpointManagementPageList } from '../../../screens/endpoint_management';
import { ensureResponseActionAuthzAccess } from '../../../tasks/endpoint_management';
describe(
'App Features for Essential PLI',
{
env: {
ftrConfig: {
productTypes: [{ product_line: 'security', product_tier: 'essentials' }],
},
},
},
() => {
const pages = getEndpointManagementPageList();
let username: string;
let password: string;
beforeEach(() => {
login('endpoint_operations_analyst').then((response) => {
username = response.username;
password = response.password;
});
});
for (const { url, title } of pages) {
it(`should not allow access to ${title}`, () => {
cy.visit(url);
getNoPrivilegesPage().should('exist');
});
}
for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES) {
it(`should not allow access to Response Action: ${actionName}`, () => {
ensureResponseActionAuthzAccess('none', actionName, username, password);
});
}
}
);

View file

@ -0,0 +1,71 @@
/*
* 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 { RESPONSE_ACTION_API_COMMANDS_NAMES } from '@kbn/security-solution-plugin/common/endpoint/service/response_actions/constants';
import { login } from '../../../tasks/login';
import { getAgentListTable, visitFleetAgentList } from '../../../screens';
import { getEndpointManagementPageMap } from '../../../screens/endpoint_management';
import { ensureResponseActionAuthzAccess } from '../../../tasks/endpoint_management';
describe(
'App Features for Essentials PLI with Endpoint Essentials',
{
env: {
ftrConfig: {
productTypes: [
{ product_line: 'security', product_tier: 'essentials' },
{ product_line: 'endpoint', product_tier: 'essentials' },
],
},
},
},
() => {
const allPages = getEndpointManagementPageMap();
const allowedPages = [
allPages.endpointList,
allPages.policyList,
allPages.trustedApps,
allPages.blocklist,
allPages.eventFilters,
];
const deniedPages = [allPages.responseActionLog, allPages.hostIsolationExceptions];
let username: string;
let password: string;
beforeEach(() => {
login('endpoint_operations_analyst').then((response) => {
username = response.username;
password = response.password;
});
});
for (const { url, title, pageTestSubj } of allowedPages) {
it(`should allow access to ${title}`, () => {
cy.visit(url);
cy.getByTestSubj(pageTestSubj).should('exist');
});
}
for (const { url, title } of deniedPages) {
it(`should NOT allow access to ${title}`, () => {
cy.visit(url);
cy.getByTestSubj('noPrivilegesPage').should('exist');
});
}
for (const actionName of RESPONSE_ACTION_API_COMMANDS_NAMES) {
it(`should not allow access to Response Action: ${actionName}`, () => {
ensureResponseActionAuthzAccess('none', actionName, username, password);
});
}
it(`should have access to Fleet`, () => {
visitFleetAgentList();
getAgentListTable().should('exist');
});
}
);

View file

@ -0,0 +1,396 @@
/*
* 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 { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data';
import { pick } from 'lodash';
import { login } from '../../../tasks/login';
import { ServerlessRoleName } from '../../../../../../../shared/lib';
import {
EndpointArtifactPageId,
ensureArtifactPageAuthzAccess,
ensureEndpointListPageAuthzAccess,
getArtifactListEmptyStateAddButton,
getEndpointManagementPageList,
getEndpointManagementPageMap,
getNoPrivilegesPage,
openConsoleFromEndpointList,
openRowActionMenu,
visitEndpointList,
visitPolicyList,
} from '../../../screens/endpoint_management';
import {
ensurePermissionDeniedScreen,
getAgentListTable,
visitFleetAgentList,
} from '../../../screens';
import {
getConsoleHelpPanelResponseActionTestSubj,
openConsoleHelpPanel,
} from '../../../screens/endpoint_management/response_console';
describe(
'User Roles for Security Complete PLI with Endpoint Complete addon',
{
env: {
ftrConfig: {
productTypes: [
{ product_line: 'security', product_tier: 'complete' },
{ product_line: 'endpoint', product_tier: 'complete' },
],
},
},
},
() => {
const allPages = getEndpointManagementPageList();
const pageById = getEndpointManagementPageMap();
const consoleHelpPanelResponseActionsTestSubj = getConsoleHelpPanelResponseActionTestSubj();
let loadedEndpoints: IndexedHostsAndAlertsResponse;
before(() => {
cy.task('indexEndpointHosts', {}, { timeout: 240000 }).then((response) => {
loadedEndpoints = response;
});
});
after(() => {
if (loadedEndpoints) {
cy.task('deleteIndexedEndpointHosts', loadedEndpoints);
}
});
// roles `t1_analyst` and `t2_analyst` are very similar with exception of one page
(['t1_analyst', `t2_analyst`] as ServerlessRoleName[]).forEach((roleName) => {
describe(`for role: ${roleName}`, () => {
const deniedPages = allPages.filter((page) => page.id !== 'endpointList');
beforeEach(() => {
login(roleName);
});
it('should have READ access to Endpoint list page', () => {
ensureEndpointListPageAuthzAccess('read', true);
});
for (const { id, url, title } of deniedPages) {
// T2 analyst has Read view to Response Actions log
if (id === 'responseActionLog' && roleName === 't2_analyst') {
it(`should have access to: ${title}`, () => {
cy.visit(url);
getNoPrivilegesPage().should('not.exist');
});
} else {
it(`should NOT have access to: ${title}`, () => {
cy.visit(url);
getNoPrivilegesPage().should('exist');
});
}
}
it('should NOT have access to Fleet', () => {
visitFleetAgentList();
ensurePermissionDeniedScreen();
});
it('should NOT have access to execute response actions', () => {
visitEndpointList();
openRowActionMenu().findByTestSubj('console').should('not.exist');
});
});
});
describe('for role: t3_analyst', () => {
const artifactPagesFullAccess = [
pageById.trustedApps,
pageById.eventFilters,
pageById.hostIsolationExceptions,
pageById.blocklist,
];
const grantedResponseActions = pick(
consoleHelpPanelResponseActionsTestSubj,
'isolate',
'release',
'processes',
'kill-process',
'suspend-process',
'get-file',
'upload'
);
const deniedResponseActions = pick(consoleHelpPanelResponseActionsTestSubj, 'execute');
beforeEach(() => {
login('t3_analyst');
});
it('should have access to Endpoint list page', () => {
ensureEndpointListPageAuthzAccess('all', true);
});
for (const { title, id } of artifactPagesFullAccess) {
it(`should have CRUD access to: ${title}`, () => {
ensureArtifactPageAuthzAccess('all', id as EndpointArtifactPageId);
});
}
it('should NOT have access to Fleet', () => {
visitFleetAgentList();
ensurePermissionDeniedScreen();
});
describe('Response Actions access', () => {
beforeEach(() => {
visitEndpointList();
openConsoleFromEndpointList();
openConsoleHelpPanel();
});
for (const [action, testSubj] of Object.entries(grantedResponseActions)) {
it(`should have access to execute action: ${action}`, () => {
cy.getByTestSubj(testSubj).should('exist');
});
}
for (const [action, testSubj] of Object.entries(deniedResponseActions)) {
it(`should NOT have access to execute: ${action}`, () => {
cy.getByTestSubj(testSubj).should('not.exist');
});
}
});
});
describe('for role: threat_intelligence_analyst', () => {
const deniedPages = allPages.filter(({ id }) => id !== 'blocklist' && id !== 'endpointList');
beforeEach(() => {
login('threat_intelligence_analyst');
});
it('should have access to Endpoint list page', () => {
ensureEndpointListPageAuthzAccess('read', true);
});
it(`should have CRUD access to: Blocklist`, () => {
cy.visit(pageById.blocklist.url);
getArtifactListEmptyStateAddButton(pageById.blocklist.id as EndpointArtifactPageId).should(
'exist'
);
});
for (const { url, title } of deniedPages) {
it(`should NOT have access to: ${title}`, () => {
cy.visit(url);
getNoPrivilegesPage().should('exist');
});
}
it('should NOT have access to Fleet', () => {
visitFleetAgentList();
ensurePermissionDeniedScreen();
});
it('should have access to Response Actions Log', () => {
cy.visit(pageById.responseActionLog);
getNoPrivilegesPage().should('not.exist');
});
it('should NOT have access to execute response actions', () => {
visitEndpointList();
openRowActionMenu().findByTestSubj('console').should('not.exist');
});
});
describe('for role: rule_author', () => {
const artifactPagesFullAccess = [
pageById.trustedApps,
pageById.eventFilters,
pageById.blocklist,
];
beforeEach(() => {
login('rule_author');
});
for (const { id, title } of artifactPagesFullAccess) {
it(`should have CRUD access to: ${title}`, () => {
ensureArtifactPageAuthzAccess('all', id as EndpointArtifactPageId);
});
}
it('should have access to Endpoint list page', () => {
ensureEndpointListPageAuthzAccess('all', true);
});
it('should have access to policy management', () => {
visitPolicyList();
getNoPrivilegesPage().should('not.exist');
});
it(`should have Read access only to: Host Isolation Exceptions`, () => {
ensureArtifactPageAuthzAccess(
'read',
pageById.hostIsolationExceptions.id as EndpointArtifactPageId
);
});
it('should NOT have access to Fleet', () => {
visitFleetAgentList();
ensurePermissionDeniedScreen();
});
it('should have access to Response Actions Log', () => {
cy.visit(pageById.responseActionLog);
getNoPrivilegesPage().should('not.exist');
});
it('should NOT have access to execute response actions', () => {
visitEndpointList();
openRowActionMenu().findByTestSubj('console').should('not.exist');
});
});
describe('for role: soc_manager', () => {
const artifactPagesFullAccess = [
pageById.trustedApps,
pageById.eventFilters,
pageById.blocklist,
pageById.hostIsolationExceptions,
];
const grantedAccessPages = [pageById.endpointList, pageById.policyList];
beforeEach(() => {
login('soc_manager');
});
for (const { id, title } of artifactPagesFullAccess) {
it(`should have CRUD access to: ${title}`, () => {
ensureArtifactPageAuthzAccess('all', id as EndpointArtifactPageId);
});
}
for (const { url, title } of grantedAccessPages) {
it(`should have access to: ${title}`, () => {
cy.visit(url);
getNoPrivilegesPage().should('not.exist');
});
}
it('should NOT have access to Fleet', () => {
visitFleetAgentList();
ensurePermissionDeniedScreen();
});
describe('Response Actions access', () => {
beforeEach(() => {
visitEndpointList();
openConsoleFromEndpointList();
openConsoleHelpPanel();
});
Object.entries(consoleHelpPanelResponseActionsTestSubj).forEach(([action, testSubj]) => {
it(`should have access to execute action: ${action}`, () => {
cy.getByTestSubj(testSubj).should('exist');
});
});
});
});
describe('for role: endpoint_operations_analyst', () => {
const artifactPagesFullAccess = [
pageById.trustedApps,
pageById.eventFilters,
pageById.blocklist,
pageById.hostIsolationExceptions,
];
const grantedAccessPages = [pageById.endpointList, pageById.policyList];
beforeEach(() => {
login('endpoint_operations_analyst');
});
for (const { id, title } of artifactPagesFullAccess) {
it(`should have CRUD access to: ${title}`, () => {
ensureArtifactPageAuthzAccess('all', id as EndpointArtifactPageId);
});
}
for (const { url, title } of grantedAccessPages) {
it(`should have access to: ${title}`, () => {
cy.visit(url);
getNoPrivilegesPage().should('not.exist');
});
}
Object.entries(consoleHelpPanelResponseActionsTestSubj).forEach(([action, testSubj]) => {
it(`should have access to response action: ${action}`, () => {
visitEndpointList();
openConsoleFromEndpointList();
openConsoleHelpPanel();
cy.getByTestSubj(testSubj).should('exist');
});
});
it('should have access to Fleet', () => {
visitFleetAgentList();
getAgentListTable().should('exist');
});
});
(['platform_engineer', 'endpoint_policy_manager'] as ServerlessRoleName[]).forEach(
(roleName) => {
describe(`for role: ${roleName}`, () => {
const artifactPagesFullAccess = [
pageById.trustedApps,
pageById.eventFilters,
pageById.blocklist,
pageById.hostIsolationExceptions,
];
const grantedAccessPages = [pageById.endpointList, pageById.policyList];
beforeEach(() => {
login(roleName);
});
for (const { id, title } of artifactPagesFullAccess) {
it(`should have CRUD access to: ${title}`, () => {
ensureArtifactPageAuthzAccess('all', id as EndpointArtifactPageId);
});
}
for (const { url, title } of grantedAccessPages) {
it(`should have access to: ${title}`, () => {
cy.visit(url);
getNoPrivilegesPage().should('not.exist');
});
}
it('should have access to Fleet', () => {
visitFleetAgentList();
getAgentListTable().should('exist');
});
it('should have access to Response Actions Log', () => {
cy.visit(pageById.responseActionLog);
if (roleName === 'endpoint_policy_manager') {
getNoPrivilegesPage().should('exist');
} else {
getNoPrivilegesPage().should('not.exist');
}
});
it('should NOT have access to execute response actions', () => {
visitEndpointList();
openRowActionMenu().findByTestSubj('console').should('not.exist');
});
});
}
);
}
);

View file

@ -0,0 +1,276 @@
/*
* 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 { IndexedHostsAndAlertsResponse } from '@kbn/security-solution-plugin/common/endpoint/index_data';
import { login } from '../../../tasks/login';
import {
getNoPrivilegesPage,
getArtifactListEmptyStateAddButton,
getEndpointManagementPageMap,
getEndpointManagementPageList,
EndpointArtifactPageId,
ensureArtifactPageAuthzAccess,
ensureEndpointListPageAuthzAccess,
ensurePolicyListPageAuthzAccess,
} from '../../../screens/endpoint_management';
import {
ensurePermissionDeniedScreen,
getAgentListTable,
visitFleetAgentList,
} from '../../../screens';
import { ServerlessRoleName } from '../../../../../../../shared/lib';
describe(
'Roles for Security Essential PLI with Endpoint Essentials addon',
{
env: {
ftrConfig: {
productTypes: [
{ product_line: 'security', product_tier: 'essentials' },
{ product_line: 'endpoint', product_tier: 'essentials' },
],
},
},
},
() => {
const allPages = getEndpointManagementPageList();
const pageById = getEndpointManagementPageMap();
let loadedEndpoints: IndexedHostsAndAlertsResponse;
before(() => {
cy.task('indexEndpointHosts', {}, { timeout: 240000 }).then((response) => {
loadedEndpoints = response;
});
});
after(() => {
if (loadedEndpoints) {
cy.task('deleteIndexedEndpointHosts', loadedEndpoints);
}
});
// roles `t1_analyst` and `t2_analyst` are the same as far as endpoint access
(['t1_analyst', `t2_analyst`] as ServerlessRoleName[]).forEach((roleName) => {
describe(`for role: ${roleName}`, () => {
const deniedPages = allPages.filter((page) => page.id !== 'endpointList');
beforeEach(() => {
login(roleName);
});
it('should have READ access to Endpoint list page', () => {
ensureEndpointListPageAuthzAccess('read', true);
});
for (const { url, title } of deniedPages) {
it(`should NOT have access to: ${title}`, () => {
cy.visit(url);
getNoPrivilegesPage().should('exist');
});
}
it('should NOT have access to Fleet', () => {
visitFleetAgentList();
ensurePermissionDeniedScreen();
});
});
});
describe('for role: t3_analyst', () => {
const artifactPagesFullAccess = [
pageById.trustedApps,
pageById.eventFilters,
pageById.blocklist,
];
beforeEach(() => {
login('t3_analyst');
});
it('should have access to Endpoint list page', () => {
ensureEndpointListPageAuthzAccess('all', true);
});
it('should have read access to Endpoint Policy Management', () => {
ensurePolicyListPageAuthzAccess('read', true);
});
for (const { title, id } of artifactPagesFullAccess) {
it(`should have CRUD access to: ${title}`, () => {
ensureArtifactPageAuthzAccess('all', id as EndpointArtifactPageId);
});
}
it(`should NOT have access to Host Isolation Exceptions`, () => {
ensureArtifactPageAuthzAccess(
'none',
pageById.hostIsolationExceptions.id as EndpointArtifactPageId
);
});
it('should NOT have access to Fleet', () => {
visitFleetAgentList();
ensurePermissionDeniedScreen();
});
});
describe('for role: threat_intelligence_analyst', () => {
const deniedPages = allPages.filter(({ id }) => id !== 'blocklist' && id !== 'endpointList');
beforeEach(() => {
login('threat_intelligence_analyst');
});
it('should have access to Endpoint list page', () => {
ensureEndpointListPageAuthzAccess('read', true);
});
it(`should have ALL access to: Blocklist`, () => {
cy.visit(pageById.blocklist.url);
getArtifactListEmptyStateAddButton(pageById.blocklist.id as EndpointArtifactPageId).should(
'exist'
);
});
for (const { url, title } of deniedPages) {
it(`should NOT have access to: ${title}`, () => {
cy.visit(url);
getNoPrivilegesPage().should('exist');
});
}
it('should NOT have access to Fleet', () => {
visitFleetAgentList();
ensurePermissionDeniedScreen();
});
});
describe('for role: rule_author', () => {
const artifactPagesFullAccess = [
pageById.trustedApps,
pageById.eventFilters,
pageById.blocklist,
];
beforeEach(() => {
login('rule_author');
});
for (const { id, title } of artifactPagesFullAccess) {
it(`should have CRUD access to: ${title}`, () => {
ensureArtifactPageAuthzAccess('all', id as EndpointArtifactPageId);
});
}
it('should have access to Endpoint list page', () => {
ensureEndpointListPageAuthzAccess('all', true);
});
it('should have access to policy management', () => {
ensurePolicyListPageAuthzAccess('all', true);
});
it(`should NOT have access to Host Isolation Exceptions`, () => {
ensureArtifactPageAuthzAccess(
'none',
pageById.hostIsolationExceptions.id as EndpointArtifactPageId
);
});
it('should NOT have access to Fleet', () => {
visitFleetAgentList();
ensurePermissionDeniedScreen();
});
});
describe('for role: soc_manager', () => {
const artifactPagesFullAccess = [
pageById.trustedApps,
pageById.eventFilters,
pageById.blocklist,
];
const grantedAccessPages = [pageById.endpointList, pageById.policyList];
beforeEach(() => {
login('soc_manager');
});
for (const { id, title } of artifactPagesFullAccess) {
it(`should have CRUD access to: ${title}`, () => {
ensureArtifactPageAuthzAccess('all', id as EndpointArtifactPageId);
});
}
for (const { url, title } of grantedAccessPages) {
it(`should have access to: ${title}`, () => {
cy.visit(url);
getNoPrivilegesPage().should('not.exist');
});
}
it(`should NOT have access to Host Isolation Exceptions`, () => {
ensureArtifactPageAuthzAccess(
'none',
pageById.hostIsolationExceptions.id as EndpointArtifactPageId
);
});
it('should NOT have access to Fleet', () => {
visitFleetAgentList();
ensurePermissionDeniedScreen();
});
});
// Endpoint Operations Manager, Endpoint Policy Manager and Platform Engineer currently have the same level of access
(
[
'platform_engineer',
`endpoint_operations_analyst`,
'endpoint_policy_manager',
] as ServerlessRoleName[]
).forEach((roleName) => {
describe(`for role: ${roleName}`, () => {
const artifactPagesFullAccess = [
pageById.trustedApps,
pageById.eventFilters,
pageById.blocklist,
];
const grantedAccessPages = [pageById.endpointList, pageById.policyList];
beforeEach(() => {
login(roleName);
});
for (const { id, title } of artifactPagesFullAccess) {
it(`should have CRUD access to: ${title}`, () => {
ensureArtifactPageAuthzAccess('all', id as EndpointArtifactPageId);
});
}
for (const { url, title } of grantedAccessPages) {
it(`should have access to: ${title}`, () => {
cy.visit(url);
getNoPrivilegesPage().should('not.exist');
});
}
it(`should NOT have access to Host Isolation Exceptions`, () => {
ensureArtifactPageAuthzAccess(
'none',
pageById.hostIsolationExceptions.id as EndpointArtifactPageId
);
});
it('should have access to Fleet', () => {
visitFleetAgentList();
getAgentListTable().should('exist');
});
});
});
}
);

View file

@ -6,10 +6,12 @@
*/
import { LEFT_NAVIGATION } from '../screens/landing_page';
import { login } from '../tasks/login';
import { navigatesToLandingPage } from '../tasks/navigation';
describe('Serverless', () => {
it('Should navigate to the landing page', () => {
login();
navigatesToLandingPage();
cy.get(LEFT_NAVIGATION).should('exist');
});

View file

@ -5,6 +5,7 @@
"private": true,
"license": "Elastic License 2.0",
"scripts": {
"cypress": "../../../../../../node_modules/.bin/cypress",
"cypress:open": "node ../../../../../plugins/security_solution/scripts/start_cypress_parallel open --config-file ../../../x-pack/test_serverless/functional/test_suites/security/cypress/cypress.config.ts --ftr-config-file ../../../../../../x-pack/test_serverless/functional/test_suites/security/cypress/security_config",
"cypress:run": "node ../../../../../plugins/security_solution/scripts/start_cypress_parallel run --browser chrome --config-file ../../../x-pack/test_serverless/functional/test_suites/security/cypress/cypress.config.ts --ftr-config-file ../../../../../../x-pack/test_serverless/functional/test_suites/security/cypress/security_config --reporter ../../../../../../node_modules/cypress-multi-reporters --reporter-options configFile=./reporter_config.json; status=$?; yarn junit:merge && exit $status",
"junit:merge": "../../../../../../node_modules/.bin/mochawesome-merge ../../../../../../target/kibana-security-serverless/cypress/results/mochawesome*.json > ../../../../../../target/kibana-security-serverless/cypress/results/output.json && ../../../../../../node_modules/.bin/marge ../../../../../../target/kibana-security-serverless/cypress/results/output.json --reportDir ../../../../../../target/kibana-security-serverless/cypress/results && mkdir -p ../../../../../../target/junit && cp ../../../../../../target/kibana-security-serverless/cypress/results/*.xml ../../../../../../target/junit/"

View file

@ -0,0 +1,104 @@
/*
* 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 { DeepReadonly } from 'utility-types';
import { subj as testSubjSelector } from '@kbn/test-subj-selector';
import {
EndpointArtifactPageId,
EndpointManagementPageMap,
getEndpointManagementPageMap,
} from './page_reference';
import { UserAuthzAccessLevel } from './types';
const artifactPageTopTestSubjPrefix: Readonly<Record<EndpointArtifactPageId, string>> = {
trustedApps: 'trustedAppsListPage',
eventFilters: 'EventFiltersListPage',
hostIsolationExceptions: 'hostIsolationExceptionsListPage',
blocklist: 'blocklistPage',
};
const pagesById: DeepReadonly<EndpointManagementPageMap> = getEndpointManagementPageMap();
const createSubjectSelector = (selectorSuffix: string, pageId?: EndpointArtifactPageId): string => {
if (pageId) {
return testSubjSelector(`${artifactPageTopTestSubjPrefix[pageId]}${selectorSuffix}`);
}
return Object.values(artifactPageTopTestSubjPrefix)
.map((testSubjPrefix) => testSubjSelector(testSubjPrefix + selectorSuffix))
.join(',');
};
export const visitEndpointArtifactPage = (page: EndpointArtifactPageId): Cypress.Chainable => {
return cy.visit(pagesById[page]);
};
export const getArtifactListEmptyStateAddButton = (
artifactType: keyof typeof artifactPageTopTestSubjPrefix
): Cypress.Chainable => {
return cy.getByTestSubj(`${artifactPageTopTestSubjPrefix[artifactType]}-emptyState-addButton`);
};
export const isArtifactPageShowingEmptyState = (
pageId?: EndpointArtifactPageId
): Cypress.Chainable<boolean> => {
const emptyPageSelector = createSubjectSelector('-emptyState', pageId);
const otherPossiblePageViews = [
createSubjectSelector('-list', pageId),
testSubjSelector('noPrivilegesPage'),
].join(',');
let found: boolean = false;
return cy
.getByTestSubj('pageContainer')
.waitUntil(($pageContainer) => {
if ($pageContainer.find(emptyPageSelector).length > 0) {
found = true;
return true;
}
if ($pageContainer.find(otherPossiblePageViews).length > 0) {
found = false;
return true;
}
return false;
})
.then(() => {
return found;
});
};
/**
* Validates to ensure that the user has the given access level to an artifact page.
* @param accessLevel
* @param visitPage If defined, then the page (id) provided will first be `visit`ed and then auth is checked
*/
export const ensureArtifactPageAuthzAccess = (
accessLevel: UserAuthzAccessLevel,
visitPage?: EndpointArtifactPageId
): Cypress.Chainable => {
if (visitPage) {
visitEndpointArtifactPage(visitPage);
}
isArtifactPageShowingEmptyState().then((isEmptyState) => {
const addButtonSelector = isEmptyState
? createSubjectSelector('-emptyState-addButton', visitPage)
: createSubjectSelector('-pageAddButton', visitPage);
if (accessLevel === 'all') {
cy.get(addButtonSelector).should('exist');
} else if (accessLevel === 'read') {
cy.get(addButtonSelector).should('not.exist');
} else {
cy.getByTestSubj('noPrivilegesPage').should('exist');
}
});
return cy.getByTestSubj('pageContainer');
};

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
export const getNoPrivilegesPage = (): Cypress.Chainable => {
return cy.getByTestSubj('noPrivilegesPage');
};

View file

@ -0,0 +1,75 @@
/*
* 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 { DeepReadonly } from 'utility-types';
import { EndpointManagementPageMap, getEndpointManagementPageMap } from './page_reference';
import { UserAuthzAccessLevel } from './types';
import { getNoPrivilegesPage } from './common';
interface ListRowOptions {
endpointId?: string;
hostName?: string;
/** Zero-based row index */
rowIndex?: number;
}
const pageById: DeepReadonly<EndpointManagementPageMap> = getEndpointManagementPageMap();
export const visitEndpointList = (): Cypress.Chainable => {
return cy.visit(pageById.endpointList.url);
};
/**
* Validate that the endpoint list has the proper level of authz
*
* @param accessLevel
* @param visitPage if `true`, then the endpoint list page will be visited first
*/
export const ensureEndpointListPageAuthzAccess = (
accessLevel: UserAuthzAccessLevel,
visitPage: boolean = false
): Cypress.Chainable => {
if (visitPage) {
visitEndpointList();
}
if (accessLevel === 'none') {
return getNoPrivilegesPage().should('exist');
}
// Read and All are currently the same
return getNoPrivilegesPage().should('not.exist');
};
export const getTableRow = ({
endpointId,
hostName,
rowIndex = 0,
}: ListRowOptions = {}): Cypress.Chainable => {
if (endpointId) {
return cy.get(`tr[data-endpoint-id="${endpointId}"]`).should('exist');
}
if (hostName) {
return cy.getByTestSubj('hostnameCellLink').contains(hostName).closest('tr').should('exist');
}
return cy
.getByTestSubj('endpointListTable')
.find(`tbody tr[data-endpoint-id]`)
.eq(rowIndex)
.should('exist');
};
export const openRowActionMenu = (options?: ListRowOptions): Cypress.Chainable => {
getTableRow(options).findByTestSubj('endpointTableRowActions', { log: true }).click();
return cy.getByTestSubj('tableRowActionsMenuPanel');
};
export const openConsoleFromEndpointList = (options?: ListRowOptions): Cypress.Chainable => {
return openRowActionMenu(options).findByTestSubj('console').click();
};

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
export * from './common';
export * from './artifacts';
export * from './endpoint_list';
export * from './policy_list';
export * from './page_reference';
export * from './types';

View file

@ -0,0 +1,90 @@
/*
* 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_BLOCKLIST_PATH,
APP_ENDPOINTS_PATH,
APP_EVENT_FILTERS_PATH,
APP_HOST_ISOLATION_EXCEPTIONS_PATH,
APP_POLICIES_PATH,
APP_RESPONSE_ACTIONS_HISTORY_PATH,
APP_TRUSTED_APPS_PATH,
} from '@kbn/security-solution-plugin/common/constants';
import { keyBy } from 'lodash';
export interface EndpointManagementPageMap {
endpointList: EndpointManagementPage;
policyList: EndpointManagementPage;
trustedApps: EndpointManagementPage;
eventFilters: EndpointManagementPage;
hostIsolationExceptions: EndpointManagementPage;
blocklist: EndpointManagementPage;
responseActionLog: EndpointManagementPage;
}
export type EndpointManagementPageId = keyof EndpointManagementPageMap;
export type EndpointArtifactPageId = keyof Pick<
EndpointManagementPageMap,
'trustedApps' | 'eventFilters' | 'hostIsolationExceptions' | 'blocklist'
>;
interface EndpointManagementPage {
id: EndpointManagementPageId;
title: string;
url: string;
pageTestSubj: string;
}
export const getEndpointManagementPageList = (): EndpointManagementPage[] => {
return [
{
id: 'endpointList',
title: 'Endpoint list page',
url: APP_ENDPOINTS_PATH,
pageTestSubj: 'endpointPage',
},
{
id: 'policyList',
title: 'Policy List page',
url: APP_POLICIES_PATH,
pageTestSubj: 'policyListPage',
},
{
id: 'trustedApps',
title: 'Trusted Apps Page',
url: APP_TRUSTED_APPS_PATH,
pageTestSubj: 'trustedAppsListPage-container',
},
{
id: 'eventFilters',
title: 'Event Filters page',
url: APP_EVENT_FILTERS_PATH,
pageTestSubj: 'EventFiltersListPage-container',
},
{
id: 'hostIsolationExceptions',
title: 'Host Isolation Exceptions page',
url: APP_HOST_ISOLATION_EXCEPTIONS_PATH,
pageTestSubj: 'hostIsolationExceptionsListPage-container',
},
{
id: 'blocklist',
title: 'Blocklist page',
url: APP_BLOCKLIST_PATH,
pageTestSubj: 'blocklistPage-container',
},
{
id: 'responseActionLog',
title: 'Response Actions History Log page',
url: APP_RESPONSE_ACTIONS_HISTORY_PATH,
pageTestSubj: 'responseActionsPage',
},
];
};
export const getEndpointManagementPageMap = (): EndpointManagementPageMap => {
return keyBy(getEndpointManagementPageList(), 'id') as unknown as EndpointManagementPageMap;
};

View file

@ -0,0 +1,34 @@
/*
* 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 { DeepReadonly } from 'utility-types';
import { EndpointManagementPageMap, getEndpointManagementPageMap } from './page_reference';
import { getNoPrivilegesPage } from './common';
import { visitEndpointList } from './endpoint_list';
import { UserAuthzAccessLevel } from './types';
const pageById: DeepReadonly<EndpointManagementPageMap> = getEndpointManagementPageMap();
export const visitPolicyList = (): Cypress.Chainable => {
return cy.visit(pageById.policyList);
};
export const ensurePolicyListPageAuthzAccess = (
accessLevel: UserAuthzAccessLevel,
visitPage: boolean = false
): Cypress.Chainable => {
if (visitPage) {
visitEndpointList();
}
if (accessLevel === 'none') {
return getNoPrivilegesPage().should('exist');
}
// Read and All currently are the same
return getNoPrivilegesPage().should('not.exist');
};

View file

@ -0,0 +1,34 @@
/*
* 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 { ConsoleResponseActionCommands } from '@kbn/security-solution-plugin/common/endpoint/service/response_actions/constants';
export const getConsoleHelpPanelResponseActionTestSubj = (): Record<
ConsoleResponseActionCommands,
string
> => {
return {
isolate: 'endpointResponseActionsConsole-commandList-Responseactions-isolate',
release: 'endpointResponseActionsConsole-commandList-Responseactions-release',
processes: 'endpointResponseActionsConsole-commandList-Responseactions-processes',
['kill-process']: 'endpointResponseActionsConsole-commandList-Responseactions-kill-process',
['suspend-process']:
'endpointResponseActionsConsole-commandList-Responseactions-suspend-process',
['get-file']: 'endpointResponseActionsConsole-commandList-Responseactions-get-file',
execute: 'endpointResponseActionsConsole-commandList-Responseactions-execute',
upload: 'endpointResponseActionsConsole-commandList-Responseactions-upload',
};
};
export const ensureResponseConsoleIsOpen = (): Cypress.Chainable => {
return cy.getByTestSubj('consolePageOverlay').should('exist');
};
export const openConsoleHelpPanel = (): Cypress.Chainable => {
ensureResponseConsoleIsOpen();
return cy.getByTestSubj('endpointResponseActionsConsole-header-helpButton').click();
};

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export type UserAuthzAccessLevel = 'all' | 'read' | 'none';

View file

@ -0,0 +1,16 @@
/*
* 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 { FLEET_BASE_PATH } from '@kbn/fleet-plugin/public/constants';
export const visitFleetAgentList = (): Cypress.Chainable => {
return cy.visit(FLEET_BASE_PATH, { failOnStatusCode: false });
};
export const getAgentListTable = (): Cypress.Chainable => {
return cy.getByTestSubj('fleetAgentListTable');
};

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export * from './permission_denied';
export * from './agent_list';

View file

@ -0,0 +1,14 @@
/*
* 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.
*/
/**
* The screen normally returned by the API when a user does not have access to a Plugin.
* Note that the requested page will likely also receive an HTTP status code of `403`
*/
export const ensurePermissionDeniedScreen = (): Cypress.Chainable => {
return cy.contains('You do not have permission to access the requested page').should('exist');
};

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export * from './fleet';
export * from './landing_page';

View file

@ -20,13 +20,10 @@
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
import 'cypress-real-events/support';
import '@kbn/security-solution-plugin/public/management/cypress/support/e2e';
Cypress.on('uncaught:exception', () => {
return false;
});
// Alternatively you can use CommonJS syntax:
// require('./commands')

View file

@ -0,0 +1,45 @@
/*
* 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 { createRuntimeServices } from '@kbn/security-solution-plugin/scripts/endpoint/common/stack_services';
import { dataLoaders } from '@kbn/security-solution-plugin/public/management/cypress/support/data_loaders';
import { LoadUserAndRoleCyTaskOptions } from '../cypress';
import { LoadedRoleAndUser, SecurityRoleAndUserLoader } from '../../../../../shared/lib';
export const setupDataLoaderTasks = (
on: Cypress.PluginEvents,
config: Cypress.PluginConfigOptions
) => {
// Reuse data loaders from endpoint management cypress setup
dataLoaders(on, config);
const stackServicesPromise = createRuntimeServices({
kibanaUrl: config.env.KIBANA_URL,
elasticsearchUrl: config.env.ELASTICSEARCH_URL,
fleetServerUrl: config.env.FLEET_SERVER_URL,
username: config.env.KIBANA_USERNAME,
password: config.env.KIBANA_PASSWORD,
esUsername: config.env.ELASTICSEARCH_USERNAME,
esPassword: config.env.ELASTICSEARCH_PASSWORD,
});
const roleAndUserLoaderPromise: Promise<SecurityRoleAndUserLoader> = stackServicesPromise.then(
({ kbnClient, log }) => {
return new SecurityRoleAndUserLoader(kbnClient, log);
}
);
on('task', {
/**
* Loads a user/role into Kibana. Used from `login()` task.
* @param name
*/
loadUserAndRole: async ({ name }: LoadUserAndRoleCyTaskOptions): Promise<LoadedRoleAndUser> => {
return (await roleAndUserLoaderPromise).load(name);
},
});
};

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './response_actions';

View file

@ -0,0 +1,111 @@
/*
* 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 { ResponseActionsApiCommandNames } from '@kbn/security-solution-plugin/common/endpoint/service/response_actions/constants';
import { request } from '@kbn/security-solution-plugin/public/management/cypress/tasks/common';
import {
EXECUTE_ROUTE,
GET_FILE_ROUTE,
GET_PROCESSES_ROUTE,
ISOLATE_HOST_ROUTE_V2,
KILL_PROCESS_ROUTE,
SUSPEND_PROCESS_ROUTE,
UNISOLATE_HOST_ROUTE_V2,
UPLOAD_ROUTE,
} from '@kbn/security-solution-plugin/common/endpoint/constants';
import { UserAuthzAccessLevel } from '../../screens/endpoint_management';
/**
* Ensure user has the given `accessLevel` to the type of response action
* @param accessLevel
* @param responseAction
* @param username
* @param password
*/
export const ensureResponseActionAuthzAccess = (
accessLevel: Exclude<UserAuthzAccessLevel, 'read'>,
responseAction: ResponseActionsApiCommandNames,
username: string,
password: string
): Cypress.Chainable => {
let url: string = '';
let apiPayload: unknown = {
endpoint_ids: ['some-id'],
};
switch (responseAction) {
case 'isolate':
url = ISOLATE_HOST_ROUTE_V2;
break;
case 'unisolate':
url = UNISOLATE_HOST_ROUTE_V2;
break;
case 'get-file':
url = GET_FILE_ROUTE;
Object.assign(apiPayload, { parameters: { path: 'one/two' } });
break;
case 'execute':
url = EXECUTE_ROUTE;
Object.assign(apiPayload, { parameters: { command: 'foo' } });
break;
case 'running-processes':
url = GET_PROCESSES_ROUTE;
break;
case 'kill-process':
url = KILL_PROCESS_ROUTE;
Object.assign(apiPayload, { parameters: { pid: 123 } });
break;
case 'suspend-process':
url = SUSPEND_PROCESS_ROUTE;
Object.assign(apiPayload, { parameters: { pid: 123 } });
break;
case 'upload':
url = UPLOAD_ROUTE;
{
const file = new File(['foo'], 'foo.txt');
const formData = new FormData();
formData.append('file', file, file.name);
for (const [key, value] of Object.entries(apiPayload as object)) {
formData.append(key, typeof value !== 'string' ? JSON.stringify(value) : value);
}
apiPayload = formData;
}
break;
default:
throw new Error(`Response action [${responseAction}] has no API payload defined`);
}
const requestOptions: Partial<Cypress.RequestOptions> = {
url,
method: 'post',
auth: {
user: username,
pass: password,
},
headers: {
'Content-Type': undefined,
},
failOnStatusCode: false,
body: apiPayload as Cypress.RequestBody,
};
if (accessLevel === 'none') {
return request(requestOptions).its('status').should('equal', 403);
}
return request(requestOptions).its('status').should('not.equal', 403);
};

View file

@ -0,0 +1,86 @@
/*
* 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 { request } from '@kbn/security-solution-plugin/public/management/cypress/tasks/common';
import { isLocalhost } from '@kbn/security-solution-plugin/scripts/endpoint/common/is_localhost';
import { ServerlessRoleName } from '../../../../../shared/lib';
/**
* Send login via API
* @param username
* @param password
*
* @private
*/
const sendApiLoginRequest = (
username: string,
password: string
): Cypress.Chainable<{ username: string; password: string }> => {
const url = new URL(Cypress.config().baseUrl ?? '');
url.pathname = '/internal/security/login';
cy.log(`Authenticating [${username}] via ${url.toString()}`);
return request({
headers: { 'kbn-xsrf': 'cypress-creds-via-env' },
method: 'POST',
url: url.toString(),
body: {
providerType: 'basic',
providerName: isLocalhost(url.hostname) ? 'basic' : 'cloud-basic',
currentURL: '/',
params: {
username,
password,
},
},
}).then(() => {
return {
username,
password,
};
});
};
interface CyLoginTask {
(user?: ServerlessRoleName): ReturnType<typeof sendApiLoginRequest>;
/**
* Login using any username/password
* @param username
* @param password
*/
with(username: string, password: string): ReturnType<typeof sendApiLoginRequest>;
}
/**
* Login to Kibana using API (not login page). By default, user will be logged in using
* the username and password defined via `KIBANA_USERNAME` and `KIBANA_PASSWORD` cypress env
* variables.
* @param user Defaults to `soc_manager`
*/
export const login: CyLoginTask = (
user: ServerlessRoleName | 'elastic' = 'soc_manager'
): ReturnType<typeof sendApiLoginRequest> => {
let username = Cypress.env('KIBANA_USERNAME');
let password = Cypress.env('KIBANA_PASSWORD');
if (user && user !== 'elastic') {
return cy.task('loadUserAndRole', { name: user }).then((loadedUser) => {
username = loadedUser.username;
password = loadedUser.password;
return sendApiLoginRequest(username, password);
});
} else {
return sendApiLoginRequest(username, password);
}
};
login.with = (username: string, password: string): ReturnType<typeof sendApiLoginRequest> => {
return sendApiLoginRequest(username, password);
};

View file

@ -5,14 +5,6 @@
* 2.0.
*/
const ELASTICSEARCH_USERNAME = Cypress.env('ELASTICSEARCH_USERNAME');
const ELASTICSEARCH_PASSWORD = Cypress.env('ELASTICSEARCH_PASSWORD');
export const navigatesToLandingPage = () => {
cy.visit('/app/security/get_started', {
auth: {
username: ELASTICSEARCH_USERNAME,
password: ELASTICSEARCH_PASSWORD,
},
});
cy.visit('/app/security/get_started');
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getPageObject, getService }: FtrProviderContext) {
const svlSecLandingPage = getPageObject('svlSecLandingPage');

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getPageObject }: FtrProviderContext) {
const PageObject = getPageObject('common');

View file

@ -6,7 +6,7 @@
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getPageObject, getService }: FtrProviderContext) {
const svlSecLandingPage = getPageObject('svlSecLandingPage');

View file

@ -9,8 +9,8 @@ import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('serverless security UI', function () {
loadTestFile(require.resolve('./landing_page'));
loadTestFile(require.resolve('./navigation'));
loadTestFile(require.resolve('./management'));
loadTestFile(require.resolve('./ftr/landing_page'));
loadTestFile(require.resolve('./ftr/navigation'));
loadTestFile(require.resolve('./ftr/management'));
});
}

View file

@ -0,0 +1,4 @@
# Shared Libraries
Shared libraries that are test framework independent and thus can be included and used by both FTR and Cypress.

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './security';

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './kibana_roles';

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export * from './kibana_roles';
export * from './role_loader';

View file

@ -0,0 +1,87 @@
/*
* 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 { safeLoad as loadYaml } from 'js-yaml';
import { readFileSync } from 'fs';
import * as path from 'path';
import { cloneDeep } from 'lodash';
import { FeaturesPrivileges, Role, RoleIndexPrivilege } from '@kbn/security-plugin/common';
const ROLES_YAML_FILE_PATH = path.join(__dirname, 'project_controller_security_roles.yml');
const ROLE_NAMES = [
't1_analyst',
't2_analyst',
't3_analyst',
'threat_intelligence_analyst',
'rule_author',
'soc_manager',
'detections_admin',
'platform_engineer',
'endpoint_operations_analyst',
'endpoint_policy_manager',
] as const;
export type ServerlessRoleName = typeof ROLE_NAMES[number];
type YamlRoleDefinitions = Record<
ServerlessRoleName,
{
cluster: string[] | null;
indices: RoleIndexPrivilege[];
applications: Array<{
application: string;
privileges: string[];
resources: string;
}>;
}
>;
const roleDefinitions = loadYaml(readFileSync(ROLES_YAML_FILE_PATH, 'utf8')) as YamlRoleDefinitions;
export type ServerlessSecurityRoles = Record<ServerlessRoleName, Role>;
export const getServerlessSecurityKibanaRoleDefinitions = (): ServerlessSecurityRoles => {
const definitions = cloneDeep(roleDefinitions);
return Object.entries(definitions).reduce((roles, [roleName, definition]) => {
if (!ROLE_NAMES.includes(roleName as ServerlessRoleName)) {
throw new Error(
`Un-expected role [${roleName}] found in YAML file [${ROLES_YAML_FILE_PATH}]`
);
}
const kibanaRole: Role = {
name: roleName,
elasticsearch: {
cluster: definition.cluster ?? [],
indices: definition.indices ?? [],
run_as: [],
},
kibana: [
{
base: [],
spaces: ['*'],
feature: definition.applications.reduce((features, application) => {
if (application.resources !== '*') {
throw new Error(
`YAML role definition parser does not currently support 'application.resource = ${application.resources}' for ${application.application} `
);
}
features[application.application] = application.privileges;
return features;
}, {} as FeaturesPrivileges),
},
],
};
roles[roleName as ServerlessRoleName] = kibanaRole;
return roles;
}, {} as ServerlessSecurityRoles);
};

View file

@ -0,0 +1,670 @@
# -----
# Source: https://github.com/elastic/project-controller/blob/main/internal/project/security/config/roles.yml
# -----
t1_analyst:
cluster:
indices:
- names:
- ".alerts-security*"
- ".siem-signals-*"
privileges:
- read
- write
- maintenance
- names:
- apm-*-transaction*
- traces-apm*
- auditbeat-*
- endgame-*
- filebeat-*
- logs-*
- packetbeat-*
- winlogbeat-*
- metrics-endpoint.metadata_current_*
- ".fleet-agents*"
- ".fleet-actions*"
privileges:
- read
applications:
- application: ml
privileges:
- read
resources: "*"
- application: siem
privileges:
- read
- read_alerts
- endpoint_list_read
resources: "*"
- application: securitySolutionCases
privileges:
- read
resources: "*"
- application: actions
privileges:
- read
resources: "*"
- application: builtInAlerts
privileges:
- read
resources: "*"
- application: spaces
privileges:
- all
resources: "*"
t2_analyst:
cluster:
indices:
- names:
- .alerts-security*
- .siem-signals-*
privileges:
- read
- write
- maintenance
- names:
- .lists*
- .items*
- apm-*-transaction*
- traces-apm*
- auditbeat-*
- endgame-*
- filebeat-*
- logs-*
- packetbeat-*
- winlogbeat-*
- metrics-endpoint.metadata_current_*
- .fleet-agents*
- .fleet-actions*
privileges:
- read
applications:
- application: ml
privileges:
- read
resources: "*"
- application: siem
privileges:
- read
- read_alerts
- endpoint_list_read
resources: "*"
- application: securitySolutionCases
privileges:
- all
resources: "*"
- application: actions
privileges:
- read
resources: "*"
- application: builtInAlerts
privileges:
- read
resources: "*"
- application: spaces
privileges:
- all
resources: "*"
t3_analyst:
cluster:
indices:
- names:
- apm-*-transaction*
- traces-apm*
- auditbeat-*
- endgame-*
- filebeat-*
- logs-*
- packetbeat-*
- winlogbeat-*
privileges:
- read
- write
- names:
- .alerts-security*
- .siem-signals-*
privileges:
- read
- write
- names:
- .lists*
- .items*
privileges:
- read
- write
- names:
- metrics-endpoint.metadata_current_*
- .fleet-agents*
- .fleet-actions*
privileges:
- read
applications:
- application: ml
privileges:
- read
resources: "*"
- application: siem
privileges:
- all
- read_alerts
- crud_alerts
- endpoint_list_all
- trusted_applications_all
- event_filters_all
- host_isolation_exceptions_all
- blocklist_all
- policy_management_all # Elastic Defend Policy Management
- host_isolation_all
- process_operations_all
- actions_log_management_all # Response actions history
- file_operations_all
resources: "*"
- application: securitySolutionCases
privileges:
- all
resources: "*"
- application: actions
privileges:
- read
resources: "*"
- application: builtInAlerts
privileges:
- all
resources: "*"
- application: osquery
privileges:
- all
resources: "*"
- application: spaces
privileges:
- all
resources: "*"
threat_intelligence_analyst:
cluster:
indices:
- names:
- apm-*-transaction*
- traces-apm*
- auditbeat-*
- endgame-*
- filebeat-*
- logs-*
- .lists*
- .items*
- packetbeat-*
- winlogbeat-*
privileges:
- read
- names:
- .alerts-security*
- .siem-signals-*
privileges:
- read
- write
- maintenance
- names:
- metrics-endpoint.metadata_current_*
- .fleet-agents*
- .fleet-actions*
privileges:
- read
applications:
- application: ml
privileges:
- read
resources: "*"
- application: siem
privileges:
- read
- read_alerts
- endpoint_list_read
- blocklist_all
resources: "*"
- application: securitySolutionCases
privileges:
- all
resources: "*"
- application: actions
privileges:
- read
resources: "*"
- application: builtInAlerts
privileges:
- read
resources: "*"
- application: spaces
privileges:
- all
resources: "*"
rule_author:
cluster:
indices:
- names:
- apm-*-transaction*
- traces-apm*
- auditbeat-*
- endgame-*
- filebeat-*
- logs-*
- packetbeat-*
- winlogbeat-*
privileges:
- read
- write
- names:
- .alerts-security*
- .siem-signals-*
- .internal.preview.alerts-security*
- .preview.alerts-security*
privileges:
- read
- write
- maintenance
- view_index_metadata
- names:
- .lists*
- .items*
privileges:
- read
- write
- names:
- metrics-endpoint.metadata_current_*
- .fleet-agents*
- .fleet-actions*
privileges:
- read
applications:
- application: ml
privileges:
- read
resources: "*"
- application: siem
privileges:
- all
- read_alerts
- crud_alerts
- policy_management_all
- endpoint_list_all
- trusted_applications_all
- event_filters_all
- host_isolation_exceptions_read
- blocklist_all
- actions_log_management_read
resources: "*"
- application: securitySolutionCases
privileges:
- all
resources: "*"
- application: actions
privileges:
- read
resources: "*"
- application: builtInAlerts
privileges:
- all
resources: "*"
- application: spaces
privileges:
- all
resources: "*"
soc_manager:
cluster:
indices:
- names:
- apm-*-transaction*
- traces-apm*
- auditbeat-*
- endgame-*
- filebeat-*
- logs-*
- packetbeat-*
- winlogbeat-*
privileges:
- read
- write
- names:
- .alerts-security*
- .siem-signals-*
- .preview.alerts-security*
- .internal.preview.alerts-security*
privileges:
- read
- write
- manage
- names:
- .lists*
- .items*
privileges:
- read
- write
- names:
- metrics-endpoint.metadata_current_*
- .fleet-agents*
- .fleet-actions*
privileges:
- read
applications:
- application: ml
privileges:
- read
resources: "*"
- application: siem
privileges:
- all
- read_alerts
- crud_alerts
- policy_management_all
- endpoint_list_all
- trusted_applications_all
- event_filters_all
- host_isolation_exceptions_all
- blocklist_all
- host_isolation_all
- process_operations_all
- actions_log_management_all
- file_operations_all
- execute_operations_all
resources: "*"
- application: securitySolutionCases
privileges:
- all
resources: "*"
- application: actions
privileges:
- all
resources: "*"
- application: builtInAlerts
privileges:
- all
resources: "*"
- application: spaces
privileges:
- all
resources: "*"
detections_admin:
cluster:
indices:
- names:
- apm-*-transaction*
- traces-apm*
- auditbeat-*
- endgame-*
- filebeat-*
- logs-*
- packetbeat-*
- winlogbeat-*
- .lists*
- .items*
- .alerts-security*
- .siem-signals-*
- .preview.alerts-security*
- .internal.preview.alerts-security*
privileges:
- read
- write
- manage
- names:
- metrics-endpoint.metadata_current_*
- .fleet-agents*
- .fleet-actions*
privileges:
- read
applications:
- application: ml
privileges:
- all
resources: "*"
- application: siem
privileges:
- all
- read_alerts
- crud_alerts
resources: "*"
- application: securitySolutionCases
privileges:
- all
resources: "*"
- application: actions
privileges:
- read
resources: "*"
- application: builtInAlerts
privileges:
- all
resources: "*"
- application: dev_tools
privileges:
- all
resources: "*"
- application: spaces
privileges:
- all
resources: "*"
platform_engineer:
cluster:
- manage
indices:
- names:
- apm-*-transaction*
- traces-apm*
- auditbeat-*
- endgame-*
- filebeat-*
- logs-*
- packetbeat-*
- winlogbeat-*
- .lists*
- .items*
- .alerts-security*
- .siem-signals-*
- .preview.alerts-security*
- .internal.preview.alerts-security*
privileges:
- all
applications:
- application: ml
privileges:
- all
resources: "*"
- application: siem
privileges:
- all
- read_alerts
- crud_alerts
- policy_management_all
- endpoint_list_all
- trusted_applications_all
- event_filters_all
- host_isolation_exceptions_all
- blocklist_all
- actions_log_management_read
resources: "*"
- application: securitySolutionCases
privileges:
- all
resources: "*"
- application: actions
privileges:
- all
resources: "*"
- application: builtInAlerts
privileges:
- all
resources: "*"
- application: fleet
privileges:
- all
resources: "*"
- application: fleetv2
privileges:
- all
resources: "*"
- application: spaces
privileges:
- all
resources: "*"
endpoint_operations_analyst:
cluster:
indices:
- names:
- metrics-endpoint.metadata_current_*
- .fleet-agents*
- .fleet-actions*
privileges:
- read
- names:
- apm-*-transaction*
- traces-apm*
- auditbeat-*
- endgame-*
- filebeat-*
- logs-*
- packetbeat-*
- winlogbeat-*
- .lists*
- .items*
privileges:
- read
- names:
- .alerts-security*
- .siem-signals-*
- .preview.alerts-security*
- .internal.preview.alerts-security*
privileges:
- read
- write
applications:
- application: ml
privileges:
- read
resources: "*"
- application: siem
privileges:
- all
- read_alerts
- policy_management_all
- endpoint_list_all
- trusted_applications_all
- event_filters_all
- host_isolation_exceptions_all
- blocklist_all
- host_isolation_all
- process_operations_all
- actions_log_management_all # Response History
- file_operations_all
- execute_operations_all # Execute
resources: "*"
- application: securitySolutionCases
privileges:
- all
resources: "*"
- application: actions
privileges:
- all
resources: "*"
- application: builtInAlerts
privileges:
- all
resources: "*"
- application: osquery
privileges:
- all
resources: "*"
- application: fleet
privileges:
- all
resources: "*"
- application: fleetv2
privileges:
- all
resources: "*"
- application: spaces
privileges:
- all
resources: "*"
endpoint_policy_manager:
cluster:
indices:
- names:
- metrics-endpoint.metadata_current_*
- .fleet-agents*
- .fleet-actions*
privileges:
- read
- names:
- apm-*-transaction*
- traces-apm*
- auditbeat-*
- endgame-*
- filebeat-*
- logs-*
- packetbeat-*
- winlogbeat-*
- .lists*
- .items*
privileges:
- read
- names:
- .alerts-security*
- .siem-signals-*
- .preview.alerts-security*
- .internal.preview.alerts-security*
privileges:
- read
- write
- manage
applications:
- application: ml
privileges:
- read
resources: "*"
- application: siem
privileges:
- all
- read_alerts
- crud_alerts
- policy_management_all
- trusted_applications_all
- event_filters_all
- host_isolation_exceptions_all
- blocklist_all
- endpoint_list_all
resources: "*"
- application: securitySolutionCases
privileges:
- all
resources: "*"
- application: actions
privileges:
- all
resources: "*"
- application: builtInAlerts
privileges:
- all
resources: "*"
- application: osquery
privileges:
- all
resources: "*"
- application: fleet
privileges:
- all
resources: "*"
- application: fleetv2
privileges:
- all
resources: "*"
- application: spaces
privileges:
- all
resources: "*"

View file

@ -0,0 +1,122 @@
/*
* 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.
*/
/* eslint-disable max-classes-per-file */
import { KbnClient } from '@kbn/test';
import { Role } from '@kbn/security-plugin/common';
import { ToolingLog } from '@kbn/tooling-log';
import { inspect } from 'util';
import { AxiosError } from 'axios';
import {
getServerlessSecurityKibanaRoleDefinitions,
ServerlessSecurityRoles,
} from './kibana_roles';
const ignoreHttp409Error = (error: AxiosError) => {
if (error?.response?.status === 409) {
return;
}
throw error;
};
export interface LoadedRoleAndUser {
role: string;
username: string;
password: string;
}
export class RoleAndUserLoader<R extends Record<string, Role> = Record<string, Role>> {
protected readonly logPromiseError: (error: Error) => never;
constructor(
protected readonly kbnClient: KbnClient,
protected readonly logger: ToolingLog,
protected readonly roles: R
) {
this.logPromiseError = (error) => {
this.logger.error(inspect(error, { depth: 5 }));
throw error;
};
}
async load(name: keyof R): Promise<LoadedRoleAndUser> {
const role = this.roles[name];
if (!role) {
throw new Error(
`Unknown role: [${name}]. Valid values are: [${Object.keys(this.roles).join(', ')}]`
);
}
const roleName = role.name;
await this.createRole(role);
await this.createUser(roleName, 'changeme', [roleName]);
return {
role: roleName,
username: roleName,
password: 'changeme',
};
}
private async createRole(role: Role): Promise<void> {
const { name: roleName, ...roleDefinition } = role;
this.logger.debug(`creating role:`, roleDefinition);
await this.kbnClient
.request({
method: 'PUT',
path: `/api/security/role/${roleName}`,
body: roleDefinition,
})
.catch(ignoreHttp409Error)
.catch(this.logPromiseError)
.then((response) => {
this.logger.info(`Role [${roleName}] created/updated`, response?.data);
return response;
});
}
private async createUser(
username: string,
password: string,
roles: string[] = []
): Promise<void> {
const user = {
username,
password,
roles,
full_name: username,
email: '',
};
this.logger.debug(`creating user:`, user);
await this.kbnClient
.request({
method: 'POST',
path: `/internal/security/users/${username}`,
body: user,
})
.catch(ignoreHttp409Error)
.catch(this.logPromiseError)
.then((response) => {
this.logger.info(`User [${username}] created/updated`, response?.data);
return response;
});
}
}
export class SecurityRoleAndUserLoader extends RoleAndUserLoader<ServerlessSecurityRoles> {
constructor(kbnClient: KbnClient, logger: ToolingLog) {
super(kbnClient, logger, getServerlessSecurityKibanaRoleDefinitions());
}
}

View file

@ -37,5 +37,12 @@
"@kbn/default-nav-analytics",
"@kbn/default-nav-management",
"@kbn/default-nav-devtools",
"@kbn/security-plugin",
"@kbn/security-solution-plugin",
"@kbn/security-solution-plugin/public/management/cypress",
"@kbn/tooling-log",
"@kbn/fleet-plugin",
"@kbn/cases-plugin",
"@kbn/test-subj-selector",
]
}