Routes and E2E tests for kubernetes_security plugin (#133266)

* aggregate and count routes added for kubernetes_security plugins.
includes FTR e2e tests. some e2e tests also created for session view plugin.

* naming fixes

Co-authored-by: mitodrummer <karlgodard@elastic.co>
This commit is contained in:
Karl Godard 2022-06-01 09:41:18 -07:00 committed by GitHub
parent deb0598c2d
commit 40b1cb95a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 213302 additions and 70 deletions

View file

@ -189,6 +189,7 @@ enabled:
- x-pack/test/functional/config_security_basic.ts
- x-pack/test/functional/config.ccs.ts
- x-pack/test/functional/config.firefox.js
- x-pack/test/kubernetes_security/basic/config.ts
- x-pack/test/licensing_plugin/config.public.ts
- x-pack/test/licensing_plugin/config.ts
- x-pack/test/lists_api_integration/security_and_spaces/config.ts
@ -235,6 +236,7 @@ enabled:
- x-pack/test/security_functional/saml.config.ts
- x-pack/test/security_solution_endpoint_api_int/config.ts
- x-pack/test/security_solution_endpoint/config.ts
- x-pack/test/session_view/basic/config.ts
- x-pack/test/spaces_api_integration/security_and_spaces/config_basic.ts
- x-pack/test/spaces_api_integration/security_and_spaces/config_trial.ts
- x-pack/test/spaces_api_integration/spaces_only/config.ts

View file

@ -6,3 +6,11 @@
*/
export const KUBERNETES_PATH = '/kubernetes' as const;
export const AGGREGATE_ROUTE = '/internal/kubernetes_security/aggregate';
export const COUNT_ROUTE = '/internal/kubernetes_security/count';
export const AGGREGATE_PAGE_SIZE = 10;
// so, bucket sort can only page through what we request at the top level agg, which means there is a ceiling to how many aggs we can page through.
// we should also test this approach at scale.
export const AGGREGATE_MAX_BUCKETS = 2000;

View file

@ -9,7 +9,8 @@
"requiredPlugins": [
"data",
"timelines",
"ruleRegistry"
"ruleRegistry",
"sessionView"
],
"requiredBundles": [],
"server": true,

View file

@ -0,0 +1,82 @@
/*
* 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 { schema } from '@kbn/config-schema';
import type { ElasticsearchClient } from '@kbn/core/server';
import { IRouter } from '@kbn/core/server';
import { PROCESS_EVENTS_INDEX } from '@kbn/session-view-plugin/common/constants';
import {
AGGREGATE_ROUTE,
AGGREGATE_PAGE_SIZE,
AGGREGATE_MAX_BUCKETS,
} from '../../common/constants';
export const registerAggregateRoute = (router: IRouter) => {
router.get(
{
path: AGGREGATE_ROUTE,
validate: {
query: schema.object({
query: schema.string(),
groupBy: schema.string(),
page: schema.number(),
index: schema.maybe(schema.string()),
}),
},
},
async (context, request, response) => {
const client = (await context.core).elasticsearch.client.asCurrentUser;
const { query, groupBy, page, index } = request.query;
try {
const body = await doSearch(client, query, groupBy, page, index);
return response.ok({ body });
} catch (err) {
return response.badRequest(err.message);
}
}
);
};
export const doSearch = async (
client: ElasticsearchClient,
query: string,
groupBy: string,
page: number, // zero based
index?: string
) => {
const queryDSL = JSON.parse(query);
const search = await client.search({
index: [index || PROCESS_EVENTS_INDEX],
body: {
query: queryDSL,
size: 0,
aggs: {
custom_agg: {
terms: {
field: groupBy,
size: AGGREGATE_MAX_BUCKETS,
},
aggs: {
bucket_sort: {
bucket_sort: {
sort: [{ _key: { order: 'asc' } }], // defaulting to alphabetic sort
size: AGGREGATE_PAGE_SIZE,
from: AGGREGATE_PAGE_SIZE * page,
},
},
},
},
},
},
});
const agg: any = search.aggregations?.custom_agg;
return agg?.buckets || [];
};

View file

@ -0,0 +1,66 @@
/*
* 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 { schema } from '@kbn/config-schema';
import type { ElasticsearchClient } from '@kbn/core/server';
import { IRouter } from '@kbn/core/server';
import { PROCESS_EVENTS_INDEX } from '@kbn/session-view-plugin/common/constants';
import { COUNT_ROUTE } from '../../common/constants';
export const registerCountRoute = (router: IRouter) => {
router.get(
{
path: COUNT_ROUTE,
validate: {
query: schema.object({
query: schema.string(),
field: schema.string(),
index: schema.maybe(schema.string()),
}),
},
},
async (context, request, response) => {
const client = (await context.core).elasticsearch.client.asCurrentUser;
const { query, field, index } = request.query;
try {
const body = await doCount(client, query, field, index);
return response.ok({ body });
} catch (err) {
return response.badRequest(err.message);
}
}
);
};
export const doCount = async (
client: ElasticsearchClient,
query: string,
field: string,
index?: string
) => {
const queryDSL = JSON.parse(query);
const search = await client.search({
index: [index || PROCESS_EVENTS_INDEX],
body: {
query: queryDSL,
size: 0,
aggs: {
custom_count: {
cardinality: {
field,
},
},
},
},
});
const agg: any = search.aggregations?.custom_count;
return agg?.value || 0;
};

View file

@ -6,7 +6,10 @@
*/
import { IRouter } from '@kbn/core/server';
import { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server';
import { registerAggregateRoute } from './aggregate';
import { registerCountRoute } from './count';
export const registerRoutes = (router: IRouter, ruleRegistry: RuleRegistryPluginStartContract) => {
// register new routes here
registerAggregateRoute(router);
registerCountRoute(router);
};

View file

@ -38,6 +38,7 @@
{ "path": "../../../src/plugins/es_ui_shared/tsconfig.json" },
{ "path": "../infra/tsconfig.json" },
{ "path": "../../../src/plugins/kibana_utils/tsconfig.json" },
{ "path": "../rule_registry/tsconfig.json" }
{ "path": "../rule_registry/tsconfig.json" },
{ "path": "../session_view/tsconfig.json" }
]
}

View file

@ -9,7 +9,7 @@ export const PROCESS_EVENTS_ROUTE = '/internal/session_view/process_events_route
export const ALERTS_ROUTE = '/internal/session_view/alerts_route';
export const ALERT_STATUS_ROUTE = '/internal/session_view/alert_status_route';
export const SESSION_ENTRY_LEADERS_ROUTE = '/internal/session_view/session_entry_leaders_route';
export const PROCESS_EVENTS_INDEX = 'logs-endpoint.events.process-*';
export const PROCESS_EVENTS_INDEX = 'logs-endpoint.events.process*';
export const PREVIEW_ALERTS_INDEX = '.preview.alerts-security.alerts-default';
export const ENTRY_SESSION_ENTITY_ID_PROPERTY = 'process.entry_leader.entity_id';
export const ALERT_UUID_PROPERTY = 'kibana.alert.uuid';

View file

@ -40,16 +40,21 @@ export const registerAlertsRoute = (
async (_context, request, response) => {
const client = await ruleRegistry.getRacClientWithRequest(request);
const { sessionEntityId, investigatedAlertId, range, cursor } = request.query;
const body = await searchAlerts(
client,
sessionEntityId,
ALERTS_PER_PAGE,
investigatedAlertId,
range,
cursor
);
return response.ok({ body });
try {
const body = await searchAlerts(
client,
sessionEntityId,
ALERTS_PER_PAGE,
investigatedAlertId,
range,
cursor
);
return response.ok({ body });
} catch (err) {
return response.badRequest(err.message);
}
}
);
};
@ -70,60 +75,69 @@ export const searchAlerts = async (
return { events: [] };
}
const results = await client.find({
query: {
bool: {
must: [
{
term: {
[ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId,
},
},
range && {
range: {
[ALERT_ORIGINAL_TIME_PROPERTY]: {
gte: range[0],
lte: range[1],
try {
const results = await client.find({
query: {
bool: {
must: [
{
term: {
[ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId,
},
},
},
].filter((item) => !!item),
},
},
track_total_hits: true,
size,
index: indices.join(','),
sort: [{ '@timestamp': 'asc' }],
search_after: cursor ? [cursor] : undefined,
});
// if an alert is being investigated, fetch it on it's own, as it's not guaranteed to come back in the above request.
// we only need to do this for the first page of alerts.
if (!cursor && investigatedAlertId) {
const investigatedAlertSearch = await client.find({
query: {
match: {
[ALERT_UUID_PROPERTY]: investigatedAlertId,
range && {
range: {
[ALERT_ORIGINAL_TIME_PROPERTY]: {
gte: range[0],
lte: range[1],
},
},
},
].filter((item) => !!item),
},
},
size: 1,
track_total_hits: true,
size,
index: indices.join(','),
sort: [{ '@timestamp': 'asc' }],
search_after: cursor ? [cursor] : undefined,
});
if (investigatedAlertSearch.hits.hits.length > 0) {
results.hits.hits.unshift(investigatedAlertSearch.hits.hits[0]);
// if an alert is being investigated, fetch it on it's own, as it's not guaranteed to come back in the above request.
// we only need to do this for the first page of alerts.
if (!cursor && investigatedAlertId) {
const investigatedAlertSearch = await client.find({
query: {
match: {
[ALERT_UUID_PROPERTY]: investigatedAlertId,
},
},
size: 1,
index: indices.join(','),
});
if (investigatedAlertSearch.hits.hits.length > 0) {
results.hits.hits.unshift(investigatedAlertSearch.hits.hits[0]);
}
}
const events = results.hits.hits.map((hit: any) => {
// the alert indexes flattens many properties. this util unflattens them as session view expects structured json.
hit._source = expandDottedObject(hit._source);
return hit;
});
const total =
typeof results.hits.total === 'number' ? results.hits.total : results.hits.total?.value;
return { total, events };
} catch (err) {
// unauthorized
if (err.output.statusCode === 404) {
return { total: 0, events: [] };
}
throw err;
}
const events = results.hits.hits.map((hit: any) => {
// the alert indexes flattens many properties. this util unflattens them as session view expects structured json.
hit._source = expandDottedObject(hit._source);
return hit;
});
const total =
typeof results.hits.total === 'number' ? results.hits.total : results.hits.total?.value;
return { total, events };
};

View file

@ -40,16 +40,26 @@ export const registerProcessEventsRoute = (
async (context, request, response) => {
const client = (await context.core).elasticsearch.client.asCurrentUser;
const alertsClient = await ruleRegistry.getRacClientWithRequest(request);
const { sessionEntityId, cursor, forward = true } = request.query;
const body = await fetchEventsAndScopedAlerts(
client,
alertsClient,
sessionEntityId,
cursor,
forward
);
const { sessionEntityId, cursor, forward } = request.query;
return response.ok({ body });
try {
const body = await fetchEventsAndScopedAlerts(
client,
alertsClient,
sessionEntityId,
cursor,
forward
);
return response.ok({ body });
} catch (err) {
// unauthorized
if (err.meta.statusCode === 403) {
return response.ok({ body: { total: 0, events: [] } });
}
return response.badRequest(err.message);
}
}
);
};
@ -58,7 +68,7 @@ export const fetchEventsAndScopedAlerts = async (
client: ElasticsearchClient,
alertsClient: AlertsClient,
sessionEntityId: string,
cursor: string | undefined,
cursor?: string,
forward = true
) => {
const cursorMillis = cursor && new Date(cursor).getTime() + (forward ? -1 : 1);

View file

@ -0,0 +1,200 @@
{
"type": "doc",
"value": {
"index": "kubernetes-test-index",
"id": "1",
"source": {
"event.kind" : "event",
"@timestamp": "2020-12-16T15:16:18.570Z",
"message": "hello world 1",
"orchestrator.namespace": "namespace",
"container.image.name": "debian11"
}
}
}
{
"type": "doc",
"value": {
"index": "kubernetes-test-index",
"id": "2",
"source": {
"event.kind" : "event",
"@timestamp": "2020-12-16T15:16:18.570Z",
"message": "hello world 1",
"orchestrator.namespace": "namespace",
"container.image.name": "debian11"
}
}
}
{
"type": "doc",
"value": {
"index": "kubernetes-test-index",
"id": "3",
"source": {
"event.kind" : "event",
"@timestamp": "2020-12-16T15:16:19.570Z",
"message": "hello world 1",
"orchestrator.namespace": "namespace02",
"container.image.name": "debian11"
}
}
}
{
"type": "doc",
"value": {
"index": "kubernetes-test-index",
"id": "4",
"source": {
"event.kind" : "event",
"@timestamp": "2020-12-16T15:16:20.570Z",
"message": "hello world security",
"orchestrator.namespace": "namespace02",
"container.image.name": "debian11"
}
}
}
{
"type": "doc",
"value": {
"index": "kubernetes-test-index",
"id": "5",
"source": {
"event.kind" : "event",
"@timestamp": "2020-12-16T15:16:21.570Z",
"message": "hello world security",
"orchestrator.namespace": "namespace03",
"container.image.name": "debian11"
}
}
}
{
"type": "doc",
"value": {
"index": "kubernetes-test-index",
"id": "6",
"source": {
"@timestamp": "2020-12-16T15:16:22.570Z",
"message": "hello world security",
"orchestrator.namespace": "namespace03",
"container.image.name": "debian11"
}
}
}
{
"type": "doc",
"value": {
"index": "kubernetes-test-index",
"id": "7",
"source": {
"@timestamp": "2020-12-16T15:16:23.570Z",
"message": "hello world security",
"orchestrator.namespace": "namespace04",
"container.image.name": "debian11"
}
}
}
{
"type": "doc",
"value": {
"index": "kubernetes-test-index",
"id": "8",
"source": {
"@timestamp": "2020-12-16T15:16:24.570Z",
"message": "hello world security",
"orchestrator.namespace": "namespace05",
"container.image.name": "debian11"
}
}
}
{
"type": "doc",
"value": {
"index": "kubernetes-test-index",
"id": "9",
"source": {
"@timestamp": "2020-12-16T15:16:25.570Z",
"message": "hello world security",
"orchestrator.namespace": "namespace06",
"container.image.name": "debian11"
}
}
}
{
"type": "doc",
"value": {
"index": "kubernetes-test-index",
"id": "10",
"source": {
"@timestamp": "2020-12-16T15:16:26.570Z",
"message": "hello world security",
"orchestrator.namespace": "namespace07",
"container.image.name": "debian11"
}
}
}
{
"type": "doc",
"value": {
"index": "kubernetes-test-index",
"id": "11",
"source": {
"@timestamp": "2020-12-16T15:16:27.570Z",
"message": "hello world security",
"orchestrator.namespace": "namespace08",
"container.image.name": "debian11"
}
}
}
{
"type": "doc",
"value": {
"index": "kubernetes-test-index",
"id": "12",
"source": {
"@timestamp": "2020-12-16T15:16:28.570Z",
"message": "hello world security",
"orchestrator.namespace": "namespace09",
"container.image.name": "debian11"
}
}
}
{
"type": "doc",
"value": {
"index": "kubernetes-test-index",
"id": "13",
"source": {
"@timestamp": "2020-12-16T15:16:29.570Z",
"message": "hello world security",
"orchestrator.namespace": "namespace10",
"container.image.name": "debian11"
}
}
}
{
"type": "doc",
"value": {
"index": "kubernetes-test-index",
"id": "14",
"source": {
"@timestamp": "2020-12-16T15:16:30.570Z",
"message": "hello world security",
"orchestrator.namespace": "namespace11",
"container.image.name": "debian11"
}
}
}

View file

@ -0,0 +1,28 @@
{
"type": "index",
"value": {
"index": "kubernetes-test-index",
"mappings": {
"properties": {
"message": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"orchestrator.namespace": {
"type": "keyword",
"ignore_above": 256
},
"container.image.name": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,24 @@
{
"type": "index",
"value": {
"index": "logs-endpoint.events.process",
"mappings": {
"properties": {
"message": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"process.entry_leader.entity_id": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}

View file

@ -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 { createTestConfig } from '../common/config';
// eslint-disable-next-line import/no-default-export
export default createTestConfig({
license: 'basic',
name: 'X-Pack kubernetes_security API integration tests (basic)',
testFiles: [require.resolve('./tests')],
});

View file

@ -0,0 +1,99 @@
/*
* 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 expect from '@kbn/expect';
import { AGGREGATE_ROUTE } from '@kbn/kubernetes-security-plugin/common/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
const MOCK_INDEX = 'kubernetes-test-index';
const ORCHESTRATOR_NAMESPACE_PROPERTY = 'orchestrator.namespace';
const CONTAINER_IMAGE_NAME_PROPERTY = 'container.image.name';
const TIMESTAMP_PROPERTY = '@timestamp';
// eslint-disable-next-line import/no-default-export
export default function aggregateTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const namespaces = ['namespace', 'namespace02', 'namespace03', 'namespace04'];
describe('Kubernetes security with a basic license', () => {
before(async () => {
await esArchiver.load(
'x-pack/test/functional/es_archives/kubernetes_security/process_events'
);
});
after(async () => {
await esArchiver.unload(
'x-pack/test/functional/es_archives/kubernetes_security/process_events'
);
});
it(`${AGGREGATE_ROUTE} returns aggregates on process events`, async () => {
const response = await supertest
.get(AGGREGATE_ROUTE)
.set('kbn-xsrf', 'foo')
.query({
query: JSON.stringify({ match: { [CONTAINER_IMAGE_NAME_PROPERTY]: 'debian11' } }),
groupBy: ORCHESTRATOR_NAMESPACE_PROPERTY,
page: 0,
index: MOCK_INDEX,
});
expect(response.status).to.be(200);
expect(response.body.length).to.be(10);
namespaces.forEach((namespace, i) => {
expect(response.body[i].key).to.be(namespace);
});
});
it(`${AGGREGATE_ROUTE} allows pagination`, async () => {
const response = await supertest
.get(AGGREGATE_ROUTE)
.set('kbn-xsrf', 'foo')
.query({
query: JSON.stringify({ match: { [CONTAINER_IMAGE_NAME_PROPERTY]: 'debian11' } }),
groupBy: ORCHESTRATOR_NAMESPACE_PROPERTY,
page: 1,
index: MOCK_INDEX,
});
expect(response.status).to.be(200);
expect(response.body.length).to.be(1);
expect(response.body[0].key).to.be('namespace11');
});
it(`${AGGREGATE_ROUTE} allows a range query`, async () => {
const response = await supertest
.get(AGGREGATE_ROUTE)
.set('kbn-xsrf', 'foo')
.query({
query: JSON.stringify({
range: {
[TIMESTAMP_PROPERTY]: {
gte: '2020-12-16T15:16:28.570Z',
lte: '2020-12-16T15:16:30.570Z',
},
},
}),
groupBy: ORCHESTRATOR_NAMESPACE_PROPERTY,
page: 0,
index: MOCK_INDEX,
});
expect(response.status).to.be(200);
expect(response.body.length).to.be(3);
});
it(`${AGGREGATE_ROUTE} handles a bad request`, async () => {
const response = await supertest.get(AGGREGATE_ROUTE).set('kbn-xsrf', 'foo').query({
query: 'asdf',
groupBy: ORCHESTRATOR_NAMESPACE_PROPERTY,
page: 0,
index: MOCK_INDEX,
});
expect(response.status).to.be(400);
});
});
}

View file

@ -0,0 +1,82 @@
/*
* 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 expect from '@kbn/expect';
import { COUNT_ROUTE } from '@kbn/kubernetes-security-plugin/common/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
const MOCK_INDEX = 'kubernetes-test-index';
const ORCHESTRATOR_NAMESPACE_PROPERTY = 'orchestrator.namespace';
const CONTAINER_IMAGE_NAME_PROPERTY = 'container.image.name';
const TIMESTAMP_PROPERTY = '@timestamp';
// eslint-disable-next-line import/no-default-export
export default function countTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
describe('Kubernetes security with a basic license', () => {
before(async () => {
await esArchiver.load(
'x-pack/test/functional/es_archives/kubernetes_security/process_events'
);
});
after(async () => {
await esArchiver.unload(
'x-pack/test/functional/es_archives/kubernetes_security/process_events'
);
});
it(`${COUNT_ROUTE} returns cardinality of a field`, async () => {
const response = await supertest
.get(COUNT_ROUTE)
.set('kbn-xsrf', 'foo')
.query({
query: JSON.stringify({ match: { [CONTAINER_IMAGE_NAME_PROPERTY]: 'debian11' } }),
field: ORCHESTRATOR_NAMESPACE_PROPERTY,
index: MOCK_INDEX,
});
expect(response.status).to.be(200);
expect(response.body).to.be(11);
});
it(`${COUNT_ROUTE} allows a range query`, async () => {
const response = await supertest
.get(COUNT_ROUTE)
.set('kbn-xsrf', 'foo')
.query({
query: JSON.stringify({
range: {
[TIMESTAMP_PROPERTY]: {
gte: '2020-12-16T15:16:28.570Z',
lte: '2020-12-16T15:16:30.570Z',
},
},
}),
field: ORCHESTRATOR_NAMESPACE_PROPERTY,
index: MOCK_INDEX,
});
expect(response.status).to.be(200);
expect(response.body).to.be(3);
});
it(`${COUNT_ROUTE} handles a bad query`, async () => {
const response = await supertest
.get(COUNT_ROUTE)
.set('kbn-xsrf', 'foo')
.query({
query: JSON.stringify({
range: 'asdf',
}),
field: ORCHESTRATOR_NAMESPACE_PROPERTY,
index: MOCK_INDEX,
});
expect(response.status).to.be(400);
});
});
}

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrProviderContext } from '../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function kubernetesSecurityApiIntegrationTests({
loadTestFile,
}: FtrProviderContext) {
describe('Kubernetes security API (basic)', function () {
loadTestFile(require.resolve('./aggregate'));
loadTestFile(require.resolve('./count'));
});
}

View file

@ -0,0 +1,39 @@
/*
* 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 { FtrConfigProviderContext } from '@kbn/test';
interface Settings {
license: 'basic' | 'trial';
testFiles: string[];
name: string;
}
export function createTestConfig(settings: Settings) {
const { testFiles, license, name } = settings;
return async ({ readConfigFile }: FtrConfigProviderContext) => {
const xPackAPITestsConfig = await readConfigFile(
require.resolve('../../api_integration/config.ts')
);
return {
testFiles,
servers: xPackAPITestsConfig.get('servers'),
services: xPackAPITestsConfig.get('services'),
junit: {
reportName: name,
},
esTestCluster: {
...xPackAPITestsConfig.get('esTestCluster'),
license,
},
kbnTestServer: xPackAPITestsConfig.get('kbnTestServer'),
};
};
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export type { FtrProviderContext } from '../../api_integration/ftr_provider_context';

View file

@ -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 { createTestConfig } from '../common/config';
// eslint-disable-next-line import/no-default-export
export default createTestConfig({
license: 'basic',
name: 'X-Pack session_view API integration tests (basic)',
testFiles: [require.resolve('./tests')],
});

View file

@ -0,0 +1,114 @@
/*
* 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 '../../common/ftr_provider_context';
import {
createUsersAndRoles,
deleteUsersAndRoles,
} from '../../../rule_registry/common/lib/authentication';
import {
superUser,
globalRead,
secOnlyReadSpacesAll,
obsOnlySpacesAll,
noKibanaPrivileges,
} from '../../../rule_registry/common/lib/authentication/users';
import { noKibanaPrivileges as noKibanaPrivilegesRole } from '../../../rule_registry/common/lib/authentication/roles';
import { Role } from '../../../rule_registry/common/lib/authentication/types';
const globalReadRole: Role = {
name: 'global_read',
privileges: {
elasticsearch: {
indices: [
{
privileges: ['all'],
names: ['logs-*'],
},
],
},
kibana: [
{
base: ['read'],
spaces: ['*'],
},
],
},
};
export const securitySolutionOnlyReadSpacesAll: Role = {
name: 'sec_only_read_spaces_all',
privileges: {
elasticsearch: {
indices: [
{
privileges: ['all'],
names: ['logs-*'],
},
],
},
kibana: [
{
feature: {
siem: ['read'],
},
spaces: ['*'],
},
],
},
};
export const observabilityOnlyAllSpacesAll: Role = {
name: 'obs_only_all_spaces_all',
privileges: {
elasticsearch: {
indices: [
{
privileges: ['all'],
names: ['logs-*'],
},
],
},
kibana: [
{
feature: {
apm: ['all'],
},
spaces: ['*'],
},
],
},
};
const users = [superUser, globalRead, secOnlyReadSpacesAll, obsOnlySpacesAll, noKibanaPrivileges];
const roles = [
globalReadRole,
securitySolutionOnlyReadSpacesAll,
observabilityOnlyAllSpacesAll,
noKibanaPrivilegesRole,
];
// eslint-disable-next-line import/no-default-export
export default function kubernetesSecurityApiIntegrationTests({
loadTestFile,
getService,
}: FtrProviderContext) {
describe('Session View API (basic)', function () {
before(async () => {
await createUsersAndRoles(getService, users, roles);
});
after(async () => {
await deleteUsersAndRoles(getService, users, roles);
});
loadTestFile(require.resolve('./process_events_route'));
});
}

View file

@ -0,0 +1,160 @@
/*
* 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 expect from '@kbn/expect';
import {
PROCESS_EVENTS_ROUTE,
PROCESS_EVENTS_PER_PAGE,
} from '@kbn/session-view-plugin/common/constants';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { User } from '../../../rule_registry/common/lib/authentication/types';
import {
superUser,
globalRead,
secOnlyReadSpacesAll,
obsOnlySpacesAll,
noKibanaPrivileges,
} from '../../../rule_registry/common/lib/authentication/users';
const MOCK_SESSION_ENTITY_ID =
'MDEwMTAxMDEtMDEwMS0wMTAxLTAxMDEtMDEwMTAxMDEwMTAxLTUyMDU3LTEzMjk2NDkxMDQwLjEzMDAwMDAwMA==';
interface TestCase {
/** The ID of the alert */
authorizedUsers: User[];
/** Unauthorized users */
unauthorizedUsers: User[];
}
// eslint-disable-next-line import/no-default-export
export default function processEventsTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const esArchiver = getService('esArchiver');
describe(`Session view - ${PROCESS_EVENTS_ROUTE} - with a basic license`, () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/session_view/process_events');
await esArchiver.load('x-pack/test/functional/es_archives/session_view/alerts');
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/session_view/process_events');
await esArchiver.unload('x-pack/test/functional/es_archives/session_view/alerts');
});
it(`${PROCESS_EVENTS_ROUTE} returns a page of process events`, async () => {
const response = await supertest.get(PROCESS_EVENTS_ROUTE).set('kbn-xsrf', 'foo').query({
sessionEntityId: MOCK_SESSION_ENTITY_ID,
});
expect(response.status).to.be(200);
expect(response.body.total).to.be(504);
expect(response.body.events.length).to.be(PROCESS_EVENTS_PER_PAGE);
});
it(`${PROCESS_EVENTS_ROUTE} returns a page of process events (w alerts) (paging forward)`, async () => {
const response = await supertest.get(PROCESS_EVENTS_ROUTE).set('kbn-xsrf', 'foo').query({
sessionEntityId: MOCK_SESSION_ENTITY_ID,
cursor: '2022-05-10T20:39:23.6817084Z', // paginating from the timestamp of the first alert.
});
expect(response.status).to.be(200);
const alerts = response.body.events.filter(
(event: any) => event._source.event.kind === 'signal'
);
expect(alerts.length).to.above(0);
});
it(`${PROCESS_EVENTS_ROUTE} returns a page of process events (w alerts) (paging backwards)`, async () => {
const response = await supertest.get(PROCESS_EVENTS_ROUTE).set('kbn-xsrf', 'foo').query({
sessionEntityId: MOCK_SESSION_ENTITY_ID,
cursor: '2022-05-10T20:39:23.6817084Z',
forward: false,
});
expect(response.status).to.be(200);
const alerts = response.body.events.filter(
(event: any) => event._source.event.kind === 'signal'
);
expect(alerts.length).to.be(1); // only one since we are starting at the cursor of the first alert in the esarchiver data, and working backwards.
const events = response.body.events.filter(
(event: any) => event._source.event.kind === 'event'
);
expect(events[0]._source['@timestamp']).to.be.below(
events[events.length - 1]._source['@timestamp']
);
});
function addTests({ authorizedUsers, unauthorizedUsers }: TestCase) {
authorizedUsers.forEach(({ username, password }) => {
it(`${username} should be able to view alerts in session view`, async () => {
const response = await supertestWithoutAuth
.get(`${PROCESS_EVENTS_ROUTE}`)
.auth(username, password)
.set('kbn-xsrf', 'true')
.query({
sessionEntityId: MOCK_SESSION_ENTITY_ID,
cursor: '2022-05-10T20:39:23.6817084Z', // paginating from the timestamp of the first alert.
});
expect(response.status).to.be(200);
const alerts = response.body.events.filter(
(event: any) => event._source.event.kind === 'signal'
);
expect(alerts.length).to.above(0);
});
});
unauthorizedUsers.forEach(({ username, password }) => {
it(`${username} should NOT be able to view alerts in session view`, async () => {
const response = await supertestWithoutAuth
.get(`${PROCESS_EVENTS_ROUTE}`)
.auth(username, password)
.set('kbn-xsrf', 'true')
.query({
sessionEntityId: MOCK_SESSION_ENTITY_ID,
cursor: '2022-05-10T20:39:23.6817084Z', // paginating from the timestamp of the first alert.
});
expect(response.status).to.be(200);
if (username === 'no_kibana_privileges') {
expect(response.body.events.length).to.be.equal(0);
} else {
// process events should still load (since logs-* is granted, except for no_kibana_privileges user)
expect(response.body.events.length).to.be.above(0);
}
const alerts = response.body.events.filter(
(event: any) => event._source.event.kind === 'signal'
);
expect(alerts.length).to.be(0);
});
});
}
describe('Session View', () => {
const authorizedInAllSpaces = [superUser, globalRead, secOnlyReadSpacesAll];
const unauthorized = [
// these users are not authorized to get alerts for session view
obsOnlySpacesAll,
noKibanaPrivileges,
];
addTests({
authorizedUsers: [...authorizedInAllSpaces],
unauthorizedUsers: [...unauthorized],
});
});
});
}

View file

@ -0,0 +1,39 @@
/*
* 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 { FtrConfigProviderContext } from '@kbn/test';
interface Settings {
license: 'basic' | 'trial';
testFiles: string[];
name: string;
}
export function createTestConfig(settings: Settings) {
const { testFiles, license, name } = settings;
return async ({ readConfigFile }: FtrConfigProviderContext) => {
const xPackAPITestsConfig = await readConfigFile(
require.resolve('../../api_integration/config.ts')
);
return {
testFiles,
servers: xPackAPITestsConfig.get('servers'),
services: xPackAPITestsConfig.get('services'),
junit: {
reportName: name,
},
esTestCluster: {
...xPackAPITestsConfig.get('esTestCluster'),
license,
},
kbnTestServer: xPackAPITestsConfig.get('kbnTestServer'),
};
};
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export type { FtrProviderContext } from '../../api_integration/ftr_provider_context';