Skip to content

Commit

Permalink
[Security Solution][Artifacts] Refactor endpoint Artifact manifest pr…
Browse files Browse the repository at this point in the history
…ocessing (#95846)

* Remove references to class `ArtifactClient` and replace with EndpointArtifactClientInterface
* refactor artifact client tests to use new class
* Added additional test to Fleet Artifacts create service
* remove SavedObject type wrapper from getArtifact response
  • Loading branch information
paul-tavares authored Apr 1, 2021
1 parent b29ccdc commit 6238ef7
Show file tree
Hide file tree
Showing 10 changed files with 103 additions and 181 deletions.
10 changes: 10 additions & 0 deletions x-pack/plugins/fleet/server/services/artifacts/artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { elasticsearchServiceMock } from 'src/core/server/mocks';

import { ResponseError } from '@elastic/elasticsearch/lib/errors';

import type { ApiResponse } from '@elastic/elasticsearch/lib/Transport';

import { FLEET_SERVER_ARTIFACTS_INDEX } from '../../../common';

import { ArtifactsElasticsearchError } from '../../errors';
Expand Down Expand Up @@ -100,6 +102,14 @@ describe('When using the artifacts services', () => {
});
});

it('should ignore 409 errors from elasticsearch', async () => {
const error = new ResponseError({ statusCode: 409 } as ApiResponse);
// Unclear why `mockRejectedValue()` has the params value type set to `never`
// @ts-expect-error
esClientMock.create.mockRejectedValue(error);
await expect(() => createArtifact(esClientMock, newArtifact)).not.toThrow();
});

it('should throw an ArtifactElasticsearchError if one is encountered', async () => {
setEsClientMethodResponseToError(esClientMock, 'create');
await expect(createArtifact(esClientMock, newArtifact)).rejects.toBeInstanceOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
export const ArtifactConstants = {
GLOBAL_ALLOWLIST_NAME: 'endpoint-exceptionlist',
/**
* Saved objects no longer used for storing artifacts. Value
* Saved objects no longer used for storing artifacts
* @deprecated
*/
SAVED_OBJECT_TYPE: 'endpoint:user-artifact',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ describe('test alerts route', () => {
// and this entire test file refactored to start using fleet's exposed FleetArtifactClient class.
endpointAppContextService!
.getManifestManager()!
.getArtifactsClient().getArtifact = jest.fn().mockResolvedValue(soFindResp);
.getArtifactsClient().getArtifact = jest.fn().mockResolvedValue(soFindResp.attributes);

[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/artifacts/download')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ export function registerDownloadArtifactRoute(
return res.notFound({ body: `No artifact found for ${id}` });
}

const bodyBuffer = Buffer.from(artifact.attributes.body, 'base64');
const bodyBuffer = Buffer.from(artifact.body, 'base64');
cache.set(id, bodyBuffer);
return buildAndValidateResponse(artifact.attributes.identifier, bodyBuffer);
return buildAndValidateResponse(artifact.identifier, bodyBuffer);
}
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,49 @@
* 2.0.
*/

import { savedObjectsClientMock } from 'src/core/server/mocks';
import { ArtifactConstants, getArtifactId } from '../../lib/artifacts';
import { getInternalArtifactMock } from '../../schemas/artifacts/saved_objects.mock';
import { ArtifactClient } from './artifact_client';
import { EndpointArtifactClient } from './artifact_client';
import { createArtifactsClientMock } from '../../../../../fleet/server/mocks';

describe('artifact_client', () => {
describe('ArtifactClient sanity checks', () => {
let fleetArtifactClient: ReturnType<typeof createArtifactsClientMock>;
let artifactClient: EndpointArtifactClient;

beforeEach(() => {
fleetArtifactClient = createArtifactsClientMock();
artifactClient = new EndpointArtifactClient(fleetArtifactClient);
});

test('can create ArtifactClient', () => {
const artifactClient = new ArtifactClient(savedObjectsClientMock.create());
expect(artifactClient).toBeInstanceOf(ArtifactClient);
expect(artifactClient).toBeInstanceOf(EndpointArtifactClient);
});

test('can get artifact', async () => {
const savedObjectsClient = savedObjectsClientMock.create();
const artifactClient = new ArtifactClient(savedObjectsClient);
await artifactClient.getArtifact('abcd');
expect(savedObjectsClient.get).toHaveBeenCalled();
expect(fleetArtifactClient.listArtifacts).toHaveBeenCalled();
});

test('can create artifact', async () => {
const savedObjectsClient = savedObjectsClientMock.create();
const artifactClient = new ArtifactClient(savedObjectsClient);
const artifact = await getInternalArtifactMock('linux', 'v1');
const artifact = await getInternalArtifactMock('linux', 'v1', { compress: true });
await artifactClient.createArtifact(artifact);
expect(savedObjectsClient.create).toHaveBeenCalledWith(
ArtifactConstants.SAVED_OBJECT_TYPE,
{
...artifact,
created: expect.any(Number),
},
{ id: getArtifactId(artifact) }
);
expect(fleetArtifactClient.createArtifact).toHaveBeenCalledWith({
identifier: artifact.identifier,
type: 'exceptionlist',
content:
'{"entries":[{"type":"simple","entries":[{"entries":[{"field":"some.nested.field","operator":"included","type":"exact_cased","value":"some value"}],' +
'"field":"some.parentField","type":"nested"},{"field":"some.not.nested.field","operator":"included","type":"exact_cased","value":"some value"}]},' +
'{"type":"simple","entries":[{"field":"some.other.not.nested.field","operator":"included","type":"exact_cased","value":"some other value"}]}]}',
});
});

test('can delete artifact', async () => {
const savedObjectsClient = savedObjectsClientMock.create();
const artifactClient = new ArtifactClient(savedObjectsClient);
await artifactClient.deleteArtifact('abcd');
expect(savedObjectsClient.delete).toHaveBeenCalledWith(
ArtifactConstants.SAVED_OBJECT_TYPE,
'abcd'
);
await artifactClient.deleteArtifact('endpoint-trustlist-linux-v1-sha26hash');
expect(fleetArtifactClient.listArtifacts).toHaveBeenCalledWith({
kuery: `decoded_sha256: "sha26hash" AND identifier: "endpoint-trustlist-linux-v1"`,
perPage: 1,
});
expect(fleetArtifactClient.deleteArtifact).toHaveBeenCalledWith('123');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,23 @@
* 2.0.
*/

/* eslint-disable max-classes-per-file */

import { inflate as _inflate } from 'zlib';
import { promisify } from 'util';
import { SavedObject, SavedObjectsClientContract } from 'src/core/server';
import { ArtifactConstants, getArtifactId } from '../../lib/artifacts';
import {
InternalArtifactCompleteSchema,
InternalArtifactCreateSchema,
} from '../../schemas/artifacts';
import { InternalArtifactCompleteSchema } from '../../schemas/artifacts';
import { Artifact, ArtifactsClientInterface } from '../../../../../fleet/server';

const inflateAsync = promisify(_inflate);

export interface EndpointArtifactClientInterface {
getArtifact(id: string): Promise<SavedObject<InternalArtifactCompleteSchema> | undefined>;
getArtifact(id: string): Promise<InternalArtifactCompleteSchema | undefined>;

createArtifact(
artifact: InternalArtifactCompleteSchema
): Promise<SavedObject<InternalArtifactCompleteSchema>>;
createArtifact(artifact: InternalArtifactCompleteSchema): Promise<InternalArtifactCompleteSchema>;

deleteArtifact(id: string): Promise<void>;
}

export class ArtifactClient implements EndpointArtifactClientInterface {
private savedObjectsClient: SavedObjectsClientContract;

constructor(savedObjectsClient: SavedObjectsClientContract) {
this.savedObjectsClient = savedObjectsClient;
}

public async getArtifact(id: string): Promise<SavedObject<InternalArtifactCompleteSchema>> {
return this.savedObjectsClient.get<InternalArtifactCompleteSchema>(
ArtifactConstants.SAVED_OBJECT_TYPE,
id
);
}

public async createArtifact(
artifact: InternalArtifactCompleteSchema
): Promise<SavedObject<InternalArtifactCompleteSchema>> {
return this.savedObjectsClient.create<InternalArtifactCreateSchema>(
ArtifactConstants.SAVED_OBJECT_TYPE,
{
...artifact,
created: Date.now(),
},
{ id: getArtifactId(artifact) }
);
}

public async deleteArtifact(id: string) {
await this.savedObjectsClient.delete(ArtifactConstants.SAVED_OBJECT_TYPE, id);
}
}

/**
* Endpoint specific artifact managment client which uses FleetArtifactsClient to persist artifacts
* Endpoint specific artifact management client which uses FleetArtifactsClient to persist artifacts
* to the Fleet artifacts index (then used by Fleet Server)
*/
export class EndpointArtifactClient implements EndpointArtifactClientInterface {
Expand Down Expand Up @@ -91,15 +50,12 @@ export class EndpointArtifactClient implements EndpointArtifactClientInterface {
return;
}

// FIXME:PT change method signature so that it returns back only the `InternalArtifactCompleteSchema`
return ({
attributes: artifacts.items[0],
} as unknown) as SavedObject<InternalArtifactCompleteSchema>;
return artifacts.items[0];
}

async createArtifact(
artifact: InternalArtifactCompleteSchema
): Promise<SavedObject<InternalArtifactCompleteSchema>> {
): Promise<InternalArtifactCompleteSchema> {
// FIXME:PT refactor to make this more efficient by passing through the uncompressed artifact content
// Artifact `.body` is compressed/encoded. We need it decoded and as a string
const artifactContent = await inflateAsync(Buffer.from(artifact.body, 'base64'));
Expand All @@ -110,15 +66,13 @@ export class EndpointArtifactClient implements EndpointArtifactClientInterface {
type: this.parseArtifactId(artifact.identifier).type,
});

return ({
attributes: createdArtifact,
} as unknown) as SavedObject<InternalArtifactCompleteSchema>;
return createdArtifact;
}

async deleteArtifact(id: string) {
// Ignoring the `id` not being in the type until we can refactor the types in endpoint.
// @ts-ignore
const artifactId = (await this.getArtifact(id)).attributes?.id;
const artifactId = (await this.getArtifact(id))?.id!;
return this.fleetArtifacts.deleteArtifact(artifactId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ import {
getMockArtifactsWithDiff,
getEmptyMockArtifacts,
} from '../../../lib/artifacts/mocks';
import { ArtifactClient } from '../artifact_client';
import { getManifestClientMock } from '../mocks';
import { createEndpointArtifactClientMock, getManifestClientMock } from '../mocks';
import { ManifestManager, ManifestManagerContext } from './manifest_manager';

export const createExceptionListResponse = (data: ExceptionListItemSchema[], total?: number) => ({
Expand Down Expand Up @@ -84,7 +83,7 @@ export const buildManifestManagerContextMock = (

return {
...fullOpts,
artifactClient: new ArtifactClient(fullOpts.savedObjectsClient),
artifactClient: createEndpointArtifactClientMock(),
logger: loggingSystemMock.create().get() as jest.Mocked<Logger>,
};
};
Expand Down
Loading

0 comments on commit 6238ef7

Please sign in to comment.