diff --git a/x-pack/package.json b/x-pack/package.json index 5095d76a4f71d..edcaef783fa22 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -28,11 +28,14 @@ "@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers", "@kbn/test": "link:../packages/kbn-test", "@types/boom": "^4.3.8", + "@types/chance": "^1.0.1", + "@types/expect.js": "^0.3.29", "@types/hapi": "15.0.1", "@types/jest": "^22.2.3", "@types/joi": "^10.4.0", "@types/lodash": "^3.10.0", "@types/pngjs": "^3.3.0", + "@types/sinon": "^5.0.1", "abab": "^1.0.4", "ansicolors": "0.3.2", "aws-sdk": "2.2.33", @@ -89,6 +92,8 @@ "@kbn/ui-framework": "link:../packages/kbn-ui-framework", "@samverschueren/stream-to-observable": "^0.3.0", "@slack/client": "^4.2.2", + "@types/elasticsearch": "^5.0.24", + "@types/jsonwebtoken": "^7.2.7", "@types/uuid": "^3.4.3", "angular-paging": "2.2.1", "angular-resource": "1.4.9", @@ -121,6 +126,7 @@ "isomorphic-fetch": "2.2.1", "joi": "6.10.1", "jquery": "^3.3.1", + "jsonwebtoken": "^8.3.0", "jstimezonedetect": "1.0.5", "lodash": "3.10.1", "lodash.mean": "^4.1.0", diff --git a/x-pack/plugins/beats/index.ts b/x-pack/plugins/beats/index.ts index ce9b8147dbe4b..ced89c186f73e 100644 --- a/x-pack/plugins/beats/index.ts +++ b/x-pack/plugins/beats/index.ts @@ -15,6 +15,7 @@ export function beats(kibana: any) { config: () => Joi.object({ enabled: Joi.boolean().default(true), + encryptionKey: Joi.string(), enrollmentTokensTtlInSeconds: Joi.number() .integer() .min(1) diff --git a/x-pack/plugins/beats/readme.md b/x-pack/plugins/beats/readme.md new file mode 100644 index 0000000000000..fdd56a393e573 --- /dev/null +++ b/x-pack/plugins/beats/readme.md @@ -0,0 +1,7 @@ +# Documentation for Beats CM in x-pack kibana + +### Run tests + +``` +node scripts/jest.js plugins/beats --watch +``` diff --git a/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts index 283f65c1258ae..76fbf956dafc9 100644 --- a/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { flatten, get, omit } from 'lodash'; +import { flatten, get as _get, omit } from 'lodash'; import moment from 'moment'; import { INDEX_NAMES } from '../../../../common/constants'; import { @@ -35,7 +35,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { return null; } - return get(response, '_source.beat'); + return _get(response, '_source.beat'); } public async insert(beat: CMBeat) { @@ -73,22 +73,6 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { public async getWithIds(req: FrameworkRequest, beatIds: string[]) { const ids = beatIds.map(beatId => `beat:${beatId}`); - const params = { - _source: false, - body: { - ids, - }, - index: INDEX_NAMES.BEATS, - type: '_doc', - }; - const response = await this.framework.callWithRequest(req, 'mget', params); - return get(response, 'docs', []); - } - - // TODO merge with getBeatsWithIds - public async getVerifiedWithIds(req: FrameworkRequest, beatIds: string[]) { - const ids = beatIds.map(beatId => `beat:${beatId}`); - const params = { _sourceInclude: ['beat.id', 'beat.verified_on'], body: { @@ -98,7 +82,10 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { type: '_doc', }; const response = await this.framework.callWithRequest(req, 'mget', params); - return get(response, 'docs', []); + + return get(response, 'docs', []) + .filter((b: any) => b.found) + .map((b: any) => b._source.beat); } public async verifyBeats(req: FrameworkRequest, beatIds: string[]) { @@ -115,6 +102,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { ); const params = { + _sourceInclude: ['beat.id', 'beat.verified_on'], body, index: INDEX_NAMES.BEATS, refresh: 'wait_for', @@ -122,7 +110,11 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { }; const response = await this.framework.callWithRequest(req, 'bulk', params); - return get(response, 'items', []); + + return _get(response, 'items', []).map(b => ({ + ..._get(b, 'update.get._source.beat', {}), + updateStatus: _get(b, 'update.result', 'unknown error'), + })); } public async getAll(req: FrameworkRequest) { diff --git a/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts new file mode 100644 index 0000000000000..9de8297c0f73e --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts @@ -0,0 +1,123 @@ +/* + * 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 { omit } from 'lodash'; +import moment from 'moment'; + +import { + CMBeat, + CMBeatsAdapter, + CMTagAssignment, + FrameworkRequest, +} from '../../lib'; + +export class MemoryBeatsAdapter implements CMBeatsAdapter { + private beatsDB: CMBeat[]; + + constructor(beatsDB: CMBeat[]) { + this.beatsDB = beatsDB; + } + + public async get(id: string) { + return this.beatsDB.find(beat => beat.id === id); + } + + public async insert(beat: CMBeat) { + this.beatsDB.push(beat); + } + + public async update(beat: CMBeat) { + const beatIndex = this.beatsDB.findIndex(b => b.id === beat.id); + + this.beatsDB[beatIndex] = { + ...this.beatsDB[beatIndex], + ...beat, + }; + } + + public async getWithIds(req: FrameworkRequest, beatIds: string[]) { + return this.beatsDB.filter(beat => beatIds.includes(beat.id)); + } + + public async verifyBeats(req: FrameworkRequest, beatIds: string[]) { + if (!Array.isArray(beatIds) || beatIds.length === 0) { + return []; + } + + const verifiedOn = moment().toJSON(); + + this.beatsDB.forEach((beat, i) => { + if (beatIds.includes(beat.id)) { + this.beatsDB[i].verified_on = verifiedOn; + } + }); + + return this.beatsDB.filter(beat => beatIds.includes(beat.id)); + } + + public async getAll(req: FrameworkRequest) { + return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])); + } + + public async removeTagsFromBeats( + req: FrameworkRequest, + removals: CMTagAssignment[] + ): Promise { + const beatIds = removals.map(r => r.beatId); + + const response = this.beatsDB + .filter(beat => beatIds.includes(beat.id)) + .map(beat => { + const tagData = removals.find(r => r.beatId === beat.id); + if (tagData) { + if (beat.tags) { + beat.tags = beat.tags.filter(tag => tag !== tagData.tag); + } + } + return beat; + }); + + return response.map((item: CMBeat, resultIdx: number) => ({ + idxInRequest: removals[resultIdx].idxInRequest, + result: 'updated', + status: 200, + })); + } + + public async assignTagsToBeats( + req: FrameworkRequest, + assignments: CMTagAssignment[] + ): Promise { + const beatIds = assignments.map(r => r.beatId); + + this.beatsDB.filter(beat => beatIds.includes(beat.id)).map(beat => { + // get tags that need to be assigned to this beat + const tags = assignments + .filter(a => a.beatId === beat.id) + .map((t: CMTagAssignment) => t.tag); + + if (tags.length > 0) { + if (!beat.tags) { + beat.tags = []; + } + const nonExistingTags = tags.filter( + (t: string) => beat.tags && !beat.tags.includes(t) + ); + + if (nonExistingTags.length > 0) { + beat.tags = beat.tags.concat(nonExistingTags); + } + } + return beat; + }); + + return assignments.map((item: CMTagAssignment, resultIdx: number) => ({ + idxInRequest: assignments[resultIdx].idxInRequest, + result: 'updated', + status: 200, + })); + } +} diff --git a/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts index 6fc2fc4853b03..a54997370ac5d 100644 --- a/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts @@ -19,18 +19,25 @@ import { export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { public version: string; - private server: Server; + private cryptoHash: string | null; constructor(hapiServer: Server) { this.server = hapiServer; this.version = hapiServer.plugins.kibana.status.plugin.version; + this.cryptoHash = null; + + this.validateConfig(); } public getSetting(settingPath: string) { - // TODO type check this properly + // TODO type check server properly + if (settingPath === 'xpack.beats.encryptionKey') { + // @ts-ignore + return this.server.config().get(settingPath) || this.cryptoHash; + } // @ts-ignore - return this.server.config().get(settingPath); + return this.server.config().get(settingPath) || this.cryptoHash; } public exposeStaticDir(urlPath: string, dir: string): void { @@ -79,4 +86,17 @@ export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { const fields = await callWithRequest(internalRequest, ...rest); return fields; } + + private validateConfig() { + // @ts-ignore + const config = this.server.config(); + const encryptionKey = config.get('xpack.beats.encryptionKey'); + + if (!encryptionKey) { + this.server.log( + 'Using a default encryption key for xpack.beats.encryptionKey. It is recommended that you set xpack.beats.encryptionKey in kibana.yml with a unique token' + ); + this.cryptoHash = 'xpack_beats_default_encryptionKey'; + } + } } diff --git a/x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts new file mode 100644 index 0000000000000..9c928a05cfd5a --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts @@ -0,0 +1,83 @@ +/* + * 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 { Client } from 'elasticsearch'; +import { Request } from 'hapi'; +import { get } from 'lodash'; +import { + BackendFrameworkAdapter, + FrameworkRequest, + FrameworkRouteOptions, + WrappableRequest, +} from '../../../lib'; + +interface TestSettings { + enrollmentTokensTtlInSeconds: number; + encryptionKey: string; +} + +export class TestingBackendFrameworkAdapter implements BackendFrameworkAdapter { + public version: string; + private client: Client | null; + private settings: TestSettings; + + constructor(client: Client | null, settings: TestSettings) { + this.client = client; + this.settings = settings || { + encryptionKey: 'something_who_cares', + enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes + }; + this.version = 'testing'; + } + + public getSetting(settingPath: string) { + switch (settingPath) { + case 'xpack.beats.enrollmentTokensTtlInSeconds': + return this.settings.enrollmentTokensTtlInSeconds; + case 'xpack.beats.encryptionKey': + return this.settings.encryptionKey; + } + } + + public exposeStaticDir(urlPath: string, dir: string): void { + // not yet testable + } + + public registerRoute( + route: FrameworkRouteOptions + ) { + // not yet testable + } + + public installIndexTemplate(name: string, template: {}) { + if (this.client) { + return this.client.indices.putTemplate({ + body: template, + name, + }); + } + } + + public async callWithInternalUser(esMethod: string, options: {}) { + const api = get(this.client, esMethod); + + api(options); + + return await api(options); + } + + public async callWithRequest( + req: FrameworkRequest, + esMethod: string, + options: {} + ) { + const api = get(this.client, esMethod); + + api(options); + + return await api(options); + } +} diff --git a/x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts index 2293ba77677fd..44aea344151ca 100644 --- a/x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts @@ -25,7 +25,7 @@ export class ElasticsearchTagsAdapter implements CMTagsAdapter { // TODO abstract to kibana adapter as the more generic getDocs const params = { - _source: false, + _sourceInclude: ['tag.configuration_blocks'], body: { ids, }, @@ -33,7 +33,13 @@ export class ElasticsearchTagsAdapter implements CMTagsAdapter { type: '_doc', }; const response = await this.framework.callWithRequest(req, 'mget', params); - return get(response, 'docs', []); + + return get(response, 'docs', []) + .filter((b: any) => b.found) + .map((b: any) => ({ + ...b._source.tag, + id: b._id.replace('tag:', ''), + })); } public async upsertTag(req: FrameworkRequest, tag: BeatTag) { diff --git a/x-pack/plugins/beats/server/lib/adapters/tags/memory_tags_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/tags/memory_tags_adapter.ts new file mode 100644 index 0000000000000..4d2a80e1b39c2 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/tags/memory_tags_adapter.ts @@ -0,0 +1,29 @@ +/* + * 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 { BeatTag, CMTagsAdapter, FrameworkRequest } from '../../lib'; + +export class MemoryTagsAdapter implements CMTagsAdapter { + private tagsDB: BeatTag[] = []; + + constructor(tagsDB: BeatTag[]) { + this.tagsDB = tagsDB; + } + + public async getTagsWithIds(req: FrameworkRequest, tagIds: string[]) { + return this.tagsDB.filter(tag => tagIds.includes(tag.id)); + } + + public async upsertTag(req: FrameworkRequest, tag: BeatTag) { + const existingTagIndex = this.tagsDB.findIndex(t => t.id === tag.id); + if (existingTagIndex !== -1) { + this.tagsDB[existingTagIndex] = tag; + } else { + this.tagsDB.push(tag); + } + return tag; + } +} diff --git a/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts index c8969c7ab08d0..7a63c784ecf6a 100644 --- a/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts @@ -79,5 +79,6 @@ export class ElasticsearchTokensAdapter implements CMTokensAdapter { }; await this.framework.callWithRequest(req, 'bulk', params); + return tokens; } } diff --git a/x-pack/plugins/beats/server/lib/adapters/tokens/memory_tokens_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/tokens/memory_tokens_adapter.ts new file mode 100644 index 0000000000000..1734327007e08 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/tokens/memory_tokens_adapter.ts @@ -0,0 +1,47 @@ +/* + * 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 { CMTokensAdapter, EnrollmentToken, FrameworkRequest } from '../../lib'; + +export class MemoryTokensAdapter implements CMTokensAdapter { + private tokenDB: EnrollmentToken[]; + + constructor(tokenDB: EnrollmentToken[]) { + this.tokenDB = tokenDB; + } + + public async deleteEnrollmentToken(enrollmentToken: string) { + const index = this.tokenDB.findIndex( + token => token.token === enrollmentToken + ); + + if (index > -1) { + this.tokenDB.splice(index, 1); + } + } + + public async getEnrollmentToken( + tokenString: string + ): Promise { + return new Promise(resolve => { + return resolve(this.tokenDB.find(token => token.token === tokenString)); + }); + } + + public async upsertTokens(req: FrameworkRequest, tokens: EnrollmentToken[]) { + tokens.forEach(token => { + const existingIndex = this.tokenDB.findIndex( + t => t.token === token.token + ); + if (existingIndex !== -1) { + this.tokenDB[existingIndex] = token; + } else { + this.tokenDB.push(token); + } + }); + return tokens; + } +} diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/assign_tags.test.ts b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/assign_tags.test.ts new file mode 100644 index 0000000000000..c1e360ffd75f4 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/assign_tags.test.ts @@ -0,0 +1,231 @@ +/* + * 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 expect from 'expect.js'; +import { wrapRequest } from '../../../../utils/wrap_request'; +import { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter'; +import { TestingBackendFrameworkAdapter } from '../../../adapters/famework/kibana/testing_framework_adapter'; +import { MemoryTagsAdapter } from '../../../adapters/tags/memory_tags_adapter'; +import { MemoryTokensAdapter } from '../../../adapters/tokens/memory_tokens_adapter'; + +import { BeatTag, CMBeat } from './../../../lib'; + +import { CMBeatsDomain } from '../../beats'; +import { CMTagsDomain } from '../../tags'; +import { CMTokensDomain } from '../../tokens'; + +import Chance from 'chance'; + +const seed = Date.now(); +const chance = new Chance(seed); + +const fakeReq = wrapRequest({ + headers: {}, + info: {}, + params: {}, + payload: {}, + query: {}, +}); + +const settings = { + encryptionKey: 'something_who_cares', + enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes +}; + +describe('Beats Domain Lib', () => { + let beatsLib: CMBeatsDomain; + let beatsDB: CMBeat[] = []; + let tagsDB: BeatTag[] = []; + + describe('assign_tags_to_beats', () => { + beforeEach(async () => { + beatsDB = [ + { + access_token: '9a6c99ae0fd84b068819701169cd8a4b', + host_ip: '1.2.3.4', + host_name: 'foo.bar.com', + id: 'qux', + type: 'filebeat', + }, + { + access_token: '188255eb560a4448b72656c5e99cae6f', + host_ip: '22.33.11.44', + host_name: 'baz.bar.com', + id: 'baz', + type: 'metricbeat', + }, + { + access_token: '93c4a4dd08564c189a7ec4e4f046b975', + host_ip: '1.2.3.4', + host_name: 'foo.bar.com', + id: 'foo', + tags: ['production', 'qa'], + type: 'metricbeat', + verified_on: '2018-05-15T16:25:38.924Z', + }, + { + access_token: '3c4a4dd08564c189a7ec4e4f046b9759', + host_ip: '11.22.33.44', + host_name: 'foo.com', + id: 'bar', + type: 'filebeat', + }, + ]; + tagsDB = [ + { + configuration_blocks: [], + id: 'production', + }, + { + configuration_blocks: [], + id: 'development', + }, + { + configuration_blocks: [], + id: 'qa', + }, + ]; + const framework = new TestingBackendFrameworkAdapter(null, settings); + + const tokensLib = new CMTokensDomain(new MemoryTokensAdapter([]), { + framework, + }); + + const tagsLib = new CMTagsDomain(new MemoryTagsAdapter(tagsDB)); + + beatsLib = new CMBeatsDomain(new MemoryBeatsAdapter(beatsDB), { + tags: tagsLib, + tokens: tokensLib, + }); + }); + + it('should add a single tag to a single beat', async () => { + const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + { beatId: 'bar', tag: 'production' }, + ]); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' }, + ]); + }); + + it('should not re-add an existing tag to a beat', async () => { + const tags = ['production']; + + let beat = beatsDB.find(b => b.id === 'foo') as any; + expect(beat.tags).to.eql([...tags, 'qa']); + + // Adding the existing tag + const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + { beatId: 'foo', tag: 'production' }, + ]); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' }, + ]); + + beat = beatsDB.find(b => b.id === 'foo') as any; + expect(beat.tags).to.eql([...tags, 'qa']); + }); + + it('should add a single tag to a multiple beats', async () => { + const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + { beatId: 'foo', tag: 'development' }, + { beatId: 'bar', tag: 'development' }, + ]); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + let beat = beatsDB.find(b => b.id === 'foo') as any; + expect(beat.tags).to.eql(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it + + beat = beatsDB.find(b => b.id === 'bar') as any; + expect(beat.tags).to.eql(['development']); + }); + + it('should add multiple tags to a single beat', async () => { + const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + { beatId: 'bar', tag: 'development' }, + { beatId: 'bar', tag: 'production' }, + ]); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + const beat = beatsDB.find(b => b.id === 'bar') as any; + expect(beat.tags).to.eql(['development', 'production']); + }); + + it('should add multiple tags to a multiple beats', async () => { + const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + { beatId: 'foo', tag: 'development' }, + { beatId: 'bar', tag: 'production' }, + ]); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + let beat = beatsDB.find(b => b.id === 'foo') as any; + expect(beat.tags).to.eql(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it + + beat = beatsDB.find(b => b.id === 'bar') as any; + expect(beat.tags).to.eql(['production']); + }); + + it('should return errors for non-existent beats', async () => { + const nonExistentBeatId = chance.word(); + + const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + { beatId: nonExistentBeatId, tag: 'production' }, + ]); + + expect(apiResponse.assignments).to.eql([ + { status: 404, result: `Beat ${nonExistentBeatId} not found` }, + ]); + }); + + it('should return errors for non-existent tags', async () => { + const nonExistentTag = chance.word(); + + const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + { beatId: 'bar', tag: nonExistentTag }, + ]); + + expect(apiResponse.assignments).to.eql([ + { status: 404, result: `Tag ${nonExistentTag} not found` }, + ]); + + const beat = beatsDB.find(b => b.id === 'bar') as any; + expect(beat).to.not.have.property('tags'); + }); + + it('should return errors for non-existent beats and tags', async () => { + const nonExistentBeatId = chance.word(); + const nonExistentTag = chance.word(); + + const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + { beatId: nonExistentBeatId, tag: nonExistentTag }, + ]); + + expect(apiResponse.assignments).to.eql([ + { + result: `Beat ${nonExistentBeatId} and tag ${nonExistentTag} not found`, + status: 404, + }, + ]); + + const beat = beatsDB.find(b => b.id === 'bar') as any; + expect(beat).to.not.have.property('tags'); + }); + }); +}); diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts new file mode 100644 index 0000000000000..f52f1096227c0 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts @@ -0,0 +1,136 @@ +/* + * 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 expect from 'expect.js'; +import { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter'; +import { TestingBackendFrameworkAdapter } from '../../../adapters/famework/kibana/testing_framework_adapter'; +import { MemoryTagsAdapter } from '../../../adapters/tags/memory_tags_adapter'; +import { MemoryTokensAdapter } from '../../../adapters/tokens/memory_tokens_adapter'; + +import { BeatTag, CMBeat, EnrollmentToken } from './../../../lib'; + +import { CMBeatsDomain } from '../../beats'; +import { CMTagsDomain } from '../../tags'; +import { CMTokensDomain } from '../../tokens'; + +import Chance from 'chance'; +import { sign as signToken } from 'jsonwebtoken'; +import { omit } from 'lodash'; +import moment from 'moment'; + +const seed = Date.now(); +const chance = new Chance(seed); + +const settings = { + encryptionKey: 'something_who_cares', + enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes +}; + +describe('Beats Domain Lib', () => { + let beatsLib: CMBeatsDomain; + let tokensLib: CMTokensDomain; + + let beatsDB: CMBeat[] = []; + let tagsDB: BeatTag[] = []; + let tokensDB: EnrollmentToken[] = []; + let validEnrollmentToken: string; + let beatId: string; + let beat: Partial; + + describe('enroll_beat', () => { + beforeEach(async () => { + validEnrollmentToken = chance.word(); + beatId = chance.word(); + + beatsDB = []; + tagsDB = []; + tokensDB = [ + { + expires_on: moment() + .add(4, 'hours') + .toJSON(), + token: validEnrollmentToken, + }, + ]; + + const version = + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }); + + beat = { + host_name: 'foo.bar.com', + type: 'filebeat', + version, + }; + + const framework = new TestingBackendFrameworkAdapter(null, settings); + + tokensLib = new CMTokensDomain(new MemoryTokensAdapter(tokensDB), { + framework, + }); + + const tagsLib = new CMTagsDomain(new MemoryTagsAdapter(tagsDB)); + + beatsLib = new CMBeatsDomain(new MemoryBeatsAdapter(beatsDB), { + tags: tagsLib, + tokens: tokensLib, + }); + }); + + it('should enroll beat, returning an access token', async () => { + const { token } = await tokensLib.getEnrollmentToken( + validEnrollmentToken + ); + + expect(token).to.equal(validEnrollmentToken); + const { accessToken } = await beatsLib.enrollBeat( + beatId, + '192.168.1.1', + omit(beat, 'enrollment_token') + ); + + expect(beatsDB.length).to.eql(1); + expect(beatsDB[0]).to.have.property('host_ip'); + + expect(accessToken).to.eql(beatsDB[0].access_token); + + await tokensLib.deleteEnrollmentToken(validEnrollmentToken); + + expect(tokensDB.length).to.eql(0); + }); + + it('should reject an invalid enrollment token', async () => { + const { token } = await tokensLib.getEnrollmentToken(chance.word()); + + expect(token).to.eql(null); + }); + + it('should reject an expired enrollment token', async () => { + const { token } = await tokensLib.getEnrollmentToken( + signToken({}, settings.encryptionKey, { + expiresIn: '-1min', + }) + ); + + expect(token).to.eql(null); + }); + + it('should delete the given enrollment token so it may not be reused', async () => { + expect(tokensDB[0].token).to.eql(validEnrollmentToken); + await tokensLib.deleteEnrollmentToken(validEnrollmentToken); + expect(tokensDB.length).to.eql(0); + + const { token } = await tokensLib.getEnrollmentToken( + validEnrollmentToken + ); + + expect(token).to.eql(null); + }); + }); +}); diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/verify.test.ts b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/verify.test.ts new file mode 100644 index 0000000000000..7310b101a351a --- /dev/null +++ b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/verify.test.ts @@ -0,0 +1,192 @@ +/* + * 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 expect from 'expect.js'; +import { wrapRequest } from '../../../../utils/wrap_request'; +import { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter'; +import { TestingBackendFrameworkAdapter } from '../../../adapters/famework/kibana/testing_framework_adapter'; +import { MemoryTagsAdapter } from '../../../adapters/tags/memory_tags_adapter'; +import { MemoryTokensAdapter } from '../../../adapters/tokens/memory_tokens_adapter'; + +import { BeatTag, CMBeat, EnrollmentToken } from './../../../lib'; + +import { CMBeatsDomain } from '../../beats'; +import { CMTagsDomain } from '../../tags'; +import { CMTokensDomain } from '../../tokens'; + +import Chance from 'chance'; + +const seed = Date.now(); +const chance = new Chance(seed); + +const settings = { + encryptionKey: 'something_who_cares', + enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes +}; + +const fakeReq = wrapRequest({ + headers: {}, + info: {}, + params: {}, + payload: {}, + query: {}, +}); + +describe('Beats Domain Lib', () => { + let beatsLib: CMBeatsDomain; + let tokensLib: CMTokensDomain; + + let beatsDB: CMBeat[] = []; + let tagsDB: BeatTag[] = []; + let tokensDB: EnrollmentToken[] = []; + + describe('verify_beat', () => { + beforeEach(async () => { + beatsDB = [ + { + access_token: '9a6c99ae0fd84b068819701169cd8a4b', + host_ip: '1.2.3.4', + host_name: 'foo.bar.com', + id: 'qux', + type: 'filebeat', + }, + { + access_token: '188255eb560a4448b72656c5e99cae6f', + host_ip: '22.33.11.44', + host_name: 'baz.bar.com', + id: 'baz', + type: 'metricbeat', + }, + { + access_token: '93c4a4dd08564c189a7ec4e4f046b975', + host_ip: '1.2.3.4', + host_name: 'foo.bar.com', + id: 'foo', + tags: ['production', 'qa'], + type: 'metricbeat', + verified_on: '2018-05-15T16:25:38.924Z', + }, + { + access_token: '3c4a4dd08564c189a7ec4e4f046b9759', + host_ip: '11.22.33.44', + host_name: 'foo.com', + id: 'bar', + type: 'filebeat', + }, + ]; + tagsDB = [ + { + configuration_blocks: [], + id: 'production', + }, + { + configuration_blocks: [], + id: 'development', + }, + { + configuration_blocks: [], + id: 'qa', + }, + ]; + tokensDB = []; + + const framework = new TestingBackendFrameworkAdapter(null, settings); + + tokensLib = new CMTokensDomain(new MemoryTokensAdapter(tokensDB), { + framework, + }); + + const tagsLib = new CMTagsDomain(new MemoryTagsAdapter(tagsDB)); + + beatsLib = new CMBeatsDomain(new MemoryBeatsAdapter(beatsDB), { + tags: tagsLib, + tokens: tokensLib, + }); + }); + + it('should return errors for non-existent beats', async () => { + const nonExistentBeatId = chance.word(); + + interface Beats { + id: string; + status?: number; + result?: string; + } + + const beats: Beats[] = [{ id: 'bar' }, { id: nonExistentBeatId }]; + const beatIds = beats.map(b => b.id); + + const { + verifiedBeatIds, + alreadyVerifiedBeatIds, + nonExistentBeatIds, + } = await beatsLib.verifyBeats(fakeReq, beatIds); + + // TODO calculation of status should be done in-lib, w/switch statement here + beats.forEach(b => { + if (nonExistentBeatIds.includes(b.id)) { + b.status = 404; + b.result = 'not found'; + } else if (alreadyVerifiedBeatIds.includes(b.id)) { + b.status = 200; + b.result = 'already verified'; + } else if (verifiedBeatIds.includes(b.id)) { + b.status = 200; + b.result = 'verified'; + } else { + b.status = 400; + b.result = 'not verified'; + } + }); + + const response = { beats }; + expect(response.beats).to.eql([ + { id: 'bar', status: 200, result: 'verified' }, + { id: nonExistentBeatId, status: 404, result: 'not found' }, + ]); + }); + + it('should not re-verify already-verified beats', async () => { + interface Beats { + id: string; + status?: number; + result?: string; + } + + const beats: Beats[] = [{ id: 'foo' }, { id: 'bar' }]; + const beatIds = beats.map(b => b.id); + + const { + verifiedBeatIds, + alreadyVerifiedBeatIds, + nonExistentBeatIds, + } = await beatsLib.verifyBeats(fakeReq, beatIds); + + // TODO calculation of status should be done in-lib, w/switch statement here + beats.forEach(beat => { + if (nonExistentBeatIds.includes(beat.id)) { + beat.status = 404; + beat.result = 'not found'; + } else if (alreadyVerifiedBeatIds.includes(beat.id)) { + beat.status = 200; + beat.result = 'already verified'; + } else if (verifiedBeatIds.includes(beat.id)) { + beat.status = 200; + beat.result = 'verified'; + } else { + beat.status = 400; + beat.result = 'not verified'; + } + }); + + const response = { beats }; + expect(response.beats).to.eql([ + { id: 'foo', status: 200, result: 'already verified' }, + { id: 'bar', status: 200, result: 'verified' }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/tokens.test.ts b/x-pack/plugins/beats/server/lib/domains/__tests__/tokens.test.ts new file mode 100644 index 0000000000000..174c7d628778c --- /dev/null +++ b/x-pack/plugins/beats/server/lib/domains/__tests__/tokens.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from 'expect.js'; +import { wrapRequest } from '../../../utils/wrap_request'; +import { TestingBackendFrameworkAdapter } from '../../adapters/famework/kibana/testing_framework_adapter'; +import { MemoryTokensAdapter } from '../../adapters/tokens/memory_tokens_adapter'; +import { EnrollmentToken } from '../../lib'; +import { CMTokensDomain } from '../tokens'; + +import Chance from 'chance'; +import moment from 'moment'; + +const seed = Date.now(); +const chance = new Chance(seed); + +const fakeReq = wrapRequest({ + headers: {}, + info: {}, + params: {}, + payload: {}, + query: {}, +}); + +const settings = { + encryptionKey: 'something_who_cares', + enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes +}; + +describe('Token Domain Lib', () => { + let tokensLib: CMTokensDomain; + let tokensDB: EnrollmentToken[] = []; + + beforeEach(async () => { + tokensDB = []; + const framework = new TestingBackendFrameworkAdapter(null, settings); + + tokensLib = new CMTokensDomain(new MemoryTokensAdapter(tokensDB), { + framework, + }); + }); + + it('should generate webtokens with a qty of 1', async () => { + const tokens = await tokensLib.createEnrollmentTokens(fakeReq, 1); + + expect(tokens.length).to.be(1); + + expect(typeof tokens[0]).to.be('string'); + }); + + it('should create the specified number of tokens', async () => { + const numTokens = chance.integer({ min: 1, max: 20 }); + const tokensFromApi = await tokensLib.createEnrollmentTokens( + fakeReq, + numTokens + ); + + expect(tokensFromApi.length).to.eql(numTokens); + expect(tokensFromApi).to.eql(tokensDB.map((t: EnrollmentToken) => t.token)); + }); + + it('should set token expiration to 10 minutes from now by default', async () => { + await tokensLib.createEnrollmentTokens(fakeReq, 1); + + const token = tokensDB[0]; + + // We do a fuzzy check to see if the token expires between 9 and 10 minutes + // from now because a bit of time has elapsed been the creation of the + // tokens and this check. + const tokenExpiresOn = moment(token.expires_on).valueOf(); + + // Because sometimes the test runs so fast it it equal, and we dont use expect.js version that has toBeLessThanOrEqualTo + const tenMinutesFromNow = moment() + .add('10', 'minutes') + .add('1', 'seconds') + .valueOf(); + + const almostTenMinutesFromNow = moment(tenMinutesFromNow) + .subtract('2', 'seconds') + .valueOf(); + expect(tokenExpiresOn).to.be.lessThan(tenMinutesFromNow); + expect(tokenExpiresOn).to.be.greaterThan(almostTenMinutesFromNow); + }); +}); diff --git a/x-pack/plugins/beats/server/lib/domains/beats.ts b/x-pack/plugins/beats/server/lib/domains/beats.ts index c0d9ec704e2b1..0d5e068ff4ff7 100644 --- a/x-pack/plugins/beats/server/lib/domains/beats.ts +++ b/x-pack/plugins/beats/server/lib/domains/beats.ts @@ -11,7 +11,6 @@ */ import { uniq } from 'lodash'; -import uuid from 'uuid'; import { findNonExistentItems } from '../../utils/find_non_existent_items'; import { @@ -45,15 +44,16 @@ export class CMBeatsDomain { ) { const beat = await this.adapter.get(beatId); + const { verified: isAccessTokenValid } = this.tokens.verifyToken( + beat ? beat.access_token : '', + accessToken + ); + // TODO make return type enum if (beat === null) { return 'beat-not-found'; } - const isAccessTokenValid = this.tokens.areTokensEqual( - beat.access_token, - accessToken - ); if (!isAccessTokenValid) { return 'invalid-access-token'; } @@ -74,8 +74,7 @@ export class CMBeatsDomain { remoteAddress: string, beat: Partial ) { - // TODO move this to the token lib - const accessToken = uuid.v4().replace(/-/g, ''); + const accessToken = this.tokens.generateAccessToken(); await this.adapter.insert({ ...beat, access_token: accessToken, @@ -136,37 +135,28 @@ export class CMBeatsDomain { // TODO cleanup return value, should return a status enum public async verifyBeats(req: FrameworkRequest, beatIds: string[]) { - const beatsFromEs = await this.adapter.getVerifiedWithIds(req, beatIds); - - const nonExistentBeatIds = beatsFromEs.reduce( - (nonExistentIds: any, beatFromEs: any, idx: any) => { - if (!beatFromEs.found) { - nonExistentIds.push(beatIds[idx]); - } - return nonExistentIds; - }, - [] - ); + const beatsFromEs = await this.adapter.getWithIds(req, beatIds); + + const nonExistentBeatIds = findNonExistentItems(beatsFromEs, beatIds); const alreadyVerifiedBeatIds = beatsFromEs - .filter((beat: any) => beat.found) - .filter((beat: any) => beat._source.beat.hasOwnProperty('verified_on')) - .map((beat: any) => beat._source.beat.id); + .filter((beat: any) => beat.hasOwnProperty('verified_on')) + .map((beat: any) => beat.id); const toBeVerifiedBeatIds = beatsFromEs - .filter((beat: any) => beat.found) - .filter((beat: any) => !beat._source.beat.hasOwnProperty('verified_on')) - .map((beat: any) => beat._source.beat.id); + .filter((beat: any) => !beat.hasOwnProperty('verified_on')) + .map((beat: any) => beat.id); const verifications = await this.adapter.verifyBeats( req, toBeVerifiedBeatIds ); + return { alreadyVerifiedBeatIds, nonExistentBeatIds, toBeVerifiedBeatIds, - verifications, + verifiedBeatIds: verifications.map((v: any) => v.id), }; } @@ -182,7 +172,6 @@ export class CMBeatsDomain { }; const beats = await this.adapter.getWithIds(req, beatIds); const tags = await this.tags.getTagsWithIds(req, tagIds); - // Handle assignments containing non-existing beat IDs or tags const nonExistentBeatIds = findNonExistentItems(beats, beatIds); const nonExistentTags = findNonExistentItems(tags, tagIds); diff --git a/x-pack/plugins/beats/server/lib/domains/tokens.ts b/x-pack/plugins/beats/server/lib/domains/tokens.ts index 6e55d78ecdcc8..b2a9d283e484a 100644 --- a/x-pack/plugins/beats/server/lib/domains/tokens.ts +++ b/x-pack/plugins/beats/server/lib/domains/tokens.ts @@ -5,6 +5,7 @@ */ import { timingSafeEqual } from 'crypto'; +import { sign as signToken, verify as verifyToken } from 'jsonwebtoken'; import moment from 'moment'; import uuid from 'uuid'; import { CMTokensAdapter, FrameworkRequest } from '../lib'; @@ -26,32 +27,96 @@ export class CMTokensDomain { } public async getEnrollmentToken(enrollmentToken: string) { - return await this.adapter.getEnrollmentToken(enrollmentToken); + const fullToken = await this.adapter.getEnrollmentToken(enrollmentToken); + + if (!fullToken) { + return { + token: null, + expired: true, + expires_on: null, + }; + } + + const { verified, expired } = this.verifyToken( + enrollmentToken, + fullToken.token || '', + false + ); + + if (!verified) { + return { + expired, + token: null, + expires_on: null, + }; + } + + return { ...fullToken, expired }; } public async deleteEnrollmentToken(enrollmentToken: string) { return await this.adapter.deleteEnrollmentToken(enrollmentToken); } - public areTokensEqual(token1: string, token2: string) { + public verifyToken(recivedToken: string, token2: string, decode = true) { + let tokenDecoded = true; + let expired = false; + + if (decode) { + const enrollmentTokenSecret = this.framework.getSetting( + 'xpack.beats.encryptionKey' + ); + + try { + verifyToken(recivedToken, enrollmentTokenSecret); + tokenDecoded = true; + } catch (err) { + if (err.name === 'TokenExpiredError') { + expired = true; + } + tokenDecoded = false; + } + } + if ( - typeof token1 !== 'string' || + typeof recivedToken !== 'string' || typeof token2 !== 'string' || - token1.length !== token2.length + recivedToken.length !== token2.length ) { // This prevents a more subtle timing attack where we know already the tokens aren't going to // match but still we don't return fast. Instead we compare two pre-generated random tokens using // the same comparison algorithm that we would use to compare two equal-length tokens. - return timingSafeEqual( - Buffer.from(RANDOM_TOKEN_1, 'utf8'), - Buffer.from(RANDOM_TOKEN_2, 'utf8') - ); + return { + expired, + verified: + timingSafeEqual( + Buffer.from(RANDOM_TOKEN_1, 'utf8'), + Buffer.from(RANDOM_TOKEN_2, 'utf8') + ) && tokenDecoded, + }; } - return timingSafeEqual( - Buffer.from(token1, 'utf8'), - Buffer.from(token2, 'utf8') + return { + expired, + verified: + timingSafeEqual( + Buffer.from(recivedToken, 'utf8'), + Buffer.from(token2, 'utf8') + ) && tokenDecoded, + }; + } + + public generateAccessToken() { + const enrollmentTokenSecret = this.framework.getSetting( + 'xpack.beats.encryptionKey' ); + + const tokenData = { + created: moment().toJSON(), + randomHash: this.createRandomHash(), + }; + + return signToken(tokenData, enrollmentTokenSecret); } public async createEnrollmentTokens( @@ -62,6 +127,7 @@ export class CMTokensDomain { const enrollmentTokensTtlInSeconds = this.framework.getSetting( 'xpack.beats.enrollmentTokensTtlInSeconds' ); + const enrollmentTokenExpiration = moment() .add(enrollmentTokensTtlInSeconds, 'seconds') .toJSON(); @@ -69,7 +135,7 @@ export class CMTokensDomain { while (tokens.length < numTokens) { tokens.push({ expires_on: enrollmentTokenExpiration, - token: uuid.v4().replace(/-/g, ''), + token: this.createRandomHash(), }); } @@ -77,4 +143,8 @@ export class CMTokensDomain { return tokens.map(token => token.token); } + + private createRandomHash() { + return uuid.v4().replace(/-/g, ''); + } } diff --git a/x-pack/plugins/beats/server/lib/lib.ts b/x-pack/plugins/beats/server/lib/lib.ts index 37d0a989e4cf5..6aab0acd733d8 100644 --- a/x-pack/plugins/beats/server/lib/lib.ts +++ b/x-pack/plugins/beats/server/lib/lib.ts @@ -43,16 +43,16 @@ export interface ConfigurationBlock { export interface CMBeat { id: string; access_token: string; - verified_on: string; + verified_on?: string; type: string; - version: string; + version?: string; host_ip: string; host_name: string; - ephemeral_id: string; - local_configuration_yml: string; - tags: string; - central_configuration_yml: string; - metadata: {}; + ephemeral_id?: string; + local_configuration_yml?: string; + tags?: string[]; + central_configuration_yml?: string; + metadata?: {}; } export interface BeatTag { @@ -68,7 +68,10 @@ export interface EnrollmentToken { export interface CMTokensAdapter { deleteEnrollmentToken(enrollmentToken: string): Promise; getEnrollmentToken(enrollmentToken: string): Promise; - upsertTokens(req: FrameworkRequest, tokens: EnrollmentToken[]): Promise; + upsertTokens( + req: FrameworkRequest, + tokens: EnrollmentToken[] + ): Promise; } // FIXME: fix getTagsWithIds return type @@ -84,7 +87,6 @@ export interface CMBeatsAdapter { get(id: string): any; getAll(req: FrameworkRequest): any; getWithIds(req: FrameworkRequest, beatIds: string[]): any; - getVerifiedWithIds(req: FrameworkRequest, beatIds: string[]): any; verifyBeats(req: FrameworkRequest, beatIds: string[]): any; removeTagsFromBeats( req: FrameworkRequest, @@ -108,7 +110,7 @@ export interface CMTagAssignment { export interface BackendFrameworkAdapter { version: string; - getSetting(settingPath: string): string | number; + getSetting(settingPath: string): any; exposeStaticDir(urlPath: string, dir: string): void; installIndexTemplate(name: string, template: {}): void; registerRoute( diff --git a/x-pack/plugins/beats/server/rest_api/beats/enroll.ts b/x-pack/plugins/beats/server/rest_api/beats/enroll.ts index fe154592564ae..c86e5272e1e23 100644 --- a/x-pack/plugins/beats/server/rest_api/beats/enroll.ts +++ b/x-pack/plugins/beats/server/rest_api/beats/enroll.ts @@ -33,17 +33,16 @@ export const createBeatEnrollmentRoute = (libs: CMServerLibs) => ({ const enrollmentToken = request.headers['kbn-beats-enrollment-token']; try { - const { - token, - expires_on: expiresOn, - } = await libs.tokens.getEnrollmentToken(enrollmentToken); + const { token, expires_on } = await libs.tokens.getEnrollmentToken( + enrollmentToken + ); + if (expires_on && moment(expires_on).isBefore(moment())) { + return reply({ message: 'Expired enrollment token' }).code(400); + } if (!token) { return reply({ message: 'Invalid enrollment token' }).code(400); } - if (moment(expiresOn).isBefore(moment())) { - return reply({ message: 'Expired enrollment token' }).code(400); - } const { accessToken } = await libs.beats.enrollBeat( beatId, request.info.remoteAddress, diff --git a/x-pack/plugins/beats/server/rest_api/beats/update.ts b/x-pack/plugins/beats/server/rest_api/beats/update.ts index 41d403399d45f..3683c02ca2ccb 100644 --- a/x-pack/plugins/beats/server/rest_api/beats/update.ts +++ b/x-pack/plugins/beats/server/rest_api/beats/update.ts @@ -15,7 +15,7 @@ export const createBeatUpdateRoute = (libs: CMServerLibs) => ({ auth: false, validate: { headers: Joi.object({ - 'kbn-beats-access-token': Joi.string().required(), + 'kbn-beats-access-token': Joi.string(), }).options({ allowUnknown: true, }), diff --git a/x-pack/plugins/beats/server/rest_api/beats/verify.ts b/x-pack/plugins/beats/server/rest_api/beats/verify.ts index 866fa77d0c337..7dba7f4e20692 100644 --- a/x-pack/plugins/beats/server/rest_api/beats/verify.ts +++ b/x-pack/plugins/beats/server/rest_api/beats/verify.ts @@ -29,22 +29,11 @@ export const createBeatVerificationRoute = (libs: CMServerLibs) => ({ try { const { - verifications, + verifiedBeatIds, alreadyVerifiedBeatIds, - toBeVerifiedBeatIds, nonExistentBeatIds, } = await libs.beats.verifyBeats(request, beatIds); - const verifiedBeatIds = verifications.reduce( - (verifiedBeatList: any, verification: any, idx: any) => { - if (verification.update.status === 200) { - verifiedBeatList.push(toBeVerifiedBeatIds[idx]); - } - return verifiedBeatList; - }, - [] - ); - // TODO calculation of status should be done in-lib, w/switch statement here beats.forEach(beat => { if (nonExistentBeatIds.includes(beat.id)) { diff --git a/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.js b/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.js index 03b04a2ef61d2..de79815258f7a 100644 --- a/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.js +++ b/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from 'expect.js'; import { wrapEsError } from './wrap_es_error'; describe('wrap_es_error', () => { @@ -34,7 +35,7 @@ describe('wrap_es_error', () => { expect(wrappedError.isBoom).to.be(true); expect(wrappedError.message).to.be( - 'Insufficient user permissions for managing Logstash pipelines' + 'Insufficient user permissions for managing Beats configuration' ); }); }); diff --git a/x-pack/plugins/beats/server/utils/find_non_existent_items.ts b/x-pack/plugins/beats/server/utils/find_non_existent_items.ts index 53e4066acc879..d6b2a0c9e143b 100644 --- a/x-pack/plugins/beats/server/utils/find_non_existent_items.ts +++ b/x-pack/plugins/beats/server/utils/find_non_existent_items.ts @@ -4,11 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -export function findNonExistentItems(items: any, requestedItems: any) { - return items.reduce((nonExistentItems: any, item: any, idx: any) => { - if (!item.found) { - nonExistentItems.push(requestedItems[idx]); - } - return nonExistentItems; - }, []); +interface RandomItem { + id: string; + [key: string]: any; +} + +export function findNonExistentItems(items: RandomItem[], requestedItems: any) { + return requestedItems.reduce( + (nonExistentItems: string[], requestedItem: string, idx: number) => { + if ( + items.findIndex( + (item: RandomItem) => item && item.id === requestedItem + ) === -1 + ) { + nonExistentItems.push(requestedItems[idx]); + } + return nonExistentItems; + }, + [] + ); } diff --git a/x-pack/plugins/beats/wallaby.js b/x-pack/plugins/beats/wallaby.js index c20488d35cfb6..8c0c4aa355925 100644 --- a/x-pack/plugins/beats/wallaby.js +++ b/x-pack/plugins/beats/wallaby.js @@ -3,15 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +const path = require('path'); +process.env.NODE_PATH = path.join(__dirname, '..', '..', 'node_modules'); module.exports = function (wallaby) { return { debug: true, files: [ - '../../tsconfig.json', + './tsconfig.json', //'plugins/beats/public/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', 'server/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', 'common/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', + '!**/*.test.ts', ], tests: ['**/*.test.ts'], @@ -22,6 +25,40 @@ module.exports = function (wallaby) { testFramework: 'jest', compilers: { '**/*.ts?(x)': wallaby.compilers.typeScript({ module: 'commonjs' }), + '**/*.js': wallaby.compilers.babel({ + babelrc: false, + presets: [require.resolve('@kbn/babel-preset/node_preset')], + }), + }, + setup: wallaby => { + const path = require('path'); + + const kibanaDirectory = path.resolve( + wallaby.localProjectDir, + '..', + '..', + '..' + ); + wallaby.testFramework.configure({ + rootDir: wallaby.localProjectDir, + moduleNameMapper: { + '^ui/(.*)': `${kibanaDirectory}/src/ui/public/$1`, + // eslint-disable-next-line + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `${kibanaDirectory}/src/dev/jest/mocks/file_mock.js`, + '\\.(css|less|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/style_mock.js`, + }, + + setupFiles: [ + `${kibanaDirectory}/x-pack/dev-tools/jest/setup/enzyme.js`, + ], + snapshotSerializers: [ + `${kibanaDirectory}/node_modules/enzyme-to-json/serializer`, + ], + transform: { + '^.+\\.js$': `${kibanaDirectory}/src/dev/jest/babel_transform.js`, + //"^.+\\.tsx?$": `${kibanaDirectory}/src/dev/jest/ts_transform.js`, + }, + }); }, }; }; diff --git a/x-pack/test/api_integration/apis/beats/enroll_beat.js b/x-pack/test/api_integration/apis/beats/enroll_beat.js index 91317bca976ee..1dde64c9ee1d8 100644 --- a/x-pack/test/api_integration/apis/beats/enroll_beat.js +++ b/x-pack/test/api_integration/apis/beats/enroll_beat.js @@ -6,10 +6,8 @@ import expect from 'expect.js'; import moment from 'moment'; -import { - ES_INDEX_NAME, - ES_TYPE_NAME -} from './constants'; + +import { ES_INDEX_NAME, ES_TYPE_NAME } from './constants'; export default function ({ getService }) { const supertest = getService('supertest'); @@ -23,17 +21,19 @@ export default function ({ getService }) { beforeEach(async () => { validEnrollmentToken = chance.word(); + beatId = chance.word(); - const version = chance.integer({ min: 1, max: 10 }) - + '.' - + chance.integer({ min: 1, max: 10 }) - + '.' - + chance.integer({ min: 1, max: 10 }); + const version = + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }); beat = { type: 'filebeat', host_name: 'foo.bar.com', - version + version, }; await es.index({ @@ -44,17 +44,17 @@ export default function ({ getService }) { type: 'enrollment_token', enrollment_token: { token: validEnrollmentToken, - expires_on: moment().add(4, 'hours').toJSON() - } - } + expires_on: moment() + .add(4, 'hours') + .toJSON(), + }, + }, }); }); it('should enroll beat in an unverified state', async () => { await supertest - .post( - `/api/beats/agent/${beatId}` - ) + .post(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') .set('kbn-beats-enrollment-token', validEnrollmentToken) .send(beat) @@ -63,7 +63,7 @@ export default function ({ getService }) { const esResponse = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:${beatId}` + id: `beat:${beatId}`, }); expect(esResponse._source.beat).to.not.have.property('verified_on'); @@ -72,9 +72,7 @@ export default function ({ getService }) { it('should contain an access token in the response', async () => { const { body: apiResponse } = await supertest - .post( - `/api/beats/agent/${beatId}` - ) + .post(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') .set('kbn-beats-enrollment-token', validEnrollmentToken) .send(beat) @@ -85,7 +83,7 @@ export default function ({ getService }) { const esResponse = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:${beatId}` + id: `beat:${beatId}`, }); const accessTokenInEs = esResponse._source.beat.access_token; @@ -96,9 +94,7 @@ export default function ({ getService }) { it('should reject an invalid enrollment token', async () => { const { body: apiResponse } = await supertest - .post( - `/api/beats/agent/${beatId}` - ) + .post(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') .set('kbn-beats-enrollment-token', chance.word()) .send(beat) @@ -108,7 +104,10 @@ export default function ({ getService }) { }); it('should reject an expired enrollment token', async () => { - const expiredEnrollmentToken = chance.word(); + const expiredEnrollmentToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + + 'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1LCJleHAiOjE1MzAzMzAxMzV9.' + + 'Azf4czAwWZEflR7Pf8pi-DUTcve9xyxWyViNYeUSGog'; await es.index({ index: ES_INDEX_NAME, @@ -118,15 +117,15 @@ export default function ({ getService }) { type: 'enrollment_token', enrollment_token: { token: expiredEnrollmentToken, - expires_on: moment().subtract(1, 'minute').toJSON() - } - } + expires_on: moment() + .subtract(1, 'minute') + .toJSON(), + }, + }, }); const { body: apiResponse } = await supertest - .post( - `/api/beats/agent/${beatId}` - ) + .post(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') .set('kbn-beats-enrollment-token', expiredEnrollmentToken) .send(beat) @@ -137,9 +136,7 @@ export default function ({ getService }) { it('should delete the given enrollment token so it may not be reused', async () => { await supertest - .post( - `/api/beats/agent/${beatId}` - ) + .post(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') .set('kbn-beats-enrollment-token', validEnrollmentToken) .send(beat) @@ -149,7 +146,7 @@ export default function ({ getService }) { index: ES_INDEX_NAME, type: ES_TYPE_NAME, id: `enrollment_token:${validEnrollmentToken}`, - ignore: [ 404 ] + ignore: [404], }); expect(esResponse.found).to.be(false); @@ -157,9 +154,7 @@ export default function ({ getService }) { it('should fail if the beat with the same ID is enrolled twice', async () => { await supertest - .post( - `/api/beats/agent/${beatId}` - ) + .post(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') .set('kbn-beats-enrollment-token', validEnrollmentToken) .send(beat) @@ -173,15 +168,15 @@ export default function ({ getService }) { type: 'enrollment_token', enrollment_token: { token: validEnrollmentToken, - expires_on: moment().add(4, 'hours').toJSON() - } - } + expires_on: moment() + .add(4, 'hours') + .toJSON(), + }, + }, }); await supertest - .post( - `/api/beats/agent/${beatId}` - ) + .post(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') .set('kbn-beats-enrollment-token', validEnrollmentToken) .send(beat) diff --git a/x-pack/test/api_integration/apis/beats/update_beat.js b/x-pack/test/api_integration/apis/beats/update_beat.js index 92e5771e0ef4b..fb30970d3cc7f 100644 --- a/x-pack/test/api_integration/apis/beats/update_beat.js +++ b/x-pack/test/api_integration/apis/beats/update_beat.js @@ -5,10 +5,8 @@ */ import expect from 'expect.js'; -import { - ES_INDEX_NAME, - ES_TYPE_NAME -} from './constants'; +import { ES_INDEX_NAME, ES_TYPE_NAME } from './constants'; +import moment from 'moment'; export default function ({ getService }) { const supertest = getService('supertest'); @@ -17,23 +15,44 @@ export default function ({ getService }) { const esArchiver = getService('esArchiver'); describe('update_beat', () => { + let validEnrollmentToken; let beat; const archive = 'beats/list'; beforeEach('load beats archive', () => esArchiver.load(archive)); - beforeEach(() => { - const version = chance.integer({ min: 1, max: 10 }) - + '.' - + chance.integer({ min: 1, max: 10 }) - + '.' - + chance.integer({ min: 1, max: 10 }); + beforeEach(async () => { + validEnrollmentToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + + 'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.' + + 'SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI'; + const version = + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }); beat = { type: `${chance.word()}beat`, host_name: `www.${chance.word()}.net`, version, - ephemeral_id: chance.word() + ephemeral_id: chance.word(), }; + + await es.index({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `enrollment_token:${validEnrollmentToken}`, + body: { + type: 'enrollment_token', + enrollment_token: { + token: validEnrollmentToken, + expires_on: moment() + .add(4, 'hours') + .toJSON(), + }, + }, + }); }); afterEach('unload beats archive', () => esArchiver.unload(archive)); @@ -41,18 +60,16 @@ export default function ({ getService }) { it('should update an existing verified beat', async () => { const beatId = 'foo'; await supertest - .put( - `/api/beats/agent/${beatId}` - ) + .put(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') - .set('kbn-beats-access-token', '93c4a4dd08564c189a7ec4e4f046b975') + .set('kbn-beats-access-token', validEnrollmentToken) .send(beat) .expect(204); const beatInEs = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:${beatId}` + id: `beat:${beatId}`, }); expect(beatInEs._source.beat.id).to.be(beatId); @@ -65,9 +82,7 @@ export default function ({ getService }) { it('should return an error for an invalid access token', async () => { const beatId = 'foo'; const { body } = await supertest - .put( - `/api/beats/agent/${beatId}` - ) + .put(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') .set('kbn-beats-access-token', chance.word()) .send(beat) @@ -78,7 +93,7 @@ export default function ({ getService }) { const beatInEs = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:${beatId}` + id: `beat:${beatId}`, }); expect(beatInEs._source.beat.id).to.be(beatId); @@ -90,12 +105,16 @@ export default function ({ getService }) { it('should return an error for an existing but unverified beat', async () => { const beatId = 'bar'; + const { body } = await supertest - .put( - `/api/beats/agent/${beatId}` - ) + .put(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') - .set('kbn-beats-access-token', '3c4a4dd08564c189a7ec4e4f046b9759') + .set( + 'kbn-beats-access-token', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + + 'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.' + + 'SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI' + ) .send(beat) .expect(400); @@ -104,7 +123,7 @@ export default function ({ getService }) { const beatInEs = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:${beatId}` + id: `beat:${beatId}`, }); expect(beatInEs._source.beat.id).to.be(beatId); @@ -117,11 +136,9 @@ export default function ({ getService }) { it('should return an error for a non-existent beat', async () => { const beatId = chance.word(); const { body } = await supertest - .put( - `/api/beats/agent/${beatId}` - ) + .put(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') - .set('kbn-beats-access-token', chance.word()) + .set('kbn-beats-access-token', validEnrollmentToken) .send(beat) .expect(404); diff --git a/x-pack/test/api_integration/apis/beats/verify_beats.js b/x-pack/test/api_integration/apis/beats/verify_beats.js index 2b085308b43d1..30521d0483c2d 100644 --- a/x-pack/test/api_integration/apis/beats/verify_beats.js +++ b/x-pack/test/api_integration/apis/beats/verify_beats.js @@ -19,15 +19,10 @@ export default function ({ getService }) { it('verify the given beats', async () => { const { body: apiResponse } = await supertest - .post( - '/api/beats/agents/verify' - ) + .post('/api/beats/agents/verify') .set('kbn-xsrf', 'xxx') .send({ - beats: [ - { id: 'bar' }, - { id: 'baz' } - ] + beats: [{ id: 'bar' }, { id: 'baz' }], }) .expect(200); @@ -39,36 +34,26 @@ export default function ({ getService }) { it('should not re-verify already-verified beats', async () => { const { body: apiResponse } = await supertest - .post( - '/api/beats/agents/verify' - ) + .post('/api/beats/agents/verify') .set('kbn-xsrf', 'xxx') .send({ - beats: [ - { id: 'foo' }, - { id: 'bar' } - ] + beats: [{ id: 'foo' }, { id: 'bar' }], }) .expect(200); expect(apiResponse.beats).to.eql([ { id: 'foo', status: 200, result: 'already verified' }, - { id: 'bar', status: 200, result: 'verified' } + { id: 'bar', status: 200, result: 'verified' }, ]); }); it('should return errors for non-existent beats', async () => { const nonExistentBeatId = chance.word(); const { body: apiResponse } = await supertest - .post( - '/api/beats/agents/verify' - ) + .post('/api/beats/agents/verify') .set('kbn-xsrf', 'xxx') .send({ - beats: [ - { id: 'bar' }, - { id: nonExistentBeatId } - ] + beats: [{ id: 'bar' }, { id: nonExistentBeatId }], }) .expect(200); diff --git a/x-pack/test/functional/es_archives/beats/list/data.json.gz b/x-pack/test/functional/es_archives/beats/list/data.json.gz index b33ecf434c104..6af0e1b8aeb47 100644 Binary files a/x-pack/test/functional/es_archives/beats/list/data.json.gz and b/x-pack/test/functional/es_archives/beats/list/data.json.gz differ diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index 803b25f38cd9a..6be4ca3b66fdd 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -122,14 +122,26 @@ version "4.3.10" resolved "https://registry.yarnpkg.com/@types/boom/-/boom-4.3.10.tgz#39dad8c0614c26b91ef016a57d7eee4ffe4f8a25" +"@types/chance@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.0.1.tgz#c10703020369602c40dd9428cc6e1437027116df" + "@types/delay@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/delay/-/delay-2.0.1.tgz#61bcf318a74b61e79d1658fbf054f984c90ef901" +"@types/elasticsearch@^5.0.24": + version "5.0.24" + resolved "https://registry.yarnpkg.com/@types/elasticsearch/-/elasticsearch-5.0.24.tgz#b09082d2ba3d8ae1627ea771bd2fbd2851e4a035" + "@types/events@*": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" +"@types/expect.js@^0.3.29": + version "0.3.29" + resolved "https://registry.yarnpkg.com/@types/expect.js/-/expect.js-0.3.29.tgz#28dd359155b84b8ecb094afc3f4b74c3222dca3b" + "@types/form-data@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.2.1.tgz#ee2b3b8eaa11c0938289953606b745b738c54b1e" @@ -162,6 +174,12 @@ version "10.6.2" resolved "https://registry.yarnpkg.com/@types/joi/-/joi-10.6.2.tgz#0e7d632fe918c337784e87b16c7cc0098876179a" +"@types/jsonwebtoken@^7.2.7": + version "7.2.7" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-7.2.7.tgz#5dd62e0c0a0c6f211c3c1d13d322360894625b47" + dependencies: + "@types/node" "*" + "@types/lodash@^3.10.0": version "3.10.2" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-3.10.2.tgz#c1fbda1562ef5603c8192fe1fe65b017849d5873" @@ -202,6 +220,10 @@ version "0.10.2" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.10.2.tgz#bd1740c4ad51966609b058803ee6874577848b37" +"@types/sinon@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-5.0.1.tgz#a15b36ec42f1f53166617491feabd1734cb03e21" + "@types/url-join@^0.8.2": version "0.8.2" resolved "https://registry.yarnpkg.com/@types/url-join/-/url-join-0.8.2.tgz#1181ecbe1d97b7034e0ea1e35e62e86cc26b422d" @@ -1214,6 +1236,10 @@ buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + buffer-equal@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" @@ -2158,6 +2184,12 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +ecdsa-sig-formatter@1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" + dependencies: + safe-buffer "^5.0.1" + elasticsearch@13.0.1: version "13.0.1" resolved "https://registry.yarnpkg.com/elasticsearch/-/elasticsearch-13.0.1.tgz#fa58204233052c4cd221e8721e48f3906b385b32" @@ -4477,6 +4509,20 @@ jsonpointer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" +jsonwebtoken@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.3.0.tgz#056c90eee9a65ed6e6c72ddb0a1d325109aaf643" + dependencies: + jws "^3.1.5" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -4494,6 +4540,21 @@ just-extend@^1.1.27: version "1.1.27" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.27.tgz#ec6e79410ff914e472652abfa0e603c03d60e905" +jwa@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.10" + safe-buffer "^5.0.1" + +jws@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" + dependencies: + jwa "^1.1.5" + safe-buffer "^5.0.1" + keymirror@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/keymirror/-/keymirror-0.1.1.tgz#918889ea13f8d0a42e7c557250eee713adc95c35" @@ -4745,6 +4806,10 @@ lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -4753,6 +4818,10 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + lodash.isempty@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" @@ -4761,10 +4830,26 @@ lodash.isequal@^4.1.1, lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + lodash.isobject@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + lodash.istypedarray@^3.0.0: version "3.0.6" resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" @@ -4785,6 +4870,10 @@ lodash.mergewith@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + lodash.orderby@4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.orderby/-/lodash.orderby-4.6.0.tgz#e697f04ce5d78522f54d9338b32b81a3393e4eb3" @@ -5192,7 +5281,7 @@ ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" -ms@^2.0.0: +ms@^2.0.0, ms@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" diff --git a/yarn.lock b/yarn.lock index c6eeaf8861466..df64bec876ca3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -295,6 +295,10 @@ version "2.0.1" resolved "https://registry.yarnpkg.com/@types/delay/-/delay-2.0.1.tgz#61bcf318a74b61e79d1658fbf054f984c90ef901" +"@types/elasticsearch@^5.0.24": + version "5.0.24" + resolved "https://registry.yarnpkg.com/@types/elasticsearch/-/elasticsearch-5.0.24.tgz#b09082d2ba3d8ae1627ea771bd2fbd2851e4a035" + "@types/eslint@^4.16.2": version "4.16.2" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-4.16.2.tgz#30f4f026019eb78a6ef12f276b75cd16ea2afb27" @@ -397,6 +401,12 @@ version "1.0.32" resolved "https://registry.yarnpkg.com/@types/json-stable-stringify/-/json-stable-stringify-1.0.32.tgz#121f6917c4389db3923640b2e68de5fa64dda88e" +"@types/jsonwebtoken@^7.2.7": + version "7.2.7" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-7.2.7.tgz#5dd62e0c0a0c6f211c3c1d13d322360894625b47" + dependencies: + "@types/node" "*" + "@types/listr@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@types/listr/-/listr-0.13.0.tgz#6250bc4a04123cafa24fc73d1b880653a6ae6721" @@ -2267,6 +2277,10 @@ buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + buffer-equal@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" @@ -4210,6 +4224,12 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +ecdsa-sig-formatter@1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" + dependencies: + safe-buffer "^5.0.1" + editions@^1.1.1, editions@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b" @@ -7885,6 +7905,20 @@ jsonpointer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" +jsonwebtoken@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.3.0.tgz#056c90eee9a65ed6e6c72ddb0a1d325109aaf643" + dependencies: + jws "^3.1.5" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -7940,6 +7974,21 @@ just-extend@^1.1.27: version "1.1.27" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.27.tgz#ec6e79410ff914e472652abfa0e603c03d60e905" +jwa@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.10" + safe-buffer "^5.0.1" + +jws@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" + dependencies: + jwa "^1.1.5" + safe-buffer "^5.0.1" + karma-chrome-launcher@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.1.1.tgz#216879c68ac04d8d5140e99619ba04b59afd46cf" @@ -8478,6 +8527,10 @@ lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -8486,6 +8539,10 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + lodash.isempty@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" @@ -8494,10 +8551,26 @@ lodash.isequal@^4.0.0, lodash.isequal@^4.1.1, lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + lodash.isobject@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + lodash.istypedarray@^3.0.0: version "3.0.6" resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" @@ -8538,6 +8611,10 @@ lodash.mergewith@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + lodash.orderby@4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.orderby/-/lodash.orderby-4.6.0.tgz#e697f04ce5d78522f54d9338b32b81a3393e4eb3" @@ -9114,7 +9191,7 @@ ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" -ms@^2.0.0: +ms@^2.0.0, ms@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"