Skip to content

Commit

Permalink
[Security Solution] Update serverless FTR tests to not run with opera…
Browse files Browse the repository at this point in the history
…tor privileges (#185870)

## Summary

* ***Create a new service that replaced the serverless `supertest` with
a custom implementation that adds auth headers***
* `username` updates
  * Update `SessionManager` to store `username`
  * Create and export `securitySolutionUtils` to return the `username`
  * Update tests to use the `getUsername` helper
* Create a helper that allows switching serverless roles on a test

```js
export default ({ getService }: FtrProviderContext) => {
   const utils = getService('securitySolutionUtils');

   describe('@ess @serverless my_test', () => {
      let supertest: TestAgent;

      before(async () => {
         supertest = await utils.createSuperTest('admin');
      });
   ...
```
* Update FTR tests [README
file](https://github.com/machadoum/kibana/blob/siem-ea-183512/x-pack/test/security_solution_api_integration/README.md#testing-with-serverless-roles)
with further details

Flaky test runner:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/6320

### Know issues
* Currently `utils.createSuperTest('viewer')` fails on the API creation.
It will be fixed by @elastic/kibana-security
#184948

### Checklist

Delete any items that are not applicable to this PR.

- [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
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
  • Loading branch information
machadoum authored Jun 25, 2024
1 parent 02096a6 commit 131ade8
Show file tree
Hide file tree
Showing 63 changed files with 464 additions and 366 deletions.
40 changes: 38 additions & 2 deletions x-pack/test/security_solution_api_integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,43 @@ In this project, you can run various commands to execute tests and workflows, ea
```shell
npm run initialize-server:dr:default exceptions/workflows ess
```
5. **Run tests for "exception_workflows" using the ess runner in the "essEnv" environment:**
5. **Run tests for "exception_workflows" using the ess runner in the "essEnv" environment:**
```shell
npm run run-tests:dr:default exceptions/workflows ess essEnv
```
```

## Testing with serverless roles

The `supertest` service is logged with the `admin` role by default on serverless. Ideally, every test that runs on serverless should use the most appropriate role.

The `securitySolutionUtils` helper exports the `createSuperTest` function, which accepts the role as a parameter.
You need to call `createSuperTest` from a lifecycle hook and wait for it to return the `supertest` instance.
All API calls using the returned instance will inject the required auth headers.

**On ESS, `createSuperTest` returns a basic `supertest` instance without headers.*

```js
import TestAgent from 'supertest/lib/agent';
export default ({ getService }: FtrProviderContext) => {
const utils = getService('securitySolutionUtils');
describe('@ess @serverless my_test', () => {
let supertest: TestAgent;
before(async () => {
supertest = await utils.createSuperTest('admin');
});
...
```
If you need to use multiple roles in a single test, you can instantiate multiple `supertest` versions.
```js
before(async () => {
adminSupertest = await utils.createSuperTest('admin');
viewerSupertest = await utils.createSuperTest('viewer');
});
...
```
The helper keeps track of only one active session per role. So, if you instantiate `supertest` twice for the same role, the first instance will have an invalid API key.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { CA_CERT_PATH } from '@kbn/dev-utils';
import { FtrConfigProviderContext, kbnTestConfig, kibanaTestUser } from '@kbn/test';
import { services } from '../../../api_integration/services';
import { services } from './services';
import { PRECONFIGURED_ACTION_CONNECTORS } from '../shared';

interface CreateTestConfigOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@

import { SpacesServiceProvider } from '../../../common/services/spaces';
import { services as essServices } from '../../../api_integration/services';
import { SecuritySolutionESSUtils } from '../services/security_solution_ess_utils';

export const services = {
...essServices,
spaces: SpacesServiceProvider,
securitySolutionUtils: SecuritySolutionESSUtils,
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface CreateTestConfigOptions {
kbnTestServerArgs?: string[];
kbnTestServerEnv?: Record<string, string>;
}
import { services } from '../../../../test_serverless/api_integration/services';
import { services } from './services';

export function createTestConfig(options: CreateTestConfigOptions) {
return async ({ readConfigFile }: FtrConfigProviderContext) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
import { SpacesServiceProvider } from '../../../common/services/spaces';
import { BsearchSecureService } from '../../../../test_serverless/shared/services/bsearch_secure';
import { services as serverlessServices } from '../../../../test_serverless/api_integration/services';
import { SecuritySolutionServerlessUtils } from '../services/security_solution_serverless_utils';
import { SecuritySolutionServerlessSuperTest } from '../services/security_solution_serverless_supertest';

export const services = {
...serverlessServices,
spaces: SpacesServiceProvider,
secureBsearch: BsearchSecureService,
securitySolutionUtils: SecuritySolutionServerlessUtils,
supertest: SecuritySolutionServerlessSuperTest,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { FtrProviderContext } from '../../ftr_provider_context';
import { SecuritySolutionUtils } from './types';

export function SecuritySolutionESSUtils({
getService,
}: FtrProviderContext): SecuritySolutionUtils {
const config = getService('config');
const supertest = getService('supertest');

return {
getUsername: (_role?: string) =>
Promise.resolve(config.get('servers.kibana.username') as string),
createSuperTest: (_role?: string) => Promise.resolve(supertest),
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { FtrProviderContext } from '../../ftr_provider_context';

// It is wrapper around supertest that injects Serverless auth headers for the admin user.
export async function SecuritySolutionServerlessSuperTest({ getService }: FtrProviderContext) {
const { createSuperTest } = getService('securitySolutionUtils');

return await createSuperTest('admin');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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 supertest from 'supertest';
import { format as formatUrl } from 'url';
import { RoleCredentials } from '../../../../test_serverless/shared/services';
import { FtrProviderContext } from '../../ftr_provider_context';
import { SecuritySolutionUtils } from './types';

export function SecuritySolutionServerlessUtils({
getService,
}: FtrProviderContext): SecuritySolutionUtils {
const svlUserManager = getService('svlUserManager');
const lifecycle = getService('lifecycle');
const svlCommonApi = getService('svlCommonApi');
const config = getService('config');
const log = getService('log');

const rolesCredentials = new Map<string, RoleCredentials>();
const commonRequestHeader = svlCommonApi.getCommonRequestHeader();
const kbnUrl = formatUrl({
...config.get('servers.kibana'),
auth: false,
});
const agentWithCommonHeaders = supertest.agent(kbnUrl).set(commonRequestHeader);

async function invalidateApiKey(credentials: RoleCredentials) {
await svlUserManager.invalidateApiKeyForRole(credentials);
}

async function cleanCredentials(role: string) {
if (rolesCredentials.has(role)) {
log.debug(`Invalidating API key for role [${role}]`);
await invalidateApiKey(rolesCredentials.get(role)!);
rolesCredentials.delete(role);
}
}

// Invalidate API keys when all tests have finished.
lifecycle.cleanup.add(async () => {
rolesCredentials.forEach((credential, role) => {
log.debug(`Invalidating API key for role [${role}]`);
invalidateApiKey(credential);
});
});

return {
getUsername: async (role = 'admin') => {
const { username } = await svlUserManager.getUserData(role);

return username;
},
/**
* Only one API key for each role can be active at a time.
*/
createSuperTest: async (role = 'admin') => {
cleanCredentials(role);
const credentials = await svlUserManager.createApiKeyForRole(role);
rolesCredentials.set(role, credentials);

return agentWithCommonHeaders.set(credentials.apiKeyHeader);
},
};
}
Original file line number Diff line number Diff line change
@@ -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.
*/

import TestAgent from 'supertest/lib/agent';

export interface SecuritySolutionUtils {
getUsername: (role?: string) => Promise<string>;
createSuperTest: (role?: string) => Promise<TestAgent<any>>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ export default ({ getService }: FtrProviderContext) => {
const supertest = getService('supertest');
const log = getService('log');
const es = getService('es');
const config = getService('config');
const ELASTICSEARCH_USERNAME = config.get('servers.kibana.username');
const utils = getService('securitySolutionUtils');

describe('@serverless @serverlessQA @ess create "rule_default" exceptions', () => {
before(async () => {
Expand All @@ -61,7 +60,7 @@ export default ({ getService }: FtrProviderContext) => {

it('creates and associates a `rule_default` exception list to a rule if one not already found', async () => {
const rule = await createRule(supertest, log, getSimpleRule('rule-2'));

const username = await utils.getUsername();
const { body: items } = await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/${rule.id}/exceptions`)
.set('kbn-xsrf', 'true')
Expand All @@ -82,7 +81,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(itemsWithoutServerGeneratedValues).to.eql([
{
comments: [],
created_by: ELASTICSEARCH_USERNAME,
created_by: username,
description: 'Exception item for rule default exception list',
entries: [
{
Expand All @@ -98,13 +97,14 @@ export default ({ getService }: FtrProviderContext) => {
os_types: [],
tags: [],
type: 'simple',
updated_by: ELASTICSEARCH_USERNAME,
updated_by: username,
},
]);
expect(udpatedRule.exceptions_list.some((list) => list.type === 'rule_default')).to.eql(true);
});

it('creates and associates a `rule_default` exception list to a rule even when rule has non existent default list attached', async () => {
const username = await utils.getUsername();
// create a rule that has a non existent default exception list
const rule = await createRule(supertest, log, {
...getSimpleRule('rule-5'),
Expand Down Expand Up @@ -146,7 +146,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(itemsWithoutServerGeneratedValues).to.eql([
{
comments: [],
created_by: ELASTICSEARCH_USERNAME,
created_by: username,
description: 'Exception item for rule default exception list',
entries: [
{
Expand All @@ -162,12 +162,13 @@ export default ({ getService }: FtrProviderContext) => {
os_types: [],
tags: [],
type: 'simple',
updated_by: ELASTICSEARCH_USERNAME,
updated_by: username,
},
]);
});

it('adds exception items to rule default exception list', async () => {
const username = await utils.getUsername();
// create default exception list
const exceptionList: CreateExceptionListSchema = {
...getCreateExceptionListMinimalSchemaMock(),
Expand Down Expand Up @@ -208,7 +209,7 @@ export default ({ getService }: FtrProviderContext) => {
);
expect(itemsWithoutServerGeneratedValues[0]).to.eql({
comments: [],
created_by: ELASTICSEARCH_USERNAME,
created_by: username,
description: 'Exception item for rule default exception list',
entries: [
{
Expand All @@ -224,7 +225,7 @@ export default ({ getService }: FtrProviderContext) => {
os_types: [],
tags: [],
type: 'simple',
updated_by: ELASTICSEARCH_USERNAME,
updated_by: username,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ export default ({ getService }: FtrProviderContext) => {
const es = getService('es');
const log = getService('log');
const kibanaServer = getService('kibanaServer');
const config = getService('config');
const ELASTICSEARCH_USERNAME = config.get('servers.kibana.username');
const utils = getService('securitySolutionUtils');

const { indexEnhancedDocuments, indexListOfDocuments, indexGeneratedDocuments } =
dataGeneratorFactory({
Expand Down Expand Up @@ -66,6 +65,7 @@ export default ({ getService }: FtrProviderContext) => {
// First test creates a real rule - remaining tests use preview API
it('should generate 1 alert with during actual rule execution', async () => {
const id = uuidv4();
const username = await utils.getUsername();
const interval: [string, string] = ['2020-10-28T06:00:00.000Z', '2020-10-28T06:10:00.000Z'];
const doc1 = { agent: { name: 'test-1' } };
const doc2 = { agent: { name: 'test-2' } };
Expand Down Expand Up @@ -140,7 +140,7 @@ export default ({ getService }: FtrProviderContext) => {
'kibana.alert.risk_score': 55,
'kibana.alert.rule.actions': [],
'kibana.alert.rule.author': [],
'kibana.alert.rule.created_by': ELASTICSEARCH_USERNAME,
'kibana.alert.rule.created_by': username,
'kibana.alert.rule.description': 'Detecting root and admin users',
'kibana.alert.rule.enabled': true,
'kibana.alert.rule.exceptions_list': [],
Expand All @@ -157,7 +157,7 @@ export default ({ getService }: FtrProviderContext) => {
'kibana.alert.rule.threat': [],
'kibana.alert.rule.to': 'now',
'kibana.alert.rule.type': 'esql',
'kibana.alert.rule.updated_by': ELASTICSEARCH_USERNAME,
'kibana.alert.rule.updated_by': username,
'kibana.alert.rule.version': 1,
'kibana.alert.workflow_tags': [],
'kibana.alert.workflow_assignee_ids': [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export default ({ getService }: FtrProviderContext) => {
// TODO: add a new service for loading archiver files similar to "getService('es')"
const config = getService('config');
const isServerless = config.get('serverless');
const ELASTICSEARCH_USERNAME = config.get('servers.kibana.username');
const utils = getService('securitySolutionUtils');
const dataPathBuilder = new EsArchivePathBuilder(isServerless);
const audibeatHostsPath = dataPathBuilder.getPath('auditbeat/hosts');
const threatIntelPath = dataPathBuilder.getPath('filebeat/threat_intel');
Expand Down Expand Up @@ -186,6 +186,7 @@ export default ({ getService }: FtrProviderContext) => {

// First 2 test creates a real rule - remaining tests use preview API
it('should be able to execute and get all alerts when doing a specific query (terms query)', async () => {
const username = await utils.getUsername();
const rule: ThreatMatchRuleCreateProps = createThreatMatchRule();

const createdRule = await createRule(supertest, log, rule);
Expand Down Expand Up @@ -320,7 +321,7 @@ export default ({ getService }: FtrProviderContext) => {
author: [],
category: 'Indicator Match Rule',
consumer: 'siem',
created_by: ELASTICSEARCH_USERNAME,
created_by: username,
description: 'Detecting root and admin users',
enabled: true,
exceptions_list: [],
Expand All @@ -342,13 +343,14 @@ export default ({ getService }: FtrProviderContext) => {
to: 'now',
type: 'threat_match',
updated_at: fullAlert[ALERT_RULE_UPDATED_AT],
updated_by: ELASTICSEARCH_USERNAME,
updated_by: username,
uuid: fullAlert[ALERT_RULE_UUID],
version: 1,
}),
});
});
it('should be able to execute and get all alerts when doing a specific query (match query)', async () => {
const username = await utils.getUsername();
const rule: ThreatMatchRuleCreateProps = createThreatMatchRule({
threat_mapping: [
// We match host.name against host.name
Expand Down Expand Up @@ -499,7 +501,7 @@ export default ({ getService }: FtrProviderContext) => {
author: [],
category: 'Indicator Match Rule',
consumer: 'siem',
created_by: ELASTICSEARCH_USERNAME,
created_by: username,
description: 'Detecting root and admin users',
enabled: true,
exceptions_list: [],
Expand All @@ -521,7 +523,7 @@ export default ({ getService }: FtrProviderContext) => {
to: 'now',
type: 'threat_match',
updated_at: fullAlert[ALERT_RULE_UPDATED_AT],
updated_by: ELASTICSEARCH_USERNAME,
updated_by: username,
uuid: fullAlert[ALERT_RULE_UUID],
version: 1,
}),
Expand Down
Loading

0 comments on commit 131ade8

Please sign in to comment.