mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
e2db0b0e66
commit
733e19c5c0
69 changed files with 3151 additions and 256 deletions
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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']>
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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());
|
||||
};
|
|
@ -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());
|
||||
};
|
||||
|
|
|
@ -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 }
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -166,5 +166,7 @@
|
|||
"@kbn/discover-plugin",
|
||||
"@kbn/data-view-editor-plugin",
|
||||
"@kbn/navigation-plugin",
|
||||
"@kbn/alerts-ui-shared",
|
||||
"@kbn/core-logging-server-mocks",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -54,7 +54,7 @@ export class SecuritySolutionServerlessPlugin
|
|||
|
||||
securitySolution.setGetStartedPage(getSecurityGetStartedComponent(services, productTypes));
|
||||
|
||||
configureNavigation(services);
|
||||
configureNavigation(services, this.config);
|
||||
setRoutes(services);
|
||||
|
||||
return {};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 }) => [
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
207
x-pack/test_serverless/functional/test_suites/security/cypress/cypress.d.ts
vendored
Normal file
207
x-pack/test_serverless/functional/test_suites/security/cypress/cypress.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
);
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
);
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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/"
|
||||
|
|
|
@ -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');
|
||||
};
|
|
@ -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');
|
||||
};
|
|
@ -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();
|
||||
};
|
|
@ -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';
|
|
@ -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;
|
||||
};
|
|
@ -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');
|
||||
};
|
|
@ -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();
|
||||
};
|
|
@ -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';
|
|
@ -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');
|
||||
};
|
|
@ -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';
|
|
@ -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');
|
||||
};
|
|
@ -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';
|
|
@ -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')
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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';
|
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -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');
|
||||
};
|
||||
|
|
|
@ -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');
|
|
@ -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');
|
|
@ -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');
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
4
x-pack/test_serverless/shared/lib/README.md
Normal file
4
x-pack/test_serverless/shared/lib/README.md
Normal 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.
|
||||
|
8
x-pack/test_serverless/shared/lib/index.ts
Normal file
8
x-pack/test_serverless/shared/lib/index.ts
Normal 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';
|
8
x-pack/test_serverless/shared/lib/security/index.ts
Normal file
8
x-pack/test_serverless/shared/lib/security/index.ts
Normal 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';
|
|
@ -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';
|
|
@ -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);
|
||||
};
|
|
@ -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: "*"
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue