Skip to content

Commit

Permalink
[Security solution] [Endpoint] Fix bad artifact migration (#111294) (#…
Browse files Browse the repository at this point in the history
…111432)

* Working test that validate migrated artifact has same properties as SO artifact
* Checks if artifact is compressed and uncompress it if necessary before creating the new one from fleet

Co-authored-by: Paul Tavares <[email protected]>
Co-authored-by: Kibana Machine <[email protected]>

Co-authored-by: David Sánchez <[email protected]>
Co-authored-by: Paul Tavares <[email protected]>
  • Loading branch information
3 people authored Sep 7, 2021
1 parent 1eab035 commit e32153d
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 11 deletions.
13 changes: 10 additions & 3 deletions x-pack/plugins/fleet/server/services/artifacts/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import { ResponseError } from '@elastic/elasticsearch/lib/errors';
import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks';
import type { SearchHit, ESSearchResponse } from '../../../../../../src/core/types/elasticsearch';

import type { Artifact, ArtifactElasticsearchProperties, ArtifactsClientInterface } from './types';
import type {
Artifact,
ArtifactElasticsearchProperties,
ArtifactsClientInterface,
NewArtifact,
} from './types';
import { newArtifactToElasticsearchProperties } from './mappings';

export const createArtifactsClientMock = (): jest.Mocked<ArtifactsClientInterface> => {
Expand Down Expand Up @@ -77,10 +82,12 @@ export const generateEsRequestErrorApiResponseMock = (
);
};

export const generateArtifactEsGetSingleHitMock = (): SearchHit<ArtifactElasticsearchProperties> => {
export const generateArtifactEsGetSingleHitMock = (
artifact?: NewArtifact
): SearchHit<ArtifactElasticsearchProperties> => {
const { id, created, ...newArtifact } = generateArtifactMock();
const _source = {
...newArtifactToElasticsearchProperties(newArtifact),
...newArtifactToElasticsearchProperties(artifact ?? newArtifact),
created,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,20 @@ import {
SavedObjectsFindResponse,
SavedObjectsFindResult,
} from 'kibana/server';
import { elasticsearchServiceMock } from 'src/core/server/mocks';
import { migrateArtifactsToFleet } from './migrate_artifacts_to_fleet';
import { createEndpointArtifactClientMock } from '../../services/artifacts/mocks';
import { getInternalArtifactMock } from '../../schemas/artifacts/saved_objects.mock';
import { InternalArtifactCompleteSchema } from '../../schemas';
import { generateArtifactEsGetSingleHitMock } from '../../../../../fleet/server/services/artifacts/mocks';
import { NewArtifact } from '../../../../../fleet/server/services';
import { CreateRequest } from '@elastic/elasticsearch/api/types';

describe('When migrating artifacts to fleet', () => {
let soClient: jest.Mocked<SavedObjectsClient>;
let logger: jest.Mocked<Logger>;
let artifactClient: ReturnType<typeof createEndpointArtifactClientMock>;
/** An artifact that was created prior to 7.14 */
let soArtifactEntry: InternalArtifactCompleteSchema;

const createSoFindResult = (
soHits: SavedObjectsFindResult[] = [],
Expand All @@ -41,6 +47,41 @@ describe('When migrating artifacts to fleet', () => {
soClient = savedObjectsClientMock.create() as jest.Mocked<SavedObjectsClient>;
logger = loggingSystemMock.create().get() as jest.Mocked<Logger>;
artifactClient = createEndpointArtifactClientMock();
// pre-v7.14 artifact, which is compressed
soArtifactEntry = {
identifier: 'endpoint-exceptionlist-macos-v1',
compressionAlgorithm: 'zlib',
encryptionAlgorithm: 'none',
decodedSha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658',
encodedSha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda',
decodedSize: 14,
encodedSize: 22,
body: 'eJyrVkrNKynKTC1WsoqOrQUAJxkFKQ==',
};

// Mock the esClient create response to include the artifact properties that were provide
// to it by fleet artifact client
artifactClient._esClient.create.mockImplementation(<T>(props: CreateRequest<T>) => {
return elasticsearchServiceMock.createSuccessTransportRequestPromise({
...generateArtifactEsGetSingleHitMock({
...((props?.body ?? {}) as NewArtifact),
}),
_index: '.fleet-artifacts-7',
_id: `endpoint:endpoint-exceptionlist-macos-v1-${
// @ts-ignore
props?.body?.decodedSha256 ?? 'UNKNOWN?'
}`,
_version: 1,
result: 'created',
_shards: {
total: 1,
successful: 1,
failed: 0,
},
_seq_no: 0,
_primary_term: 1,
});
});

soClient.find.mockResolvedValue(createSoFindResult([], 0)).mockResolvedValueOnce(
createSoFindResult([
Expand All @@ -49,7 +90,7 @@ describe('When migrating artifacts to fleet', () => {
type: '',
id: 'abc123',
references: [],
attributes: await getInternalArtifactMock('windows', 'v1'),
attributes: soArtifactEntry,
},
])
);
Expand All @@ -70,6 +111,17 @@ describe('When migrating artifacts to fleet', () => {
expect(soClient.delete).toHaveBeenCalled();
});

it('should create artifact in fleet with attributes that match the SO version', async () => {
await migrateArtifactsToFleet(soClient, artifactClient, logger);

await expect(artifactClient.createArtifact.mock.results[0].value).resolves.toEqual(
expect.objectContaining({
...soArtifactEntry,
compressionAlgorithm: 'zlib',
})
);
});

it('should ignore 404 responses for SO delete (multi-node kibana setup)', async () => {
const notFoundError: Error & { output?: { statusCode: number } } = new Error('not found');
notFoundError.output = { statusCode: 404 };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
* 2.0.
*/

import { inflate as _inflate } from 'zlib';
import { promisify } from 'util';
import { SavedObjectsClient, Logger } from 'kibana/server';
import { EndpointArtifactClientInterface } from '../../services';
import { InternalArtifactCompleteSchema } from '../../schemas';
import { InternalArtifactCompleteSchema, InternalArtifactSchema } from '../../schemas';
import { ArtifactConstants } from './common';

class ArtifactMigrationError extends Error {
Expand All @@ -16,6 +18,12 @@ class ArtifactMigrationError extends Error {
}
}

const inflateAsync = promisify(_inflate);

function isCompressed(artifact: InternalArtifactSchema) {
return artifact.compressionAlgorithm === 'zlib';
}

/**
* With v7.13, artifact storage was moved from a security_solution saved object to a fleet index
* in order to support Fleet Server.
Expand Down Expand Up @@ -57,6 +65,15 @@ export const migrateArtifactsToFleet = async (
}

for (const artifact of artifactList) {
if (isCompressed(artifact.attributes)) {
artifact.attributes = {
...artifact.attributes,
body: (await inflateAsync(Buffer.from(artifact.attributes.body, 'base64'))).toString(
'base64'
),
};
}

// Create new artifact in fleet index
await endpointArtifactClient.createArtifact(artifact.attributes);
// Delete old artifact from SO and if there are errors here, then ignore 404's
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
import { SavedObjectsClientContract } from 'src/core/server';
import { savedObjectsClientMock } from 'src/core/server/mocks';
import { ManifestClient } from './manifest_client';
import { EndpointArtifactClientInterface } from './artifact_client';
import { EndpointArtifactClient, EndpointArtifactClientInterface } from './artifact_client';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ElasticsearchClientMock } from '../../../../../../../src/core/server/elasticsearch/client/mocks';
import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks';
// Because mocks are for testing only, should be ok to import the FleetArtifactsClient directly
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { FleetArtifactsClient } from '../../../../../fleet/server/services';
import { createArtifactsClientMock } from '../../../../../fleet/server/mocks';

export const getManifestClientMock = (
savedObjectsClient?: SavedObjectsClientContract
Expand All @@ -19,10 +26,29 @@ export const getManifestClientMock = (
return new ManifestClient(savedObjectsClientMock.create(), 'v1');
};

export const createEndpointArtifactClientMock = (): jest.Mocked<EndpointArtifactClientInterface> => {
/**
* Returns back a mocked EndpointArtifactClient along with the internal FleetArtifactsClient and the Es Clients that are being used
* @param esClient
*/
export const createEndpointArtifactClientMock = (
esClient: ElasticsearchClientMock = elasticsearchServiceMock.createScopedClusterClient()
.asInternalUser
): jest.Mocked<EndpointArtifactClientInterface> & {
_esClient: ElasticsearchClientMock;
} => {
const fleetArtifactClientMocked = createArtifactsClientMock();
const endpointArtifactClientMocked = new EndpointArtifactClient(fleetArtifactClientMocked);

// Return the interface mocked with jest.fn() that fowards calls to the real instance
return {
createArtifact: jest.fn(),
getArtifact: jest.fn(),
deleteArtifact: jest.fn(),
createArtifact: jest.fn(async (...args) => {
const fleetArtifactClient = new FleetArtifactsClient(esClient, 'endpoint');
const endpointArtifactClient = new EndpointArtifactClient(fleetArtifactClient);
const response = await endpointArtifactClient.createArtifact(...args);
return response;
}),
getArtifact: jest.fn((...args) => endpointArtifactClientMocked.getArtifact(...args)),
deleteArtifact: jest.fn((...args) => endpointArtifactClientMocked.deleteArtifact(...args)),
_esClient: esClient,
};
};

0 comments on commit e32153d

Please sign in to comment.