Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[7.x] [SIEM][Security Solution][Endpoint] Endpoint Artifact Manifest Management + Artifact Download and Distribution (#67707) #70758

Merged
merged 4 commits into from
Jul 6, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion x-pack/plugins/lists/server/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ const createSetupMock = (): jest.Mocked<ListPluginSetup> => {

export const listMock = {
createSetup: createSetupMock,
getExceptionList: getExceptionListClientMock,
getExceptionListClient: getExceptionListClientMock,
getListClient: getListClientMock,
};
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,13 @@ export class EndpointDocGenerator {
enabled: true,
streams: [],
config: {
artifact_manifest: {
value: {
manifest_version: 'v0',
schema_version: '1.0.0',
artifacts: {},
},
},
policy: {
value: policyFactory(),
},
Expand Down
22 changes: 22 additions & 0 deletions x-pack/plugins/security_solution/common/endpoint/schema/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import * as t from 'io-ts';

export const identifier = t.string;

export const manifestVersion = t.string;

export const manifestSchemaVersion = t.keyof({
'1.0.0': null,
});
export type ManifestSchemaVersion = t.TypeOf<typeof manifestSchemaVersion>;

export const sha256 = t.string;

export const size = t.number;

export const url = t.string;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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 * as t from 'io-ts';
import { identifier, manifestSchemaVersion, manifestVersion, sha256, size, url } from './common';

export const manifestEntrySchema = t.exact(
t.type({
url,
sha256,
size,
})
);

export const manifestSchema = t.exact(
t.type({
manifest_version: manifestVersion,
schema_version: manifestSchemaVersion,
artifacts: t.record(identifier, manifestEntrySchema),
})
);

export type ManifestEntrySchema = t.TypeOf<typeof manifestEntrySchema>;
export type ManifestSchema = t.TypeOf<typeof manifestSchema>;
4 changes: 4 additions & 0 deletions x-pack/plugins/security_solution/common/endpoint/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import { PackageConfig, NewPackageConfig } from '../../../ingest_manager/common';
import { ManifestSchema } from './schema/manifest';

/**
* Object that allows you to maintain stateful information in the location object across navigation events
Expand Down Expand Up @@ -683,6 +684,9 @@ export type NewPolicyData = NewPackageConfig & {
enabled: boolean;
streams: [];
config: {
artifact_manifest: {
value: ManifestSchema;
};
policy: {
value: PolicyConfig;
};
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/security_solution/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"features",
"home",
"ingestManager",
"taskManager",
"inspector",
"licensing",
"maps",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ describe('policy details: ', () => {
enabled: true,
streams: [],
config: {
artifact_manifest: {
value: {
manifest_version: 'v0',
schema_version: '1.0.0',
artifacts: {},
},
},
policy: {
value: policyConfigFactory(),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,23 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { httpServerMock } from '../../../../../src/core/server/mocks';
import { EndpointAppContextService } from './endpoint_app_context_services';

describe('test endpoint app context services', () => {
it('should throw error if start is not called', async () => {
it('should throw error on getAgentService if start is not called', async () => {
const endpointAppContextService = new EndpointAppContextService();
expect(() => endpointAppContextService.getAgentService()).toThrow(Error);
});
it('should return undefined on getManifestManager if start is not called', async () => {
const endpointAppContextService = new EndpointAppContextService();
expect(endpointAppContextService.getManifestManager()).toEqual(undefined);
});
it('should throw error on getScopedSavedObjectsClient if start is not called', async () => {
const endpointAppContextService = new EndpointAppContextService();
expect(() =>
endpointAppContextService.getScopedSavedObjectsClient(httpServerMock.createKibanaRequest())
).toThrow(Error);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
SavedObjectsServiceStart,
KibanaRequest,
SavedObjectsClientContract,
} from 'src/core/server';
import { AgentService, IngestManagerStartContract } from '../../../ingest_manager/server';
import { handlePackageConfigCreate } from './ingest_integration';
import { getPackageConfigCreateCallback } from './ingest_integration';
import { ManifestManager } from './services/artifacts';

export type EndpointAppContextServiceStartContract = Pick<
IngestManagerStartContract,
'agentService'
> & {
manifestManager?: ManifestManager | undefined;
registerIngestCallback: IngestManagerStartContract['registerExternalCallback'];
savedObjectsStart: SavedObjectsServiceStart;
};

/**
Expand All @@ -19,10 +27,20 @@ export type EndpointAppContextServiceStartContract = Pick<
*/
export class EndpointAppContextService {
private agentService: AgentService | undefined;
private manifestManager: ManifestManager | undefined;
private savedObjectsStart: SavedObjectsServiceStart | undefined;

public start(dependencies: EndpointAppContextServiceStartContract) {
this.agentService = dependencies.agentService;
dependencies.registerIngestCallback('packageConfigCreate', handlePackageConfigCreate);
this.manifestManager = dependencies.manifestManager;
this.savedObjectsStart = dependencies.savedObjectsStart;

if (this.manifestManager !== undefined) {
dependencies.registerIngestCallback(
'packageConfigCreate',
getPackageConfigCreateCallback(this.manifestManager)
);
}
}

public stop() {}
Expand All @@ -33,4 +51,15 @@ export class EndpointAppContextService {
}
return this.agentService;
}

public getManifestManager(): ManifestManager | undefined {
return this.manifestManager;
}

public getScopedSavedObjectsClient(req: KibanaRequest): SavedObjectsClientContract {
if (!this.savedObjectsStart) {
throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`);
}
return this.savedObjectsStart.getScopedClient(req, { excludedWrappers: ['security'] });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,62 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { NewPackageConfig } from '../../../ingest_manager/common/types/models';
import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config';
import { NewPolicyData } from '../../common/endpoint/types';
import { NewPackageConfig } from '../../../ingest_manager/common/types/models';
import { ManifestManager } from './services/artifacts';

/**
* Callback to handle creation of package configs in Ingest Manager
* @param newPackageConfig
* Callback to handle creation of PackageConfigs in Ingest Manager
*/
export const handlePackageConfigCreate = async (
newPackageConfig: NewPackageConfig
): Promise<NewPackageConfig> => {
// We only care about Endpoint package configs
if (newPackageConfig.package?.name !== 'endpoint') {
return newPackageConfig;
}
export const getPackageConfigCreateCallback = (
manifestManager: ManifestManager
): ((newPackageConfig: NewPackageConfig) => Promise<NewPackageConfig>) => {
const handlePackageConfigCreate = async (
newPackageConfig: NewPackageConfig
): Promise<NewPackageConfig> => {
// We only care about Endpoint package configs
if (newPackageConfig.package?.name !== 'endpoint') {
return newPackageConfig;
}

// We cast the type here so that any changes to the Endpoint specific data
// follow the types/schema expected
let updatedPackageConfig = newPackageConfig as NewPolicyData;
// We cast the type here so that any changes to the Endpoint specific data
// follow the types/schema expected
let updatedPackageConfig = newPackageConfig as NewPolicyData;

// Until we get the Default Policy Configuration in the Endpoint package,
// we will add it here manually at creation time.
// @ts-ignore
if (newPackageConfig.inputs.length === 0) {
updatedPackageConfig = {
...newPackageConfig,
inputs: [
{
type: 'endpoint',
enabled: true,
streams: [],
config: {
policy: {
value: policyConfigFactory(),
const wrappedManifest = await manifestManager.refresh({ initialize: true });
if (wrappedManifest !== null) {
// Until we get the Default Policy Configuration in the Endpoint package,
// we will add it here manually at creation time.
// @ts-ignore
if (newPackageConfig.inputs.length === 0) {
updatedPackageConfig = {
...newPackageConfig,
inputs: [
{
type: 'endpoint',
enabled: true,
streams: [],
config: {
artifact_manifest: {
value: wrappedManifest.manifest.toEndpointFormat(),
},
policy: {
value: policyConfigFactory(),
},
},
},
},
},
],
};
}
],
};
}
}

try {
return updatedPackageConfig;
} finally {
await manifestManager.commit(wrappedManifest);
}
};

return updatedPackageConfig;
return handlePackageConfigCreate;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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 { ExceptionsCache } from './cache';

describe('ExceptionsCache tests', () => {
let cache: ExceptionsCache;

beforeEach(() => {
jest.clearAllMocks();
cache = new ExceptionsCache(3);
});

test('it should cache', async () => {
cache.set('test', 'body');
const cacheResp = cache.get('test');
expect(cacheResp).toEqual('body');
});

test('it should handle cache miss', async () => {
cache.set('test', 'body');
const cacheResp = cache.get('not test');
expect(cacheResp).toEqual(undefined);
});

test('it should handle cache eviction', async () => {
cache.set('1', 'a');
cache.set('2', 'b');
cache.set('3', 'c');
const cacheResp = cache.get('1');
expect(cacheResp).toEqual('a');

cache.set('4', 'd');
const secondResp = cache.get('1');
expect(secondResp).toEqual(undefined);
expect(cache.get('2')).toEqual('b');
expect(cache.get('3')).toEqual('c');
expect(cache.get('4')).toEqual('d');
});
});
Original file line number Diff line number Diff line change
@@ -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.
*/

const DEFAULT_MAX_SIZE = 10;

/**
* FIFO cache implementation for artifact downloads.
*/
export class ExceptionsCache {
private cache: Map<string, string>;
private queue: string[];
private maxSize: number;

constructor(maxSize: number) {
this.cache = new Map();
this.queue = [];
this.maxSize = maxSize || DEFAULT_MAX_SIZE;
}

set(id: string, body: string) {
if (this.queue.length + 1 > this.maxSize) {
const entry = this.queue.shift();
if (entry !== undefined) {
this.cache.delete(entry);
}
}
this.queue.push(id);
this.cache.set(id, body);
}

get(id: string): string | undefined {
return this.cache.get(id);
}
}
Original file line number Diff line number Diff line change
@@ -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.
*/

export const ArtifactConstants = {
GLOBAL_ALLOWLIST_NAME: 'endpoint-exceptionlist',
SAVED_OBJECT_TYPE: 'endpoint:exceptions-artifact',
SUPPORTED_OPERATING_SYSTEMS: ['linux', 'macos', 'windows'],
SCHEMA_VERSION: '1.0.0',
};

export const ManifestConstants = {
SAVED_OBJECT_TYPE: 'endpoint:exceptions-manifest',
SCHEMA_VERSION: '1.0.0',
};
Loading