Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Security Solution][Endpoint] User Manifest Cleanup + Artifact Compression #70759

Merged
merged 53 commits into from
Jul 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
133ea27
Stateless exception list translation with improved runtime checks
madirey Jul 2, 2020
9e43633
use flatMap and reduce to simplify logic
madirey Jul 3, 2020
e9f74b4
Update to new manifest format
madirey Jul 4, 2020
2a266b3
Merge branch 'master' of github.com:elastic/kibana into user-allowlis…
madirey Jul 4, 2020
ceca5fa
Fix test fixture SO data type
madirey Jul 4, 2020
49d22d2
Fix another test fixture data type
madirey Jul 4, 2020
9428ca5
Fix sha256 reference in artifact_client
madirey Jul 4, 2020
c376638
Refactor to remove usages of 'then' and tidy up a bit
madirey Jul 5, 2020
617d5f1
sync master
madirey Jul 5, 2020
13d9ec8
Zlib compression
Jul 6, 2020
4718110
prefer byteLength to length
Jul 6, 2020
feb1202
Make ingestManager optional for security-solution startup
madirey Jul 6, 2020
4ce792b
Merge branch 'user-allowlist-artifacts-pt3' of github.com:madirey/kib…
madirey Jul 6, 2020
aecf718
Fix download functionality
madirey Jul 7, 2020
56886ad
Use eql for deep equality check
madirey Jul 7, 2020
03e75c9
Fix base64 download bug
madirey Jul 7, 2020
8620510
Add test for artifact download
madirey Jul 7, 2020
a8216e1
Merge branch 'master' of github.com:elastic/kibana into fix-download
madirey Jul 7, 2020
1456697
Add more tests to ensure cached versions of artifacts are correct
madirey Jul 7, 2020
eae1b89
Convert to new format
madirey Jul 7, 2020
ec60546
Deflate
Jul 7, 2020
a32f960
Merge branch 'user-allowlist-artifacts-pt3' of github.com:madirey/kib…
Jul 7, 2020
339c887
missed some refs
madirey Jul 7, 2020
61b2ca8
Merge branch 'master' of github.com:elastic/kibana into user-allowlis…
Jul 7, 2020
aca7b8c
partial fix to wrapper format
madirey Jul 7, 2020
04d5701
update fixtures and integration test
madirey Jul 7, 2020
ed87ad2
Fixing unit tests
Jul 7, 2020
f2c5ba1
Merge branch 'fix-download' of github.com:madirey/kibana into fix-dow…
Jul 7, 2020
70ce85a
Merge branch 'master' of github.com:elastic/kibana into fix-download
madirey Jul 7, 2020
9d331d2
Merge branch 'master' of github.com:elastic/kibana into user-allowlis…
madirey Jul 8, 2020
ead9ae8
sync with fix-download branch
madirey Jul 8, 2020
5095d85
merge master, fix tests
madirey Jul 8, 2020
094c358
small bug fixes
madirey Jul 8, 2020
d6aefee
artifact and manifest versioning changes
madirey Jul 8, 2020
be42e8d
Merge branch 'master' of github.com:elastic/kibana into user-allowlis…
madirey Jul 8, 2020
7e48f52
Merge branch 'user-allowlist-artifacts-pt3' of github.com:madirey/kib…
Jul 8, 2020
0bd3530
Merge branch 'user-allowlist-artifacts-pt3' of github.com:madirey/kib…
Jul 8, 2020
206f222
Merge branch 'master' of github.com:elastic/kibana into user-allowlis…
madirey Jul 8, 2020
ae856b5
Remove access tag from download endpoint
madirey Jul 8, 2020
ced63a9
Merge branch 'user-allowlist-artifacts-pt3' of github.com:madirey/kib…
Jul 8, 2020
69f59fd
Adding decompression to integration test
Jul 8, 2020
4795132
Removing tag from route
Jul 8, 2020
c18948b
add try/catch in ingest callback handler
madirey Jul 8, 2020
8bef643
Fixing
Jul 8, 2020
a2b3a1b
Merging
Jul 8, 2020
a24acc8
Removing last expect from unit test for tag
Jul 8, 2020
04dbbcf
type fixes
madirey Jul 8, 2020
0809b77
Merge branch 'user-allowlist-artifacts-pt3' of github.com:madirey/kib…
madirey Jul 8, 2020
2bf4acb
Add compression type to manifest
madirey Jul 8, 2020
50b49a5
Merge branch 'master' into user-allowlist-artifacts-pt3
elasticmachine Jul 9, 2020
046f271
Merge branch 'master' into user-allowlist-artifacts-pt3
elasticmachine Jul 9, 2020
a4414f9
Merge branch 'master' into user-allowlist-artifacts-pt3
elasticmachine Jul 9, 2020
899a673
Reverting ingestManager back to being required for now
Jul 9, 2020
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
Original file line number Diff line number Diff line change
Expand Up @@ -1036,8 +1036,8 @@ export class EndpointDocGenerator {
config: {
artifact_manifest: {
value: {
manifest_version: 'v0',
schema_version: '1.0.0',
manifest_version: 'WzAsMF0=',
schema_version: 'v1',
artifacts: {},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const compressionAlgorithm = t.keyof({
none: null,
zlib: null,
});
export type CompressionAlgorithm = t.TypeOf<typeof compressionAlgorithm>;

export const encryptionAlgorithm = t.keyof({
none: null,
Expand All @@ -20,7 +21,7 @@ export const identifier = t.string;
export const manifestVersion = t.string;

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ describe('policy details: ', () => {
config: {
artifact_manifest: {
value: {
manifest_version: 'v0',
schema_version: '1.0.0',
manifest_version: 'WzAsMF0=',
schema_version: 'v1',
artifacts: {},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import { httpServerMock } from '../../../../../src/core/server/mocks';
import { EndpointAppContextService } from './endpoint_app_context_services';

describe('test endpoint app context services', () => {
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 return undefined on getAgentService if dependencies are not enabled', async () => {
// const endpointAppContextService = new EndpointAppContextService();
// expect(endpointAppContextService.getAgentService()).toEqual(undefined);
// });
// it('should return undefined on getManifestManager if dependencies are not enabled', 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(() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
SavedObjectsServiceStart,
KibanaRequest,
Logger,
SavedObjectsServiceStart,
SavedObjectsClientContract,
} from 'src/core/server';
import { AgentService, IngestManagerStartContract } from '../../../ingest_manager/server';
import { getPackageConfigCreateCallback } from './ingest_integration';
import { ManifestManager } from './services/artifacts';

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

Expand All @@ -35,20 +36,17 @@ export class EndpointAppContextService {
this.manifestManager = dependencies.manifestManager;
this.savedObjectsStart = dependencies.savedObjectsStart;

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

public stop() {}

public getAgentService(): AgentService {
if (!this.agentService) {
throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`);
}
public getAgentService(): AgentService | undefined {
return this.agentService;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { Logger } from '../../../../../src/core/server';
import { NewPackageConfig } from '../../../ingest_manager/common/types/models';
import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config';
import { NewPolicyData } from '../../common/endpoint/types';
Expand All @@ -13,6 +14,7 @@ import { ManifestManager } from './services/artifacts';
* Callback to handle creation of PackageConfigs in Ingest Manager
*/
export const getPackageConfigCreateCallback = (
logger: Logger,
manifestManager: ManifestManager
): ((newPackageConfig: NewPackageConfig) => Promise<NewPackageConfig>) => {
const handlePackageConfigCreate = async (
Expand All @@ -27,8 +29,19 @@ export const getPackageConfigCreateCallback = (
// follow the types/schema expected
let updatedPackageConfig = newPackageConfig as NewPolicyData;

const wrappedManifest = await manifestManager.refresh({ initialize: true });
if (wrappedManifest !== null) {
// get snapshot based on exception-list-agnostic SOs
// with diffs from last dispatched manifest, if it exists
const snapshot = await manifestManager.getSnapshot({ initialize: true });

if (snapshot === null) {
logger.warn('No manifest snapshot available.');
return updatedPackageConfig;
}

if (snapshot.diffs.length > 0) {
// create new artifacts
await manifestManager.syncArtifacts(snapshot, 'add');

// Until we get the Default Policy Configuration in the Endpoint package,
// we will add it here manually at creation time.
// @ts-ignore
Expand All @@ -42,7 +55,7 @@ export const getPackageConfigCreateCallback = (
streams: [],
config: {
artifact_manifest: {
value: wrappedManifest.manifest.toEndpointFormat(),
value: snapshot.manifest.toEndpointFormat(),
},
policy: {
value: policyConfigFactory(),
Expand All @@ -57,9 +70,18 @@ export const getPackageConfigCreateCallback = (
try {
return updatedPackageConfig;
} finally {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit -> We might think about sending a promise instead of counting on finally. I think that will be more readable too

// TODO: confirm creation of package config
// then commit.
await manifestManager.commit(wrappedManifest);
if (snapshot.diffs.length > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit -> I think it will be nice to have try/catch here to avoid bubble up error and have a better way to handle it

// TODO: let's revisit the way this callback happens... use promises?
// only commit when we know the package config was created
try {
await manifestManager.commit(snapshot.manifest);

// clean up old artifacts
await manifestManager.syncArtifacts(snapshot, 'delete');
} catch (err) {
logger.error(err);
}
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,41 @@ import { ExceptionsCache } from './cache';

describe('ExceptionsCache tests', () => {
let cache: ExceptionsCache;
const body = Buffer.from('body');

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

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

test('it should handle cache miss', async () => {
cache.set('test', 'body');
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 a = Buffer.from('a');
const b = Buffer.from('b');
const c = Buffer.from('c');
const d = Buffer.from('d');
cache.set('1', a);
cache.set('2', b);
cache.set('3', c);
const cacheResp = cache.get('1');
expect(cacheResp).toEqual('a');
expect(cacheResp).toEqual(a);

cache.set('4', 'd');
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');
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
Expand Up @@ -10,7 +10,7 @@ const DEFAULT_MAX_SIZE = 10;
* FIFO cache implementation for artifact downloads.
*/
export class ExceptionsCache {
private cache: Map<string, string>;
private cache: Map<string, Buffer>;
private queue: string[];
private maxSize: number;

Expand All @@ -20,7 +20,7 @@ export class ExceptionsCache {
this.maxSize = maxSize || DEFAULT_MAX_SIZE;
}

set(id: string, body: string) {
set(id: string, body: Buffer) {
if (this.queue.length + 1 > this.maxSize) {
const entry = this.queue.shift();
if (entry !== undefined) {
Expand All @@ -31,7 +31,7 @@ export class ExceptionsCache {
this.cache.set(id, body);
}

get(id: string): string | undefined {
get(id: string): Buffer | undefined {
return this.cache.get(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ export const ArtifactConstants = {
GLOBAL_ALLOWLIST_NAME: 'endpoint-exceptionlist',
SAVED_OBJECT_TYPE: 'endpoint:user-artifact:v2',
SUPPORTED_OPERATING_SYSTEMS: ['linux', 'macos', 'windows'],
SCHEMA_VERSION: '1.0.0',
SCHEMA_VERSION: 'v1',
};

export const ManifestConstants = {
SAVED_OBJECT_TYPE: 'endpoint:user-artifact-manifest:v2',
SCHEMA_VERSION: '1.0.0',
SCHEMA_VERSION: 'v1',
INITIAL_VERSION: 'WzAsMF0=',
};
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('buildEventTypeSignal', () => {

const first = getFoundExceptionListItemSchemaMock();
mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0');
const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1');
expect(resp).toEqual({
entries: [expectedEndpointExceptions],
});
Expand Down Expand Up @@ -87,7 +87,7 @@ describe('buildEventTypeSignal', () => {
first.data[0].entries = testEntries;
mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);

const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0');
const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1');
expect(resp).toEqual({
entries: [expectedEndpointExceptions],
});
Expand Down Expand Up @@ -133,7 +133,7 @@ describe('buildEventTypeSignal', () => {
first.data[0].entries = testEntries;
mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);

const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0');
const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1');
expect(resp).toEqual({
entries: [expectedEndpointExceptions],
});
Expand Down Expand Up @@ -171,7 +171,7 @@ describe('buildEventTypeSignal', () => {
first.data[0].entries = testEntries;
mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);

const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0');
const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1');
expect(resp).toEqual({
entries: [expectedEndpointExceptions],
});
Expand All @@ -193,7 +193,7 @@ describe('buildEventTypeSignal', () => {
.mockReturnValueOnce(first)
.mockReturnValueOnce(second)
.mockReturnValueOnce(third);
const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0');
const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1');
expect(resp.entries.length).toEqual(3);
});

Expand All @@ -202,7 +202,7 @@ describe('buildEventTypeSignal', () => {
exceptionsResponse.data = [];
exceptionsResponse.total = 0;
mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(exceptionsResponse);
const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0');
const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', 'v1');
expect(resp.entries.length).toEqual(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import { createHash } from 'crypto';
import { deflate } from 'zlib';
import { ExceptionListItemSchema } from '../../../../../lists/common/schemas';
import { validate } from '../../../../common/validate';

Expand Down Expand Up @@ -34,6 +35,7 @@ export async function buildArtifact(
const exceptionsBuffer = Buffer.from(JSON.stringify(exceptions));
const sha256 = createHash('sha256').update(exceptionsBuffer.toString()).digest('hex');

// Keep compression info empty in case its a duplicate. Lazily compress before committing if needed.
return {
identifier: `${ArtifactConstants.GLOBAL_ALLOWLIST_NAME}-${os}-${schemaVersion}`,
compressionAlgorithm: 'none',
Expand Down Expand Up @@ -95,7 +97,7 @@ export function translateToEndpointExceptions(
exc: FoundExceptionListItemSchema,
schemaVersion: string
): TranslatedExceptionListItem[] {
if (schemaVersion === '1.0.0') {
if (schemaVersion === 'v1') {
return exc.data.map((item) => {
return translateItem(schemaVersion, item);
});
Expand Down Expand Up @@ -180,3 +182,15 @@ function translateEntry(
}
}
}

export async function compressExceptionList(buffer: Buffer): Promise<Buffer> {
return new Promise((resolve, reject) => {
deflate(buffer, function (err, buf) {
if (err) {
reject(err);
} else {
resolve(buf);
}
});
});
}
Loading