Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Update serverless FTR tests to not run with operator privileges #185870

Merged
merged 14 commits into from
Jun 25, 2024
Merged
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;
machadoum marked this conversation as resolved.
Show resolved Hide resolved

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) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _role param seems to be unused

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, there aren't roles on ESS. But we need the same method signature so that it can be called from ESS and Serverless. I used the _ before the parameter to sign that it is unused.

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';
machadoum marked this conversation as resolved.
Show resolved Hide resolved

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