From 9247b6a2a54f0fd4e1dd2d31488d6a4af21000c9 Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Wed, 8 Jul 2020 21:27:50 -0600 Subject: [PATCH] WIP: added a few tests --- .../endpoint_metadata_telemetry.test.ts | 231 ++++++++++++++++++ .../telemetry/endpoint_metadata_telemetry.ts | 111 +++++---- .../endpoint_telemetry_saved_objects.ts | 37 +++ .../server/lib/telemetry/index.ts | 14 +- 4 files changed, 329 insertions(+), 64 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/telemetry/endpoint_metadata_telemetry.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/telemetry/endpoint_telemetry_saved_objects.ts diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/endpoint_metadata_telemetry.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/endpoint_metadata_telemetry.test.ts new file mode 100644 index 000000000000000..cf67e2a7efa9201 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/telemetry/endpoint_metadata_telemetry.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 { AgentEventSOAttributes } from './../../../../ingest_manager/common/types/models/agent'; +import { + AGENT_SAVED_OBJECT_TYPE, + AGENT_EVENT_SAVED_OBJECT_TYPE, +} from './../../../../ingest_manager/common/constants/agent'; +import { Agent } from '../../../../ingest_manager/common'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { SavedObjectsClientContract, SavedObjectsFindResponse } from 'src/core/server'; +import * as endpointTelemetry from './endpoint_metadata_telemetry'; +import * as endpointTelemetrySavedObjects from './endpoint_telemetry_saved_objects'; + +const testAgentId = 'testAgentId'; +const testConfigId = 'testConfigId'; +const osPlatform = 'somePlatform'; +const osName = 'somePlatformName'; +const osFullName = 'somePlatformFullName'; +const osVersion = '1'; + +const mockFleetObjectsResponse = ( + lastCheckIn = new Date().toISOString() +): SavedObjectsFindResponse => ({ + page: 1, + per_page: 20, + total: 1, + saved_objects: [ + { + type: AGENT_SAVED_OBJECT_TYPE, + id: testAgentId, + attributes: { + active: true, + id: testAgentId, + config_id: 'randoConfigId', + type: 'PERMANENT', + user_provided_metadata: {}, + enrolled_at: '2020-07-08T20:07:44.083Z', + current_error_events: [], + local_metadata: { + elastic: { + agent: { + id: testAgentId, + }, + }, + host: { + hostname: 'testDesktop', + name: 'testDesktop', + id: 'randoHostId', + }, + os: { + platform: osPlatform, + version: osVersion, + name: osName, + full: osFullName, + }, + }, + packages: [endpointTelemetrySavedObjects.ENDPOINT_PACKAGE_CONSTANT, 'system'], + last_checkin: lastCheckIn, + }, + references: [], + updated_at: '2020-07-08T20:55:09.216Z', + version: 'WzI4MSwxXQ==', + score: 0, + }, + ], +}); + +const mockFleetEventsObjectsResponse = ( + running?: boolean, + updatedDate = new Date().toISOString() +): SavedObjectsFindResponse => { + return { + page: 1, + per_page: 20, + total: 2, + saved_objects: [ + { + type: AGENT_EVENT_SAVED_OBJECT_TYPE, + id: 'id1', + attributes: { + agent_id: testAgentId, + type: running ? 'STATE' : 'ERROR', + timestamp: updatedDate, + subtype: running ? 'RUNNING' : 'FAILED', + message: `Application: endpoint-security--8.0.0[d8f7f6e8-9375-483c-b456-b479f1d7a4f2]: State changed to ${ + running ? 'RUNNING' : 'FAILED' + }: `, + config_id: testConfigId, + }, + references: [], + updated_at: updatedDate, + version: 'WzExOCwxXQ==', + score: 0, + }, + { + type: AGENT_EVENT_SAVED_OBJECT_TYPE, + id: 'id2', + attributes: { + agent_id: testAgentId, + type: 'STATE', + timestamp: updatedDate, + subtype: 'STARTING', + message: + 'Application: endpoint-security--8.0.0[d8f7f6e8-9375-483c-b456-b479f1d7a4f2]: State changed to STARTING: Starting', + config_id: testConfigId, + }, + references: [], + updated_at: updatedDate, + version: 'WzExNywxXQ==', + score: 0, + }, + ], + }; +}; + +describe('test security solution endpoint telemetry', () => { + let mockSavedObjectsClient: jest.Mocked; + let getFleetSavedObjectsMetadataSpy; + let getFleetEventsSavedObjectsSpy; + + beforeAll(() => { + getFleetEventsSavedObjectsSpy = jest.spyOn( + endpointTelemetrySavedObjects, + 'getFleetEventsSavedObjects' + ); + getFleetSavedObjectsMetadataSpy = jest.spyOn( + endpointTelemetrySavedObjects, + 'getFleetSavedObjectsMetadata' + ); + mockSavedObjectsClient = savedObjectsClientMock.create(); + }); + + afterAll(() => { + jest.resetAllMocks(); + }); + + it('should have a default shape', () => { + expect(endpointTelemetry.getDefaultEndpointTelemetry()).toMatchInlineSnapshot(` + Object { + "active_within_last_24_hours": 0, + "os": Object {}, + "total_installed": 0, + } + `); + }); + + describe('when agents have not been installed', () => { + it('should return the default shape if no agents are found', async () => { + getFleetSavedObjectsMetadataSpy.mockImplementation(() => + Promise.resolve({ saved_objects: [], total: 0, per_page: 0, page: 0 }) + ); + + const emptyEndpointTelemetryData = await endpointTelemetry.getEndpointMetadataTelemetryFromFleet( + mockSavedObjectsClient + ); + expect(getFleetSavedObjectsMetadataSpy).toHaveBeenCalled(); + expect(emptyEndpointTelemetryData).toEqual({ + total_installed: 0, + active_within_last_24_hours: 0, + os: {}, + }); + }); + }); + + describe('when agents have been installed', () => { + let withinLastDay; + let olderThanADay; + beforeEach(() => { + const currentTime = new Date(); + + withinLastDay = new Date(); + withinLastDay.setHours(currentTime.getHours() - 10); + withinLastDay = withinLastDay.toISOString(); + + olderThanADay = new Date(); + olderThanADay.setDate(currentTime.getDate() - 2); + olderThanADay = olderThanADay.toISOString(); + }); + + it('should show one installed but no active endpoint', async () => { + getFleetSavedObjectsMetadataSpy.mockImplementation(() => + Promise.resolve(mockFleetObjectsResponse()) + ); + getFleetEventsSavedObjectsSpy.mockImplementation(() => + Promise.resolve(mockFleetEventsObjectsResponse()) + ); + + const emptyEndpointTelemetryData = await endpointTelemetry.getEndpointMetadataTelemetryFromFleet( + mockSavedObjectsClient + ); + expect(emptyEndpointTelemetryData).toEqual({ + total_installed: 1, + active_within_last_24_hours: 0, + os: { + [osName]: { + name: osFullName, + version: osVersion, + count: 1, + }, + }, + }); + }); + + it('should show one installed with an active endpoint', async () => { + getFleetSavedObjectsMetadataSpy.mockImplementation(() => + Promise.resolve(mockFleetObjectsResponse()) + ); + getFleetEventsSavedObjectsSpy.mockImplementation(() => + Promise.resolve(mockFleetEventsObjectsResponse(true)) + ); + + const emptyEndpointTelemetryData = await endpointTelemetry.getEndpointMetadataTelemetryFromFleet( + mockSavedObjectsClient + ); + expect(emptyEndpointTelemetryData).toEqual({ + total_installed: 1, + active_within_last_24_hours: 1, + os: { + [osName]: { + name: osFullName, + version: osVersion, + count: 1, + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/endpoint_metadata_telemetry.ts b/x-pack/plugins/security_solution/server/lib/telemetry/endpoint_metadata_telemetry.ts index f476a0fea98a4d9..c86bbc56301bd18 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/endpoint_metadata_telemetry.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/endpoint_metadata_telemetry.ts @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ISavedObjectsRepository } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { AgentMetadata } from './../../../../ingest_manager/common/types/models/agent'; -import { AGENT_SAVED_OBJECT_TYPE } from './../../../../ingest_manager/common/constants/agent'; -import { AgentSOAttributes } from '../../../../ingest_manager/common'; +import { + getFleetSavedObjectsMetadata, + getFleetEventsSavedObjects, +} from './endpoint_telemetry_saved_objects'; -export interface EndpointMetadataOSTelemetry { +export interface EndpointOSMetadataTelemetry { name: string; version: string; count: number; @@ -23,7 +25,19 @@ export interface EndpointPoliciesTelemetry { }; } +export interface EndpointMetadataTelemetry { + total_installed: number; + active_within_last_24_hours: number; + os: Record; + policies?: EndpointPoliciesTelemetry; // TODO: make required when able to enable policy information +} + export interface IngestManagerLocalMetadata extends AgentMetadata { + elastic: { + agent: { + id: string; + }; + }; host: { id: string; }; @@ -34,59 +48,26 @@ export interface IngestManagerLocalMetadata extends AgentMetadata { }; } -export interface EndpointMetadataTelemetry { - endpoints: { - installed: number; - last_24_hours: number; - }; - os: Record; - policies: EndpointPoliciesTelemetry; -} - -export const getDefaultEndpointMetadataTelemetry = (): EndpointMetadataTelemetry => ({ - endpoints: { installed: 0, last_24_hours: 0 }, - os: {} as Record, - policies: { - malware: { - success: 0, - warning: 0, - failure: 0, - }, - }, +export const getDefaultEndpointTelemetry = (): EndpointMetadataTelemetry => ({ + total_installed: 0, + active_within_last_24_hours: 0, + os: {}, }); -/** - * ---Functionality Tracking--- - * Number of active endpoints (in progress) - * - Successfully installed (done) - * - Has sent response within last 24 hours (best way to go about this? May not have info?) - * See the list by customer (provided by telemetry under license.issued_to) Can most likely pull those details in the dashboards - * OS (done) - * - Type (done) - * - Version (done) - * Geolocation of endpoints (slated for 7.10): Query the agent based on the agent info in the metadata to retrieve this - * --------------------------------- - * RE Performance / usage collection timeout from Tina Helligers - * We have a timeout on waiting for collectors to report they are ready before issuing a bulkFetch method that essentially calls the fetch method for all the collectors registered. - * This timeout defaults to 60s. If you suspect that your fetch method is going to take too long (meaning data for that collector won't be reported), - * you could follow a similar approach as what we do for oss_telemetry where we use the task manager to run on a daily basis. - */ - -const ENDPOINT_PACKAGE_CONSTANT = 'system'; // TODO: replace with 'endpoint' when I have endpoint package working remotely - export const getEndpointMetadataTelemetryFromFleet = async ( - savedObjectsClient: ISavedObjectsRepository + savedObjectsClient: SavedObjectsClientContract ) => { - const { saved_objects: endpointAgents } = await savedObjectsClient.find({ - type: AGENT_SAVED_OBJECT_TYPE, - fields: ['packages', 'last_checkin', 'local_metadata'], - filter: `${AGENT_SAVED_OBJECT_TYPE}.attributes.packages: ${ENDPOINT_PACKAGE_CONSTANT}`, - }); + // Retrieve every agent that references the endpoint as an installed package. It will not be listed if it was never installed + const { saved_objects: endpointAgents } = await getFleetSavedObjectsMetadata(savedObjectsClient); + const endpointTelemetry = getDefaultEndpointTelemetry(); + // If there are no installed endpoints return the default telemetry object + if (!endpointAgents || endpointAgents.length < 1) return endpointTelemetry; + const uniqueHostIds: Set = new Set(); - const endpointTelemetry = getDefaultEndpointMetadataTelemetry(); + const uniqueAgentIds: Set = new Set(); - // All agents who have the endpoint registered as an installed package. This shows up in the packages field of the savedObjects response. - endpointTelemetry.endpoints.installed = endpointAgents.length; + // All agents who have the endpoint registered as an installed package. + endpointTelemetry.total_installed = endpointAgents.length; const aDayAgo = new Date(); aDayAgo.setDate(aDayAgo.getDate() - 1); @@ -94,13 +75,12 @@ export const getEndpointMetadataTelemetryFromFleet = async ( const endpointMetadataTelemetry = endpointAgents.reduce( (metadataTelemetry, { attributes: metadataAttributes }) => { const { last_checkin: lastCheckin, local_metadata: localMetadata } = metadataAttributes; - const { host, os } = localMetadata as IngestManagerLocalMetadata; + const { host, os, elastic } = localMetadata as IngestManagerLocalMetadata; - // If there has been a successful check in successfully in the last 24 hours, determine it as active in the last 24 hours. if (lastCheckin && new Date(lastCheckin) > aDayAgo) { - metadataTelemetry.endpoints.last_24_hours += 1; + // Get agents that have checked in within the last 24 hours to later see if their endpoints are running + uniqueAgentIds.add(elastic.agent.id); } - if (host && uniqueHostIds.has(host.id)) { return metadataTelemetry; } else { @@ -122,7 +102,26 @@ export const getEndpointMetadataTelemetryFromFleet = async ( endpointTelemetry ); - // TODO: Update endpoint policies here if possible + // Handle Last 24 Hours + for (const agentId of uniqueAgentIds) { + const { saved_objects: agentEvents } = await getFleetEventsSavedObjects( + savedObjectsClient, + agentId + ); + const lastEndpointStatus = agentEvents.find((agentEvent) => + agentEvent.attributes.message.includes('endpoint-security') + ); + + /* + We can assume that if the last status of the endpoint is RUNNING and the agent has checked in within the last 24 hours + then the endpoint has still been running within the last 24 hours. If / when we get the policy response, then we can use that + instead + */ + const endpointIsActive = lastEndpointStatus?.attributes.subtype === 'RUNNING'; + if (endpointIsActive) { + endpointMetadataTelemetry.active_within_last_24_hours += 1; + } + } return endpointMetadataTelemetry; }; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/endpoint_telemetry_saved_objects.ts b/x-pack/plugins/security_solution/server/lib/telemetry/endpoint_telemetry_saved_objects.ts new file mode 100644 index 000000000000000..8304bea0ddfac9c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/telemetry/endpoint_telemetry_saved_objects.ts @@ -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. + */ + +import { SavedObjectsClientContract } from 'src/core/server'; +import { AgentEventSOAttributes } from './../../../../ingest_manager/common/types/models/agent'; +import { + AGENT_SAVED_OBJECT_TYPE, + AGENT_EVENT_SAVED_OBJECT_TYPE, +} from './../../../../ingest_manager/common/constants/agent'; +import { Agent } from '../../../../ingest_manager/common'; + +export const ENDPOINT_PACKAGE_CONSTANT = 'endpoint'; + +export const getFleetSavedObjectsMetadata = async ( + savedObjectsClient: SavedObjectsClientContract +) => + savedObjectsClient.find({ + type: AGENT_SAVED_OBJECT_TYPE, + fields: ['active', 'packages', 'last_checkin', 'local_metadata'], + filter: `${AGENT_SAVED_OBJECT_TYPE}.attributes.packages: ${ENDPOINT_PACKAGE_CONSTANT}`, + }); + +export const getFleetEventsSavedObjects = async ( + savedObjectsClient: SavedObjectsClientContract, + agentId: string +) => + savedObjectsClient.find({ + type: AGENT_EVENT_SAVED_OBJECT_TYPE, + filter: `${AGENT_EVENT_SAVED_OBJECT_TYPE}.attributes.agent_id: ${agentId}`, + sortField: 'timestamp', + sortOrder: 'desc', + search: agentId, + searchFields: ['agent_id'], + }); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/index.ts b/x-pack/plugins/security_solution/server/lib/telemetry/index.ts index 576970b03b5e100..fecf0ed2af3d792 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/index.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/index.ts @@ -4,17 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { ISavedObjectsRepository } from '../../../../../../src/core/server'; +import { SavedObjectsClientContract } from '../../../../../../src/core/server'; import { EndpointMetadataTelemetry, - getDefaultEndpointMetadataTelemetry, getEndpointMetadataTelemetryFromFleet, } from './endpoint_metadata_telemetry'; const SIEM_USAGE_COLLECTOR_TYPE = 'security_solution'; export async function createSiemTelemetry( usageCollector: UsageCollectionSetup, - savedObjectsClient: ISavedObjectsRepository + savedObjectsClient: SavedObjectsClientContract ) { const collector = usageCollector.makeUsageCollector({ type: SIEM_USAGE_COLLECTOR_TYPE, @@ -25,13 +24,12 @@ export async function createSiemTelemetry( savedObjectsClient ); return { - ...endpointMetadataTelemetryFromFleet, + endpoint: { + ...endpointMetadataTelemetryFromFleet, + }, }; } catch (err) { - if (err.output?.statusCode === 404) { - return {}; - } - return getDefaultEndpointMetadataTelemetry(); // TODO: no data returned if an error occured or should fail silently here? + return {}; } }, });