[SIEM] Migrate backend to use New Platform services (#51144)

* Mark incoming plugin members as readonly

These cannot and should not be modifiable.

* Use env var instead of EnvironmentMode

* There doesn't appear to be an EnvMode in the new platform
* We're only using envMode to check whether we're in production
* We're already using process.env.NODE_ENV elsewhere

We can revisit this, but for now I'm simplifying things under this
assumption.

* Pass our setup context to the compose function

We're going to retrieve our router instance from this, for now.

* Remove unused static files route

I spent a few minutes trying to do this in the new platform, only to
realize that this was cargo culted from another plugin's structure and
never used.

* WIP: convert main GraphQL endpoints to New Platform

Splits the existing dual-method route into separate GET/POST
routes, while converting it to the NP routing syntax

TODO:
* Full route schema declarations
* Address context being moved off of the response object and into its
own object; callWithRequest is currently broken for this reason.

* Remove unnecesary Request type

While the defaultIndex patterns can be retrieved on the request itself,
that requires this special case of our FrameworkRequest.

In my smoke testing, the incoming `indices` argument was never different from
the one present on the request payload. Xavier had mentioned that these
might be redundant and a relic of some quick prototyping, so I'm going
to simplify this logic and delete that type under this assumption.

* Retrieve Elasticsearch client from RequestHandlerContext

In order to minimize the amount of noise on this refactor, I'm adding
the RequestHandlerContext to the existing FrameworkRequest object that
we already pass around.

This also removes some adapter methods that were cribbed from infra but
have since become unused. There are likely more.

* Use uiSettings client from RequestHandlerContext

Pulls from the new platform instead of from request.server.

* Remove unused properties from RequestFacade

One of these was obviated by the refactor to NP routing; the other may
never have been necessary.

* Remove unused interface

This is a relic that is no longer used in the codebase.

* Make error response code dynamic

* Handle GraphQL errors

Refactors to use new platform's responses instead of Boom.

Unless we intentionally do not want isGraphQLError error headers, I saw no
reason for the latter two branches of this method (and merged them).

* Fix graphiQL route

We needed to loosen the restriction on our main POST graphQL route, as
the requests coming from graphiQL do not match our normal format.

* Clean up logging

* Remove unused var injection functionality

I could not find a case where we were using these vars within the siem
app.

* Fix typo on config fetching

* Migrate to NP IndexPatterns service

* Removes unused extra parameter on callWithRequest
  * I think this was a relic from the infra code
* Clean up typings of callWithRequest
  * GenericParams is, ironically, not generic enough to handle all ES
  client calls. Instead we type it as Record<string, any> but ensure
  that our function adheres to the APICaller interface.

* Use savedObjects client in request context

These resolvers already receive a request containing the NP context, so
we can retrieve our client directly from that, now.

* Rename dependencies -> plugins to match kibana.json

* Remove unnecessary type annotation

The type of callCluster is already checked due to being passed to the
IndexPatternsFetcher constructor.

* Add siem plugin to new platform

For now this just generates a config observable with some defaults;
everything still lives in the legacy plugin.

* WIP: flattening out plugin initialization

Rather than pass our legacy API around everywhere, let's be explicit
about who needs what, and start flattening things out so that we can
move the legacy-independent stuff over.

* Pass our plugin context to initServerWithKibana

We can get the NP equivalent of `pkg.version` from
context.env.packageInfo.version, so let's do that and remove a usage of
config().

* Simplify siem configuration

As far as I can tell, the only siem config that we're using is
`xpack.siem.enabled`. The `query` was a holdover from infra, and if
we're using the `sources` queries at all, it's only with the default
values. Since our config is not typed, trying to add `sources` config
only results in runtime errors.

This removes the KibanaConfigurationAdapter entirely, and instead passes
what is effectively { sources: {} } to the SourcesConfigurationAdapter.

* Run all legacy-free setup through our plugin

Once this is vetted, we should be able to move the entire tree under the
plugin into the new platform plugin. We can inline the compose and
init_server calls into the plugin once things are vetted and stable; for
now leaving them there cuts down on the diff.

* Temporarily ignore our unused config declaration

* Fix detection engine route tests

While we're passing a properly bound route function in the app, the
tests' interfaces needed to be updated. Adds a helper method for
retrieving a bound route function from a Server object.

* Add some rudimentary schema validation to our graphQL endpoints

* Remove defunct server.config fn

The last remaining usage of this config was removed in #51985.

* Group our dev endpoints together

The graphiQL endpoint is the only thing that currently uses the GET
endpoint; everything else that talks to graphQL uses POST. For that
reason, I'm putting them in the same scope (along with annotating here)
to make that a bit clearer.

* Determine environment from plugin context

The kibana platform did and does provide this interface to check with
environment we're running in.

* Migrate xpack_main to NP features service

* Fix some issues missed in the previous merge

DE added some dependencies on both the server and request objects. Most
have NP equivalents and can be converted, but for now let's just add
them back to the Facades and convert in another PR.

Also changes one function to pull plugins from the server object, rather
than the server object living on the request (as this is how similar
functions are structured right now).

* Fix type resulting from bad merge resolution

* Fix type error due to incorrect usage of Hapi.Request

Pull elasticsearch service off our legacy server object, rather than
indirectly off the request object. Still legacy, but it's one less step
for later.
This commit is contained in:
Ryland Herrick 2019-12-09 15:17:29 -06:00 committed by GitHub
parent cb60a77bb9
commit 45df5fdf42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 415 additions and 667 deletions

View file

@ -29,6 +29,7 @@ import {
DEFAULT_SIGNALS_INDEX_KEY,
} from './common/constants';
import { defaultIndexPattern } from './default_index_pattern';
import { initServerWithKibana } from './server/kibana.index';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const siem = (kibana: any) => {
@ -136,43 +137,24 @@ export const siem = (kibana: any) => {
mappings: savedObjectMappings,
},
init(server: Server) {
const {
config,
getInjectedUiAppVars,
indexPatternsServiceFactory,
injectUiAppVars,
newPlatform,
plugins,
route,
savedObjects,
} = server;
const {
env,
coreContext: { logger },
setup,
} = newPlatform;
const initializerContext = { logger, env };
const { config, newPlatform, plugins, route } = server;
const { coreContext, env, setup } = newPlatform;
const initializerContext = { ...coreContext, env } as PluginInitializerContext;
const serverFacade = {
config,
getInjectedUiAppVars,
indexPatternsServiceFactory,
injectUiAppVars,
plugins: {
alerting: plugins.alerting,
xpack_main: plugins.xpack_main,
elasticsearch: plugins.elasticsearch,
spaces: plugins.spaces,
},
route: route.bind(server),
savedObjects,
};
plugin(initializerContext as PluginInitializerContext).setup(
setup.core,
setup.plugins,
serverFacade
);
// @ts-ignore-next-line: setup.plugins is too loosely typed
plugin(initializerContext).setup(setup.core, setup.plugins);
initServerWithKibana(initializerContext, serverFacade);
},
config(Joi: Root) {
return Joi.object()

View file

@ -55,15 +55,3 @@ export const schemas = [
uncommonProcessesSchema,
whoAmISchema,
];
// The types from graphql-tools/src/mock.ts 'any' based. I add slightly
// stricter types here, but these should go away when graphql-tools using something
// other than "any" in the future for its types.
// https://github.com/apollographql/graphql-tools/blob/master/src/mock.ts#L406
export interface SiemContext {
req: {
payload: {
operationName: string;
};
};
}

View file

@ -9,7 +9,6 @@ import { AppResolverOf, ChildResolverOf } from '../../lib/framework';
import { IndexFields } from '../../lib/index_fields';
import { SourceStatus } from '../../lib/source_status';
import { QuerySourceResolver } from '../sources/resolvers';
import { FrameworkFieldsRequest } from '../../lib/index_fields/types';
export type SourceStatusIndicesExistResolver = ChildResolverOf<
AppResolverOf<SourceStatusResolvers.IndicesExistResolver>,
@ -47,7 +46,7 @@ export const createSourceStatusResolvers = (libs: {
) {
return [];
}
return libs.fields.getFields(req as FrameworkFieldsRequest, args.defaultIndex);
return libs.fields.getFields(req, args.defaultIndex);
},
},
});

View file

@ -4,16 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { Logger, EnvironmentMode } from 'src/core/server';
import { initServer } from './init_server';
import { compose } from './lib/compose/kibana';
import {
noteSavedObjectType,
pinnedEventSavedObjectType,
timelineSavedObjectType,
} from './saved_objects';
import { PluginInitializerContext } from 'src/core/server';
import { rulesAlertType } from './lib/detection_engine/alerts/rules_alert_type';
import { isAlertExecutor } from './lib/detection_engine/alerts/types';
@ -30,74 +21,33 @@ import { deleteIndexRoute } from './lib/detection_engine/routes/index/delete_ind
const APP_ID = 'siem';
export const initServerWithKibana = (
kbnServer: ServerFacade,
logger: Logger,
mode: EnvironmentMode
) => {
if (kbnServer.plugins.alerting != null) {
const version = kbnServer.config().get<string>('pkg.version');
export const initServerWithKibana = (context: PluginInitializerContext, __legacy: ServerFacade) => {
const logger = context.logger.get('plugins', APP_ID);
const version = context.env.packageInfo.version;
if (__legacy.plugins.alerting != null) {
const type = rulesAlertType({ logger, version });
if (isAlertExecutor(type)) {
kbnServer.plugins.alerting.setup.registerType(type);
__legacy.plugins.alerting.setup.registerType(type);
}
}
kbnServer.injectUiAppVars('siem', async () => kbnServer.getInjectedUiAppVars('kibana'));
const libs = compose(kbnServer, mode);
initServer(libs);
// Detection Engine Rule routes that have the REST endpoints of /api/detection_engine/rules
// All REST rule creation, deletion, updating, etc...
createRulesRoute(kbnServer);
readRulesRoute(kbnServer);
updateRulesRoute(kbnServer);
deleteRulesRoute(kbnServer);
findRulesRoute(kbnServer);
createRulesRoute(__legacy);
readRulesRoute(__legacy);
updateRulesRoute(__legacy);
deleteRulesRoute(__legacy);
findRulesRoute(__legacy);
// Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals
// POST /api/detection_engine/signals/status
// Example usage can be found in siem/server/lib/detection_engine/scripts/signals
setSignalsStatusRoute(kbnServer);
setSignalsStatusRoute(__legacy);
// Detection Engine index routes that have the REST endpoints of /api/detection_engine/index
// All REST index creation, policy management for spaces
createIndexRoute(kbnServer);
readIndexRoute(kbnServer);
deleteIndexRoute(kbnServer);
const xpackMainPlugin = kbnServer.plugins.xpack_main;
xpackMainPlugin.registerFeature({
id: APP_ID,
name: i18n.translate('xpack.siem.featureRegistry.linkSiemTitle', {
defaultMessage: 'SIEM',
}),
icon: 'securityAnalyticsApp',
navLinkId: 'siem',
app: ['siem', 'kibana'],
catalogue: ['siem'],
privileges: {
all: {
api: ['siem'],
savedObject: {
all: [noteSavedObjectType, pinnedEventSavedObjectType, timelineSavedObjectType],
read: ['config'],
},
ui: ['show'],
},
read: {
api: ['siem'],
savedObject: {
all: [],
read: [
'config',
noteSavedObjectType,
pinnedEventSavedObjectType,
timelineSavedObjectType,
],
},
ui: ['show'],
},
},
});
createIndexRoute(__legacy);
readIndexRoute(__legacy);
deleteIndexRoute(__legacy);
};

View file

@ -4,13 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EnvironmentMode } from 'src/core/server';
import { ServerFacade } from '../../types';
import { CoreSetup, PluginInitializerContext } from 'src/core/server';
import { Anomalies } from '../anomalies';
import { ElasticsearchAnomaliesAdapter } from '../anomalies/elasticsearch_adapter';
import { Authentications } from '../authentications';
import { ElasticsearchAuthenticationAdapter } from '../authentications/elasticsearch_adapter';
import { KibanaConfigurationAdapter } from '../configuration/kibana_configuration_adapter';
import { ElasticsearchEventsAdapter, Events } from '../events';
import { KibanaBackendFrameworkAdapter } from '../framework/kibana_framework_adapter';
import { ElasticsearchHostsAdapter, Hosts } from '../hosts';
@ -28,21 +26,20 @@ import { Overview } from '../overview';
import { ElasticsearchOverviewAdapter } from '../overview/elasticsearch_adapter';
import { ElasticsearchSourceStatusAdapter, SourceStatus } from '../source_status';
import { ConfigurationSourcesAdapter, Sources } from '../sources';
import { AppBackendLibs, AppDomainLibs, Configuration } from '../types';
import { AppBackendLibs, AppDomainLibs } from '../types';
import { ElasticsearchUncommonProcessesAdapter, UncommonProcesses } from '../uncommon_processes';
import { Note } from '../note/saved_object';
import { PinnedEvent } from '../pinned_event/saved_object';
import { Timeline } from '../timeline/saved_object';
export function compose(server: ServerFacade, mode: EnvironmentMode): AppBackendLibs {
const configuration = new KibanaConfigurationAdapter<Configuration>(server);
const framework = new KibanaBackendFrameworkAdapter(server, mode);
const sources = new Sources(new ConfigurationSourcesAdapter(configuration));
export function compose(core: CoreSetup, env: PluginInitializerContext['env']): AppBackendLibs {
const framework = new KibanaBackendFrameworkAdapter(core, env);
const sources = new Sources(new ConfigurationSourcesAdapter());
const sourceStatus = new SourceStatus(new ElasticsearchSourceStatusAdapter(framework));
const timeline = new Timeline({ savedObjects: framework.getSavedObjectsService() });
const note = new Note({ savedObjects: framework.getSavedObjectsService() });
const pinnedEvent = new PinnedEvent({ savedObjects: framework.getSavedObjectsService() });
const timeline = new Timeline();
const note = new Note();
const pinnedEvent = new PinnedEvent();
const domainLibs: AppDomainLibs = {
anomalies: new Anomalies(new ElasticsearchAnomaliesAdapter(framework)),
@ -60,7 +57,6 @@ export function compose(server: ServerFacade, mode: EnvironmentMode): AppBackend
};
const libs: AppBackendLibs = {
configuration,
framework,
sourceStatus,
sources,

View file

@ -1,40 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { KibanaConfigurationAdapter } from './kibana_configuration_adapter';
describe('the KibanaConfigurationAdapter', () => {
test('queries the xpack.siem configuration of the server', async () => {
const mockConfig = {
get: jest.fn(),
};
const configurationAdapter = new KibanaConfigurationAdapter({
config: () => mockConfig,
});
await configurationAdapter.get();
expect(mockConfig.get).toBeCalledWith('xpack.siem');
});
test('applies the query defaults', async () => {
const configurationAdapter = new KibanaConfigurationAdapter({
config: () => ({
get: () => ({}),
}),
});
const configuration = await configurationAdapter.get();
expect(configuration).toMatchObject({
query: {
partitionSize: expect.any(Number),
partitionFactor: expect.any(Number),
},
});
});
});

View file

@ -1,80 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import Joi from 'joi';
import { ConfigurationAdapter } from './adapter_types';
export class KibanaConfigurationAdapter<Configuration>
implements ConfigurationAdapter<Configuration> {
private readonly server: ServerWithConfig;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(server: any) {
if (!isServerWithConfig(server)) {
throw new Error('Failed to find configuration on server.');
}
this.server = server;
}
public async get() {
const config = this.server.config();
if (!isKibanaConfiguration(config)) {
throw new Error('Failed to access configuration of server.');
}
const configuration = config.get('xpack.siem') || {};
const configurationWithDefaults = {
enabled: true,
query: {
partitionSize: 75,
partitionFactor: 1.2,
...(configuration.query || {}),
},
sources: {},
...configuration,
} as Configuration;
// we assume this to be the configuration because Kibana would have already validated it
return configurationWithDefaults;
}
}
interface ServerWithConfig {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
config(): any;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isServerWithConfig(maybeServer: any): maybeServer is ServerWithConfig {
return (
Joi.validate(
maybeServer,
Joi.object({
config: Joi.func().required(),
}).unknown()
).error === null
);
}
interface KibanaConfiguration {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get(key: string): any;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isKibanaConfiguration(maybeConfiguration: any): maybeConfiguration is KibanaConfiguration {
return (
Joi.validate(
maybeConfiguration,
Joi.object({
get: Joi.func().required(),
}).unknown()
).error === null
);
}

View file

@ -67,7 +67,7 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
try {
const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server);
const callWithRequest = callWithRequestFactory(request);
const callWithRequest = callWithRequestFactory(request, server);
const indexExists = await getIndexExists(callWithRequest, finalIndex);
if (!indexExists) {
return new Boom(
@ -118,6 +118,6 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute =
};
};
export const createRulesRoute = (server: ServerFacade) => {
export const createRulesRoute = (server: ServerFacade): void => {
server.route(createCreateRulesRoute(server));
};

View file

@ -34,7 +34,7 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute =
async handler(request: RequestFacade) {
try {
const index = getIndex(request, server);
const callWithRequest = callWithRequestFactory(request);
const callWithRequest = callWithRequestFactory(request, server);
const indexExists = await getIndexExists(callWithRequest, index);
if (indexExists) {
return new Boom(`index: "${index}" already exists`, { statusCode: 409 });

View file

@ -42,7 +42,7 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute =
async handler(request: RequestFacade) {
try {
const index = getIndex(request, server);
const callWithRequest = callWithRequestFactory(request);
const callWithRequest = callWithRequestFactory(request, server);
const indexExists = await getIndexExists(callWithRequest, index);
if (!indexExists) {
return new Boom(`index: "${index}" does not exist`, { statusCode: 404 });

View file

@ -27,7 +27,7 @@ export const createReadIndexRoute = (server: ServerFacade): Hapi.ServerRoute =>
async handler(request: RequestFacade, headers) {
try {
const index = getIndex(request, server);
const callWithRequest = callWithRequestFactory(request);
const callWithRequest = callWithRequestFactory(request, server);
const indexExists = await getIndexExists(callWithRequest, index);
if (indexExists) {
// head request is used for if you want to get if the index exists

View file

@ -27,7 +27,7 @@ export const setSignalsStatusRouteDef = (server: ServerFacade): Hapi.ServerRoute
async handler(request: SignalsRequest, headers) {
const { signal_ids: signalIds, query, status } = request.payload;
const index = getIndex(request, server);
const { callWithRequest } = request.server.plugins.elasticsearch.getCluster('data');
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
let queryObject;
if (signalIds) {
queryObject = { ids: { values: signalIds } };

View file

@ -96,8 +96,8 @@ export const getIndex = (request: RequestFacade, server: ServerFacade): string =
return `${signalsIndex}-${spaceId}`;
};
export const callWithRequestFactory = (request: RequestFacade) => {
const { callWithRequest } = request.server.plugins.elasticsearch.getCluster('data');
export const callWithRequestFactory = (request: RequestFacade, server: ServerFacade) => {
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
return <T, U>(endpoint: string, params: T, options?: U) => {
return callWithRequest(request, endpoint, params, options);
};

View file

@ -521,10 +521,8 @@ describe('events elasticsearch_adapter', () => {
const mockFramework: FrameworkAdapter = {
version: 'mock',
callWithRequest: mockCallWithRequest,
exposeStaticDir: jest.fn(),
registerGraphQLEndpoint: jest.fn(),
getIndexPatternsService: jest.fn(),
getSavedObjectsService: jest.fn(),
};
jest.doMock('../framework', () => ({
callWithRequest: mockCallWithRequest,

View file

@ -4,14 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { GenericParams } from 'elasticsearch';
import * as GraphiQL from 'apollo-server-module-graphiql';
import Boom from 'boom';
import { ResponseToolkit } from 'hapi';
import { EnvironmentMode } from 'kibana/public';
import { GraphQLSchema } from 'graphql';
import { runHttpQuery } from 'apollo-server-core';
import { ServerFacade, RequestFacade } from '../../types';
import { schema as configSchema } from '@kbn/config-schema';
import {
CoreSetup,
IRouter,
KibanaResponseFactory,
RequestHandlerContext,
PluginInitializerContext,
} from 'src/core/server';
import { IndexPatternsFetcher } from '../../../../../../../src/plugins/data/server';
import { RequestFacade } from '../../types';
import {
FrameworkAdapter,
@ -21,125 +26,119 @@ import {
WrappableRequest,
} from './types';
interface CallWithRequestParams extends GenericParams {
max_concurrent_shard_requests?: number;
}
export class KibanaBackendFrameworkAdapter implements FrameworkAdapter {
public version: string;
public envMode: EnvironmentMode;
private isProductionMode: boolean;
private router: IRouter;
constructor(private server: ServerFacade, mode: EnvironmentMode) {
this.version = server.config().get('pkg.version');
this.envMode = mode;
constructor(core: CoreSetup, env: PluginInitializerContext['env']) {
this.version = env.packageInfo.version;
this.isProductionMode = env.mode.prod;
this.router = core.http.createRouter();
}
public async callWithRequest(
req: FrameworkRequest,
endpoint: string,
params: CallWithRequestParams,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...rest: any[]
params: Record<string, any>
) {
const internalRequest = req[internalFrameworkRequest];
const { elasticsearch } = internalRequest.server.plugins;
const { callWithRequest } = elasticsearch.getCluster('data');
const includeFrozen = await internalRequest.getUiSettingsService().get('search:includeFrozen');
const { elasticsearch, uiSettings } = req.context.core;
const includeFrozen = await uiSettings.client.get('search:includeFrozen');
const maxConcurrentShardRequests =
endpoint === 'msearch'
? await internalRequest.getUiSettingsService().get('courier:maxConcurrentShardRequests')
? await uiSettings.client.get('courier:maxConcurrentShardRequests')
: 0;
const fields = await callWithRequest(
internalRequest,
endpoint,
{
...params,
ignore_throttled: !includeFrozen,
...(maxConcurrentShardRequests > 0
? { max_concurrent_shard_requests: maxConcurrentShardRequests }
: {}),
},
...rest
);
return fields;
}
public exposeStaticDir(urlPath: string, dir: string): void {
this.server.route({
handler: {
directory: {
path: dir,
},
},
method: 'GET',
path: urlPath,
return elasticsearch.dataClient.callAsCurrentUser(endpoint, {
...params,
ignore_throttled: !includeFrozen,
...(maxConcurrentShardRequests > 0
? { max_concurrent_shard_requests: maxConcurrentShardRequests }
: {}),
});
}
public registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void {
this.server.route({
options: {
tags: ['access:siem'],
},
handler: async (request: RequestFacade, h: ResponseToolkit) => {
try {
const query =
request.method === 'post'
? (request.payload as Record<string, any>) // eslint-disable-line @typescript-eslint/no-explicit-any
: (request.query as Record<string, any>); // eslint-disable-line @typescript-eslint/no-explicit-any
const gqlResponse = await runHttpQuery([request], {
method: request.method.toUpperCase(),
options: (req: RequestFacade) => ({
context: { req: wrapRequest(req) },
schema,
}),
query,
});
return h.response(gqlResponse).type('application/json');
} catch (error) {
if ('HttpQueryError' !== error.name) {
const queryError = Boom.boomify(error);
queryError.output.payload.message = error.message;
return queryError;
}
if (error.isGraphQLError === true) {
return h
.response(error.message)
.code(error.statusCode)
.type('application/json');
}
const genericError = new Boom(error.message, { statusCode: error.statusCode });
if (error.headers) {
Object.keys(error.headers).forEach(header => {
genericError.output.headers[header] = error.headers[header];
});
}
// Boom hides the error when status code is 500
genericError.output.payload.message = error.message;
throw genericError;
}
},
method: ['GET', 'POST'],
path: routePath,
vhost: undefined,
});
if (!this.envMode.prod) {
this.server.route({
this.router.post(
{
path: routePath,
validate: {
body: configSchema.object({
operationName: configSchema.string(),
query: configSchema.string(),
variables: configSchema.object({}, { allowUnknowns: true }),
}),
},
options: {
tags: ['access:siem'],
},
handler: async (request: RequestFacade, h: ResponseToolkit) => {
},
async (context, request, response) => {
try {
const gqlResponse = await runHttpQuery([request], {
method: 'POST',
options: (req: RequestFacade) => ({
context: { req: wrapRequest(req, context) },
schema,
}),
query: request.body,
});
return response.ok({
body: gqlResponse,
headers: {
'content-type': 'application/json',
},
});
} catch (error) {
return this.handleError(error, response);
}
}
);
if (!this.isProductionMode) {
this.router.get(
{
path: routePath,
validate: { query: configSchema.object({}, { allowUnknowns: true }) },
options: {
tags: ['access:siem'],
},
},
async (context, request, response) => {
try {
const { query } = request;
const gqlResponse = await runHttpQuery([request], {
method: 'GET',
options: (req: RequestFacade) => ({
context: { req: wrapRequest(req, context) },
schema,
}),
query,
});
return response.ok({
body: gqlResponse,
headers: {
'content-type': 'application/json',
},
});
} catch (error) {
return this.handleError(error, response);
}
}
);
this.router.get(
{
path: `${routePath}/graphiql`,
validate: false,
options: {
tags: ['access:siem'],
},
},
async (context, request, response) => {
const graphiqlString = await GraphiQL.resolveGraphiQLString(
request.query,
{
@ -149,42 +148,60 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter {
request
);
return h.response(graphiqlString).type('text/html');
},
method: 'GET',
path: `${routePath}/graphiql`,
});
return response.ok({
body: graphiqlString,
headers: {
'content-type': 'text/html',
},
});
}
);
}
}
public getIndexPatternsService(request: FrameworkRequest): FrameworkIndexPatternsService {
return this.server.indexPatternsServiceFactory({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callCluster: async (method: string, args: [GenericParams], ...rest: any[]) => {
const fieldCaps = await this.callWithRequest(
request,
method,
{ ...args, allowNoIndices: true } as GenericParams,
...rest
);
return fieldCaps;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private handleError(error: any, response: KibanaResponseFactory) {
if (error.name !== 'HttpQueryError') {
return response.internalError({
body: error.message,
headers: {
'content-type': 'application/json',
},
});
}
return response.customError({
statusCode: error.statusCode,
body: error.message,
headers: {
'content-type': 'application/json',
...error.headers,
},
});
}
public getSavedObjectsService() {
return this.server.savedObjects;
public getIndexPatternsService(request: FrameworkRequest): FrameworkIndexPatternsService {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callCluster = async (endpoint: string, params?: Record<string, any>) =>
this.callWithRequest(request, endpoint, {
...params,
allowNoIndices: true,
});
return new IndexPatternsFetcher(callCluster);
}
}
export function wrapRequest<InternalRequest extends WrappableRequest>(
req: InternalRequest
req: InternalRequest,
context: RequestHandlerContext
): FrameworkRequest<InternalRequest> {
const { auth, params, payload, query } = req;
return {
[internalFrameworkRequest]: req,
auth,
context,
params,
payload,
query,

View file

@ -7,8 +7,8 @@
import { IndicesGetMappingParams } from 'elasticsearch';
import { GraphQLSchema } from 'graphql';
import { RequestAuth } from 'hapi';
import { Legacy } from 'kibana';
import { RequestHandlerContext } from 'src/core/server';
import { ESQuery } from '../../../common/typed_json';
import {
PaginationInput,
@ -25,7 +25,6 @@ export const internalFrameworkRequest = Symbol('internalFrameworkRequest');
export interface FrameworkAdapter {
version: string;
exposeStaticDir(urlPath: string, dir: string): void;
registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void;
callWithRequest<Hit = {}, Aggregation = undefined>(
req: FrameworkRequest,
@ -37,27 +36,17 @@ export interface FrameworkAdapter {
method: 'msearch',
options?: object
): Promise<DatabaseMultiResponse<Hit, Aggregation>>;
callWithRequest(
req: FrameworkRequest,
method: 'indices.existsAlias',
options?: object
): Promise<boolean>;
callWithRequest(
req: FrameworkRequest,
method: 'indices.getMapping',
options?: IndicesGetMappingParams // eslint-disable-line
): Promise<MappingResponse>;
callWithRequest(
req: FrameworkRequest,
method: 'indices.getAlias' | 'indices.get', // eslint-disable-line
options?: object
): Promise<DatabaseGetIndicesResponse>;
getIndexPatternsService(req: FrameworkRequest): FrameworkIndexPatternsService;
getSavedObjectsService(): Legacy.SavedObjectsService;
}
export interface FrameworkRequest<InternalRequest extends WrappableRequest = RequestFacade> {
[internalFrameworkRequest]: InternalRequest;
context: RequestHandlerContext;
payload: InternalRequest['payload'];
params: InternalRequest['params'];
query: InternalRequest['query'];
@ -132,22 +121,6 @@ export interface FrameworkIndexPatternsService {
}): Promise<FrameworkIndexFieldDescriptor[]>;
}
interface Alias {
settings: {
index: {
uuid: string;
};
};
}
export interface DatabaseGetIndicesResponse {
[indexName: string]: {
aliases: {
[aliasName: string]: Alias;
};
};
}
export interface RequestBasicOptions {
sourceConfiguration: SourceConfiguration;
timerange: TimerangeInput;

View file

@ -161,10 +161,8 @@ describe('hosts elasticsearch_adapter', () => {
const mockFramework: FrameworkAdapter = {
version: 'mock',
callWithRequest: mockCallWithRequest,
exposeStaticDir: jest.fn(),
registerGraphQLEndpoint: jest.fn(),
getIndexPatternsService: jest.fn(),
getSavedObjectsService: jest.fn(),
};
jest.doMock('../framework', () => ({ callWithRequest: mockCallWithRequest }));
@ -184,10 +182,8 @@ describe('hosts elasticsearch_adapter', () => {
const mockFramework: FrameworkAdapter = {
version: 'mock',
callWithRequest: mockCallWithRequest,
exposeStaticDir: jest.fn(),
registerGraphQLEndpoint: jest.fn(),
getIndexPatternsService: jest.fn(),
getSavedObjectsService: jest.fn(),
};
jest.doMock('../framework', () => ({ callWithRequest: mockCallWithRequest }));
@ -207,10 +203,8 @@ describe('hosts elasticsearch_adapter', () => {
const mockFramework: FrameworkAdapter = {
version: 'mock',
callWithRequest: mockCallWithRequest,
exposeStaticDir: jest.fn(),
registerGraphQLEndpoint: jest.fn(),
getIndexPatternsService: jest.fn(),
getSavedObjectsService: jest.fn(),
};
jest.doMock('../framework', () => ({ callWithRequest: mockCallWithRequest }));

View file

@ -14,22 +14,20 @@ import {
hasDocumentation,
IndexAlias,
} from '../../utils/beat_schema';
import { FrameworkAdapter } from '../framework';
import { FieldsAdapter, IndexFieldDescriptor, FrameworkFieldsRequest } from './types';
import { FrameworkAdapter, FrameworkRequest } from '../framework';
import { FieldsAdapter, IndexFieldDescriptor } from './types';
type IndexesAliasIndices = Record<string, string[]>;
export class ElasticsearchIndexFieldAdapter implements FieldsAdapter {
constructor(private readonly framework: FrameworkAdapter) {}
public async getIndexFields(
request: FrameworkFieldsRequest,
indices: string[]
): Promise<IndexField[]> {
public async getIndexFields(request: FrameworkRequest, indices: string[]): Promise<IndexField[]> {
const indexPatternsService = this.framework.getIndexPatternsService(request);
const indexesAliasIndices: IndexesAliasIndices = indices.reduce(
(accumulator: IndexesAliasIndices, indice: string) => {
const key: string = getIndexAlias(request.payload.variables.defaultIndex, indice);
const key = getIndexAlias(indices, indice);
if (get(key, accumulator)) {
accumulator[key] = [...accumulator[key], indice];
} else {

View file

@ -6,16 +6,14 @@
import { IndexField } from '../../graphql/types';
import { FieldsAdapter, FrameworkFieldsRequest } from './types';
import { FieldsAdapter } from './types';
import { FrameworkRequest } from '../framework';
export { ElasticsearchIndexFieldAdapter } from './elasticsearch_adapter';
export class IndexFields {
constructor(private readonly adapter: FieldsAdapter) {}
public async getFields(
request: FrameworkFieldsRequest,
defaultIndex: string[]
): Promise<IndexField[]> {
public async getFields(request: FrameworkRequest, defaultIndex: string[]): Promise<IndexField[]> {
return this.adapter.getIndexFields(request, defaultIndex);
}
}

View file

@ -6,20 +6,9 @@
import { IndexField } from '../../graphql/types';
import { FrameworkRequest } from '../framework';
import { RequestFacade } from '../../types';
type IndexFieldsRequest = RequestFacade & {
payload: {
variables: {
defaultIndex: string[];
};
};
};
export type FrameworkFieldsRequest = FrameworkRequest<IndexFieldsRequest>;
export interface FieldsAdapter {
getIndexFields(req: FrameworkFieldsRequest, indices: string[]): Promise<IndexField[]>;
getIndexFields(req: FrameworkRequest, indices: string[]): Promise<IndexField[]>;
}
export interface IndexFieldDescriptor {

View file

@ -55,10 +55,8 @@ describe('getKpiHosts', () => {
const mockFramework: FrameworkAdapter = {
version: 'mock',
callWithRequest: mockCallWithRequest,
exposeStaticDir: jest.fn(),
registerGraphQLEndpoint: jest.fn(),
getIndexPatternsService: jest.fn(),
getSavedObjectsService: jest.fn(),
};
let EsKpiHosts: ElasticsearchKpiHostsAdapter;
@ -171,10 +169,8 @@ describe('getKpiHostDetails', () => {
const mockFramework: FrameworkAdapter = {
version: 'mock',
callWithRequest: mockCallWithRequest,
exposeStaticDir: jest.fn(),
registerGraphQLEndpoint: jest.fn(),
getIndexPatternsService: jest.fn(),
getSavedObjectsService: jest.fn(),
};
let EsKpiHosts: ElasticsearchKpiHostsAdapter;

View file

@ -50,10 +50,8 @@ describe('Network Kpi elasticsearch_adapter', () => {
const mockFramework: FrameworkAdapter = {
version: 'mock',
callWithRequest: mockCallWithRequest,
exposeStaticDir: jest.fn(),
registerGraphQLEndpoint: jest.fn(),
getIndexPatternsService: jest.fn(),
getSavedObjectsService: jest.fn(),
};
let EsKpiNetwork: ElasticsearchKpiNetworkAdapter;

View file

@ -37,9 +37,7 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () =
const mockFramework: FrameworkAdapter = {
version: 'mock',
callWithRequest: mockCallWithRequest,
exposeStaticDir: jest.fn(),
getIndexPatternsService: jest.fn(),
getSavedObjectsService: jest.fn(),
registerGraphQLEndpoint: jest.fn(),
};
jest.doMock('../framework', () => ({
@ -65,10 +63,8 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () =
const mockFramework: FrameworkAdapter = {
version: 'mock',
callWithRequest: mockCallWithRequest,
exposeStaticDir: jest.fn(),
registerGraphQLEndpoint: jest.fn(),
getIndexPatternsService: jest.fn(),
getSavedObjectsService: jest.fn(),
};
jest.doMock('../framework', () => ({
callWithRequest: mockCallWithRequest,
@ -107,9 +103,7 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () =
const mockFramework: FrameworkAdapter = {
version: 'mock',
callWithRequest: mockCallWithRequest,
exposeStaticDir: jest.fn(),
getIndexPatternsService: jest.fn(),
getSavedObjectsService: jest.fn(),
registerGraphQLEndpoint: jest.fn(),
};
jest.doMock('../framework', () => ({
@ -140,10 +134,8 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () =
const mockFramework: FrameworkAdapter = {
version: 'mock',
callWithRequest: mockCallWithRequest,
exposeStaticDir: jest.fn(),
registerGraphQLEndpoint: jest.fn(),
getIndexPatternsService: jest.fn(),
getSavedObjectsService: jest.fn(),
};
jest.doMock('../framework', () => ({
callWithRequest: mockCallWithRequest,
@ -165,9 +157,7 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () =
const mockFramework: FrameworkAdapter = {
version: 'mock',
callWithRequest: mockCallWithRequest,
exposeStaticDir: jest.fn(),
getIndexPatternsService: jest.fn(),
getSavedObjectsService: jest.fn(),
registerGraphQLEndpoint: jest.fn(),
};
jest.doMock('../framework', () => ({

View file

@ -6,7 +6,6 @@
import { failure } from 'io-ts/lib/PathReporter';
import { RequestAuth } from 'hapi';
import { Legacy } from 'kibana';
import { getOr } from 'lodash/fp';
import uuid from 'uuid';
@ -15,7 +14,6 @@ import { SavedObjectsFindOptions } from 'src/core/server';
import { pipe } from 'fp-ts/lib/pipeable';
import { map, fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { Pick3 } from '../../../common/utility_types';
import {
PageInfoNote,
ResponseNote,
@ -31,20 +29,11 @@ import { pickSavedTimeline } from '../timeline/pick_saved_timeline';
import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline';
export class Note {
constructor(
private readonly libs: {
savedObjects: Pick<Legacy.SavedObjectsService, 'getScopedSavedObjectsClient'> &
Pick3<Legacy.SavedObjectsService, 'SavedObjectsClient', 'errors', 'isConflictError'>;
}
) {}
public async deleteNote(request: FrameworkRequest, noteIds: string[]) {
const savedObjectsClient = request.context.core.savedObjects.client;
await Promise.all(
noteIds.map(noteId =>
this.libs.savedObjects
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
.delete(noteSavedObjectType, noteId)
)
noteIds.map(noteId => savedObjectsClient.delete(noteSavedObjectType, noteId))
);
}
@ -55,11 +44,11 @@ export class Note {
searchFields: ['timelineId'],
};
const notesToBeDeleted = await this.getAllSavedNote(request, options);
const savedObjectsClient = request.context.core.savedObjects.client;
await Promise.all(
notesToBeDeleted.notes.map(note =>
this.libs.savedObjects
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
.delete(noteSavedObjectType, note.noteId)
savedObjectsClient.delete(noteSavedObjectType, note.noteId)
)
);
}
@ -119,17 +108,17 @@ export class Note {
note: SavedNote
): Promise<ResponseNote> {
try {
const savedObjectsClient = request.context.core.savedObjects.client;
if (noteId == null) {
const timelineVersionSavedObject =
note.timelineId == null
? await (async () => {
const timelineResult = convertSavedObjectToSavedTimeline(
await this.libs.savedObjects
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
.create(
timelineSavedObjectType,
pickSavedTimeline(null, {}, request[internalFrameworkRequest].auth || null)
)
await savedObjectsClient.create(
timelineSavedObjectType,
pickSavedTimeline(null, {}, request[internalFrameworkRequest].auth || null)
)
);
note.timelineId = timelineResult.savedObjectId;
return timelineResult.version;
@ -141,12 +130,10 @@ export class Note {
code: 200,
message: 'success',
note: convertSavedObjectToSavedNote(
await this.libs.savedObjects
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
.create(
noteSavedObjectType,
pickSavedNote(noteId, note, request[internalFrameworkRequest].auth || null)
),
await savedObjectsClient.create(
noteSavedObjectType,
pickSavedNote(noteId, note, request[internalFrameworkRequest].auth || null)
),
timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined
),
};
@ -157,16 +144,14 @@ export class Note {
code: 200,
message: 'success',
note: convertSavedObjectToSavedNote(
await this.libs.savedObjects
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
.update(
noteSavedObjectType,
noteId,
pickSavedNote(noteId, note, request[internalFrameworkRequest].auth || null),
{
version: version || undefined,
}
)
await savedObjectsClient.update(
noteSavedObjectType,
noteId,
pickSavedNote(noteId, note, request[internalFrameworkRequest].auth || null),
{
version: version || undefined,
}
)
),
};
} catch (err) {
@ -189,20 +174,14 @@ export class Note {
}
private async getSavedNote(request: FrameworkRequest, NoteId: string) {
const savedObjectsClient = this.libs.savedObjects.getScopedSavedObjectsClient(
request[internalFrameworkRequest]
);
const savedObjectsClient = request.context.core.savedObjects.client;
const savedObject = await savedObjectsClient.get(noteSavedObjectType, NoteId);
return convertSavedObjectToSavedNote(savedObject);
}
private async getAllSavedNote(request: FrameworkRequest, options: SavedObjectsFindOptions) {
const savedObjectsClient = this.libs.savedObjects.getScopedSavedObjectsClient(
request[internalFrameworkRequest]
);
const savedObjectsClient = request.context.core.savedObjects.client;
const savedObjects = await savedObjectsClient.find(options);
return {

View file

@ -38,10 +38,8 @@ describe('Siem Overview elasticsearch_adapter', () => {
const mockFramework: FrameworkAdapter = {
version: 'mock',
callWithRequest: mockCallWithRequest,
exposeStaticDir: jest.fn(),
registerGraphQLEndpoint: jest.fn(),
getIndexPatternsService: jest.fn(),
getSavedObjectsService: jest.fn(),
};
jest.doMock('../framework', () => ({
callWithRequest: mockCallWithRequest,
@ -74,10 +72,8 @@ describe('Siem Overview elasticsearch_adapter', () => {
const mockFramework: FrameworkAdapter = {
version: 'mock',
callWithRequest: mockCallWithRequest,
exposeStaticDir: jest.fn(),
registerGraphQLEndpoint: jest.fn(),
getIndexPatternsService: jest.fn(),
getSavedObjectsService: jest.fn(),
};
jest.doMock('../framework', () => ({
callWithRequest: mockCallWithRequest,
@ -114,10 +110,8 @@ describe('Siem Overview elasticsearch_adapter', () => {
const mockFramework: FrameworkAdapter = {
version: 'mock',
callWithRequest: mockCallWithRequest,
exposeStaticDir: jest.fn(),
registerGraphQLEndpoint: jest.fn(),
getIndexPatternsService: jest.fn(),
getSavedObjectsService: jest.fn(),
};
jest.doMock('../framework', () => ({
callWithRequest: mockCallWithRequest,
@ -155,10 +149,8 @@ describe('Siem Overview elasticsearch_adapter', () => {
const mockFramework: FrameworkAdapter = {
version: 'mock',
callWithRequest: mockCallWithRequest,
exposeStaticDir: jest.fn(),
registerGraphQLEndpoint: jest.fn(),
getIndexPatternsService: jest.fn(),
getSavedObjectsService: jest.fn(),
};
jest.doMock('../framework', () => ({
callWithRequest: mockCallWithRequest,

View file

@ -6,7 +6,6 @@
import { failure } from 'io-ts/lib/PathReporter';
import { RequestAuth } from 'hapi';
import { Legacy } from 'kibana';
import { getOr } from 'lodash/fp';
import { SavedObjectsFindOptions } from 'src/core/server';
@ -14,7 +13,6 @@ import { SavedObjectsFindOptions } from 'src/core/server';
import { pipe } from 'fp-ts/lib/pipeable';
import { map, fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { Pick3 } from '../../../common/utility_types';
import { FrameworkRequest, internalFrameworkRequest } from '../framework';
import {
PinnedEventSavedObject,
@ -27,24 +25,18 @@ import { pickSavedTimeline } from '../timeline/pick_saved_timeline';
import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline';
export class PinnedEvent {
constructor(
private readonly libs: {
savedObjects: Pick<Legacy.SavedObjectsService, 'getScopedSavedObjectsClient'> &
Pick3<Legacy.SavedObjectsService, 'SavedObjectsClient', 'errors', 'isConflictError'>;
}
) {}
public async deletePinnedEventOnTimeline(request: FrameworkRequest, pinnedEventIds: string[]) {
const savedObjectsClient = request.context.core.savedObjects.client;
await Promise.all(
pinnedEventIds.map(pinnedEventId =>
this.libs.savedObjects
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
.delete(pinnedEventSavedObjectType, pinnedEventId)
savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEventId)
)
);
}
public async deleteAllPinnedEventsOnTimeline(request: FrameworkRequest, timelineId: string) {
const savedObjectsClient = request.context.core.savedObjects.client;
const options: SavedObjectsFindOptions = {
type: pinnedEventSavedObjectType,
search: timelineId,
@ -53,9 +45,7 @@ export class PinnedEvent {
const pinnedEventToBeDeleted = await this.getAllSavedPinnedEvents(request, options);
await Promise.all(
pinnedEventToBeDeleted.map(pinnedEvent =>
this.libs.savedObjects
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
.delete(pinnedEventSavedObjectType, pinnedEvent.pinnedEventId)
savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEvent.pinnedEventId)
)
);
}
@ -103,18 +93,18 @@ export class PinnedEvent {
eventId: string,
timelineId: string | null
): Promise<PinnedEventResponse | null> {
const savedObjectsClient = request.context.core.savedObjects.client;
try {
if (pinnedEventId == null) {
const timelineVersionSavedObject =
timelineId == null
? await (async () => {
const timelineResult = convertSavedObjectToSavedTimeline(
await this.libs.savedObjects
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
.create(
timelineSavedObjectType,
pickSavedTimeline(null, {}, request[internalFrameworkRequest].auth || null)
)
await savedObjectsClient.create(
timelineSavedObjectType,
pickSavedTimeline(null, {}, request[internalFrameworkRequest].auth || null)
)
);
timelineId = timelineResult.savedObjectId; // eslint-disable-line no-param-reassign
return timelineResult.version;
@ -133,16 +123,14 @@ export class PinnedEvent {
};
// create Pinned Event on Timeline
return convertSavedObjectToSavedPinnedEvent(
await this.libs.savedObjects
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
.create(
pinnedEventSavedObjectType,
pickSavedPinnedEvent(
pinnedEventId,
savedPinnedEvent,
request[internalFrameworkRequest].auth || null
)
),
await savedObjectsClient.create(
pinnedEventSavedObjectType,
pickSavedPinnedEvent(
pinnedEventId,
savedPinnedEvent,
request[internalFrameworkRequest].auth || null
)
),
timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined
);
}
@ -177,10 +165,7 @@ export class PinnedEvent {
}
private async getSavedPinnedEvent(request: FrameworkRequest, pinnedEventId: string) {
const savedObjectsClient = this.libs.savedObjects.getScopedSavedObjectsClient(
request[internalFrameworkRequest]
);
const savedObjectsClient = request.context.core.savedObjects.client;
const savedObject = await savedObjectsClient.get(pinnedEventSavedObjectType, pinnedEventId);
return convertSavedObjectToSavedPinnedEvent(savedObject);
@ -190,10 +175,7 @@ export class PinnedEvent {
request: FrameworkRequest,
options: SavedObjectsFindOptions
) {
const savedObjectsClient = this.libs.savedObjects.getScopedSavedObjectsClient(
request[internalFrameworkRequest]
);
const savedObjectsClient = request.context.core.savedObjects.client;
const savedObjects = await savedObjectsClient.find(options);
return savedObjects.saved_objects.map(savedObject =>

View file

@ -4,38 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { DatabaseGetIndicesResponse, FrameworkAdapter, FrameworkRequest } from '../framework';
import { FrameworkAdapter, FrameworkRequest } from '../framework';
import { SourceStatusAdapter } from './index';
export class ElasticsearchSourceStatusAdapter implements SourceStatusAdapter {
constructor(private readonly framework: FrameworkAdapter) {}
public async getIndexNames(request: FrameworkRequest, aliasName: string | string[]) {
const indexMaps = await Promise.all([
this.framework
.callWithRequest(request, 'indices.getAlias', {
name: aliasName,
filterPath: '*.settings.index.uuid', // to keep the response size as small as possible
})
.catch(withDefaultIfNotFound<DatabaseGetIndicesResponse>({})),
this.framework
.callWithRequest(request, 'indices.get', {
index: aliasName,
filterPath: '*.settings.index.uuid', // to keep the response size as small as possible
})
.catch(withDefaultIfNotFound<DatabaseGetIndicesResponse>({})),
]);
return indexMaps.reduce(
(indexNames, indexMap) => [...indexNames, ...Object.keys(indexMap)],
[] as string[]
);
}
public async hasAlias(request: FrameworkRequest, aliasName: string): Promise<boolean> {
return this.framework.callWithRequest(request, 'indices.existsAlias', {
name: aliasName,
});
}
public async hasIndices(request: FrameworkRequest, indexNames: string | string[]) {
return this.framework
@ -56,13 +29,3 @@ export class ElasticsearchSourceStatusAdapter implements SourceStatusAdapter {
);
}
}
const withDefaultIfNotFound = <DefaultValue>(defaultValue: DefaultValue) => (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: any
): DefaultValue => {
if (error && error.status === 404) {
return defaultValue;
}
throw error;
};

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ConfigurationAdapter } from '../configuration';
import { InmemoryConfigurationAdapter } from '../configuration/inmemory_configuration_adapter';
import { SourcesAdapter, SourceConfiguration } from './index';
import { PartialSourceConfigurations } from './types';
@ -15,7 +16,11 @@ interface ConfigurationWithSources {
export class ConfigurationSourcesAdapter implements SourcesAdapter {
private readonly configuration: ConfigurationAdapter<ConfigurationWithSources>;
constructor(configuration: ConfigurationAdapter<ConfigurationWithSources>) {
constructor(
configuration: ConfigurationAdapter<
ConfigurationWithSources
> = new InmemoryConfigurationAdapter({ sources: {} })
) {
this.configuration = configuration;
}

View file

@ -4,12 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Legacy } from 'kibana';
import { getOr } from 'lodash/fp';
import { SavedObjectsFindOptions } from 'src/core/server';
import { Pick3 } from '../../../common/utility_types';
import {
ResponseTimeline,
PageInfoTimeline,
@ -34,17 +32,8 @@ interface ResponseTimelines {
}
export class Timeline {
private readonly note: Note;
private readonly pinnedEvent: PinnedEvent;
constructor(
private readonly libs: {
savedObjects: Pick<Legacy.SavedObjectsService, 'getScopedSavedObjectsClient'> &
Pick3<Legacy.SavedObjectsService, 'SavedObjectsClient', 'errors', 'isConflictError'>;
}
) {
this.note = new Note({ savedObjects: this.libs.savedObjects });
this.pinnedEvent = new PinnedEvent({ savedObjects: this.libs.savedObjects });
}
private readonly note = new Note();
private readonly pinnedEvent = new PinnedEvent();
public async getTimeline(
request: FrameworkRequest,
@ -149,6 +138,8 @@ export class Timeline {
version: string | null,
timeline: SavedTimeline
): Promise<ResponseTimeline> {
const savedObjectsClient = request.context.core.savedObjects.client;
try {
if (timelineId == null) {
// Create new timeline
@ -156,40 +147,33 @@ export class Timeline {
code: 200,
message: 'success',
timeline: convertSavedObjectToSavedTimeline(
await this.libs.savedObjects
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
.create(
timelineSavedObjectType,
pickSavedTimeline(
timelineId,
timeline,
request[internalFrameworkRequest].auth || null
)
await savedObjectsClient.create(
timelineSavedObjectType,
pickSavedTimeline(
timelineId,
timeline,
request[internalFrameworkRequest].auth || null
)
)
),
};
}
// Update Timeline
await this.libs.savedObjects
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
.update(
timelineSavedObjectType,
timelineId,
pickSavedTimeline(timelineId, timeline, request[internalFrameworkRequest].auth || null),
{
version: version || undefined,
}
);
await savedObjectsClient.update(
timelineSavedObjectType,
timelineId,
pickSavedTimeline(timelineId, timeline, request[internalFrameworkRequest].auth || null),
{
version: version || undefined,
}
);
return {
code: 200,
message: 'success',
timeline: await this.getSavedTimeline(request, timelineId),
};
} catch (err) {
if (
timelineId != null &&
this.libs.savedObjects.SavedObjectsClient.errors.isConflictError(err)
) {
if (timelineId != null && savedObjectsClient.errors.isConflictError(err)) {
return {
code: 409,
message: err.message,
@ -212,12 +196,12 @@ export class Timeline {
}
public async deleteTimeline(request: FrameworkRequest, timelineIds: string[]) {
const savedObjectsClient = request.context.core.savedObjects.client;
await Promise.all(
timelineIds.map(timelineId =>
Promise.all([
this.libs.savedObjects
.getScopedSavedObjectsClient(request[internalFrameworkRequest])
.delete(timelineSavedObjectType, timelineId),
savedObjectsClient.delete(timelineSavedObjectType, timelineId),
this.note.deleteNoteByTimelineId(request, timelineId),
this.pinnedEvent.deleteAllPinnedEventsOnTimeline(request, timelineId),
])
@ -226,10 +210,7 @@ export class Timeline {
}
private async getBasicSavedTimeline(request: FrameworkRequest, timelineId: string) {
const savedObjectsClient = this.libs.savedObjects.getScopedSavedObjectsClient(
request[internalFrameworkRequest]
);
const savedObjectsClient = request.context.core.savedObjects.client;
const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId);
return convertSavedObjectToSavedTimeline(savedObject);
@ -238,10 +219,7 @@ export class Timeline {
private async getSavedTimeline(request: FrameworkRequest, timelineId: string) {
const userName = getOr(null, 'credentials.username', request[internalFrameworkRequest].auth);
const savedObjectsClient = this.libs.savedObjects.getScopedSavedObjectsClient(
request[internalFrameworkRequest]
);
const savedObjectsClient = request.context.core.savedObjects.client;
const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId);
const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject);
const timelineWithNotesAndPinnedEvents = await Promise.all([
@ -257,10 +235,7 @@ export class Timeline {
private async getAllSavedTimeline(request: FrameworkRequest, options: SavedObjectsFindOptions) {
const userName = getOr(null, 'credentials.username', request[internalFrameworkRequest].auth);
const savedObjectsClient = this.libs.savedObjects.getScopedSavedObjectsClient(
request[internalFrameworkRequest]
);
const savedObjectsClient = request.context.core.savedObjects.client;
if (options.searchFields != null && options.searchFields.includes('favorite.keySearch')) {
options.search = `${options.search != null ? options.search : ''} ${
userName != null ? convertStringToBase64(userName) : null

View file

@ -24,10 +24,8 @@ describe('elasticsearch_adapter', () => {
const mockFramework: FrameworkAdapter = {
version: 'mock',
callWithRequest: mockCallWithRequest,
exposeStaticDir: jest.fn(),
registerGraphQLEndpoint: jest.fn(),
getIndexPatternsService: jest.fn(),
getSavedObjectsService: jest.fn(),
};
beforeAll(async () => {

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { ConfigType as Configuration } from '../../../../../plugins/siem/server';
import { Anomalies } from './anomalies';
import { Authentications } from './authentications';
import { ConfigurationAdapter } from './configuration';
import { Events } from './events';
import { FrameworkAdapter, FrameworkRequest } from './framework';
import { Hosts } from './hosts';
@ -17,7 +17,7 @@ import { KpiNetwork } from './kpi_network';
import { Network } from './network';
import { Overview } from './overview';
import { SourceStatus } from './source_status';
import { Sources, SourceConfiguration } from './sources';
import { Sources } from './sources';
import { UncommonProcesses } from './uncommon_processes';
import { Note } from './note/saved_object';
import { PinnedEvent } from './pinned_event/saved_object';
@ -43,7 +43,6 @@ export interface AppDomainLibs {
}
export interface AppBackendLibs extends AppDomainLibs {
configuration: ConfigurationAdapter<Configuration>;
framework: FrameworkAdapter;
sources: Sources;
sourceStatus: SourceStatus;
@ -52,15 +51,6 @@ export interface AppBackendLibs extends AppDomainLibs {
pinnedEvent: PinnedEvent;
}
export interface Configuration {
enabled: boolean;
query: {
partitionSize: number;
partitionFactor: number;
};
sources: Record<string, SourceConfiguration>;
}
export interface SiemContext {
req: FrameworkRequest;
}

View file

@ -4,27 +4,71 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreSetup, EnvironmentMode, PluginInitializerContext, Logger } from 'src/core/server';
import { ServerFacade } from './types';
import { initServerWithKibana } from './kibana.index';
import { i18n } from '@kbn/i18n';
import { CoreSetup, PluginInitializerContext, Logger } from 'src/core/server';
import { PluginSetupContract as FeaturesSetupContract } from '../../../../plugins/features/server';
import { initServer } from './init_server';
import { compose } from './lib/compose/kibana';
import {
noteSavedObjectType,
pinnedEventSavedObjectType,
timelineSavedObjectType,
} from './saved_objects';
export interface PluginsSetup {
features: FeaturesSetupContract;
}
export class Plugin {
name = 'siem';
private mode: EnvironmentMode;
private logger: Logger;
readonly name = 'siem';
private readonly logger: Logger;
private context: PluginInitializerContext;
constructor({ env, logger }: PluginInitializerContext) {
this.logger = logger.get('plugins', this.name);
this.mode = env.mode;
constructor(context: PluginInitializerContext) {
this.context = context;
this.logger = context.logger.get('plugins', this.name);
this.logger.info('NP plugin initialized');
this.logger.debug('Shim plugin initialized');
}
public setup(core: CoreSetup, dependencies: {}, __legacy: ServerFacade) {
this.logger.info('NP plugin setup');
public setup(core: CoreSetup, plugins: PluginsSetup) {
this.logger.debug('Shim plugin setup');
initServerWithKibana(__legacy, this.logger, this.mode);
plugins.features.registerFeature({
id: this.name,
name: i18n.translate('xpack.siem.featureRegistry.linkSiemTitle', {
defaultMessage: 'SIEM',
}),
icon: 'securityAnalyticsApp',
navLinkId: 'siem',
app: ['siem', 'kibana'],
catalogue: ['siem'],
privileges: {
all: {
api: ['siem'],
savedObject: {
all: [noteSavedObjectType, pinnedEventSavedObjectType, timelineSavedObjectType],
read: ['config'],
},
ui: ['show'],
},
read: {
api: ['siem'],
savedObject: {
all: [],
read: [
'config',
noteSavedObjectType,
pinnedEventSavedObjectType,
timelineSavedObjectType,
],
},
ui: ['show'],
},
},
});
this.logger.info('NP plugin setup complete');
const libs = compose(core, this.context.env);
initServer(libs);
}
}

View file

@ -8,29 +8,21 @@ import { Legacy } from 'kibana';
export interface ServerFacade {
config: Legacy.Server['config'];
getInjectedUiAppVars: Legacy.Server['getInjectedUiAppVars'];
indexPatternsServiceFactory: Legacy.Server['indexPatternsServiceFactory'];
injectUiAppVars: Legacy.Server['injectUiAppVars'];
plugins: {
alerting?: Legacy.Server['plugins']['alerting'];
elasticsearch: Legacy.Server['plugins']['elasticsearch'];
spaces: Legacy.Server['plugins']['spaces'];
xpack_main: Legacy.Server['plugins']['xpack_main'];
};
route: Legacy.Server['route'];
savedObjects: Legacy.Server['savedObjects'];
}
export interface RequestFacade {
auth: Legacy.Request['auth'];
getAlertsClient?: Legacy.Request['getAlertsClient'];
getActionsClient?: Legacy.Request['getActionsClient'];
getUiSettingsService: Legacy.Request['getUiSettingsService'];
headers: Legacy.Request['headers'];
method: Legacy.Request['method'];
params: Legacy.Request['params'];
payload: unknown;
query: Legacy.Request['query'];
server: {
plugins: { elasticsearch: Legacy.Request['server']['plugins']['elasticsearch'] };
};
}

View file

@ -0,0 +1,8 @@
{
"id": "siem",
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": ["xpack", "siem"],
"server": true,
"ui": false
}

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Observable } from 'rxjs';
import { schema, TypeOf } from '@kbn/config-schema';
import { PluginInitializerContext } from 'src/core/server';
export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
});
export const createConfig$ = (context: PluginInitializerContext) =>
context.config.create<TypeOf<typeof configSchema>>();
export type ConfigType = ReturnType<typeof createConfig$> extends Observable<infer T>
? T
: ReturnType<typeof createConfig$>;

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializerContext } from 'src/core/server';
import { Plugin } from './plugin';
import { configSchema, ConfigType } from './config';
export const plugin = (context: PluginInitializerContext) => {
return new Plugin(context);
};
export const config = { schema: configSchema };
export { ConfigType };

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Observable } from 'rxjs';
import { CoreSetup, PluginInitializerContext, Logger } from 'src/core/server';
import { createConfig$, ConfigType } from './config';
export class Plugin {
readonly name = 'siem';
private readonly logger: Logger;
// @ts-ignore-next-line TODO(rylnd): use it or lose it
private readonly config$: Observable<ConfigType>;
constructor(context: PluginInitializerContext) {
const { logger } = context;
this.logger = logger.get();
this.logger.debug('plugin initialized');
this.config$ = createConfig$(context);
}
public setup(core: CoreSetup, plugins: {}) {
this.logger.debug('plugin setup');
}
public start() {
this.logger.debug('plugin started');
}
public stop() {
this.logger.debug('plugin stopped');
}
}