Remove <7.14 EP Metrics from Security Solution usage collector (#103632)

* Remove <7.14 EP Metrics from Security Solution usage collector.

* Update telemetry schema.

* Fix reworked method signature.
This commit is contained in:
Pete Hampton 2021-06-29 16:53:11 +01:00 committed by GitHub
parent cdfc90ca30
commit 096ee92f3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 19 additions and 1128 deletions

View file

@ -171,7 +171,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
initUsageCollectors({
core,
endpointAppContext: endpointContext,
kibanaIndex: globalConfig.kibana.index,
signalsIndex: config.signalsIndex,
ml: plugins.ml,

View file

@ -9,11 +9,9 @@ import { CoreSetup, SavedObjectsClientContract } from '../../../../../src/core/s
import { CollectorFetchContext } from '../../../../../src/plugins/usage_collection/server';
import { CollectorDependencies } from './types';
import { fetchDetectionsMetrics } from './detections';
import { EndpointUsage, getEndpointTelemetryFromFleet } from './endpoints';
export type RegisterCollector = (deps: CollectorDependencies) => void;
export interface UsageData {
endpoints: EndpointUsage | {};
detectionMetrics: {};
}
@ -25,7 +23,6 @@ export async function getInternalSavedObjectsClient(core: CoreSetup) {
export const registerCollector: RegisterCollector = ({
core,
endpointAppContext,
kibanaIndex,
signalsIndex,
ml,
@ -397,69 +394,15 @@ export const registerCollector: RegisterCollector = ({
},
},
},
endpoints: {
total_installed: {
type: 'long',
_meta: { description: 'The number of installed endpoints' },
},
active_within_last_24_hours: {
type: 'long',
_meta: { description: 'The number of active endpoints' },
},
os: {
type: 'array',
items: {
full_name: {
type: 'keyword',
_meta: { description: 'Full name of the operating system' },
},
platform: {
type: 'keyword',
_meta: { description: 'OS Platform. eg Centos, Ubuntu' },
},
version: {
type: 'keyword',
_meta: {
description:
'The version of the operating system, eg 16.04.7 LTS (Xenial Xerus), 8 (Core)',
},
},
count: {
type: 'long',
_meta: { description: 'The total number of endpoints from that platform' },
},
},
},
policies: {
malware: {
active: {
type: 'long',
_meta: { description: 'The total number of active malware policies' },
},
inactive: {
type: 'long',
_meta: { description: 'The total number of inactive malware policies' },
},
failure: {
type: 'long',
_meta: { description: 'The total number of failing malware policies' },
},
},
},
},
},
isReady: () => true,
fetch: async ({ esClient }: CollectorFetchContext): Promise<UsageData> => {
const internalSavedObjectsClient = await getInternalSavedObjectsClient(core);
const savedObjectsClient = (internalSavedObjectsClient as unknown) as SavedObjectsClientContract;
const [detectionMetrics, endpoints] = await Promise.allSettled([
fetchDetectionsMetrics(kibanaIndex, signalsIndex, esClient, ml, savedObjectsClient),
getEndpointTelemetryFromFleet(savedObjectsClient, endpointAppContext, esClient),
]);
const soClient = (internalSavedObjectsClient as unknown) as SavedObjectsClientContract;
return {
detectionMetrics: detectionMetrics.status === 'fulfilled' ? detectionMetrics.value : {},
endpoints: endpoints.status === 'fulfilled' ? endpoints.value : {},
detectionMetrics:
(await fetchDetectionsMetrics(kibanaIndex, signalsIndex, esClient, soClient, ml)) || {},
};
},
});

View file

@ -37,7 +37,7 @@ describe('Detections Usage and Metrics', () => {
});
it('returns zeroed counts if calls are empty', async () => {
const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient);
const result = await fetchDetectionsMetrics('', '', esClientMock, savedObjectsClient, mlMock);
expect(result).toEqual(
expect.objectContaining({
@ -99,7 +99,7 @@ describe('Detections Usage and Metrics', () => {
.mockReturnValue({ body: getMockRuleAlertsResponse(3400) });
(savedObjectsClient.find as jest.Mock).mockReturnValue(getMockAlertCasesResponse());
const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient);
const result = await fetchDetectionsMetrics('', '', esClientMock, savedObjectsClient, mlMock);
expect(result).toEqual(
expect.objectContaining({
@ -174,7 +174,7 @@ describe('Detections Usage and Metrics', () => {
.mockReturnValue({ body: getMockRuleAlertsResponse(800) });
(savedObjectsClient.find as jest.Mock).mockReturnValue(getMockAlertCasesResponse());
const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient);
const result = await fetchDetectionsMetrics('', '', esClientMock, savedObjectsClient, mlMock);
expect(result).toEqual(
expect.objectContaining({
@ -236,7 +236,7 @@ describe('Detections Usage and Metrics', () => {
.mockReturnValue({ body: getMockRuleAlertsResponse(0) });
(savedObjectsClient.find as jest.Mock).mockReturnValue(getMockAlertCasesResponse());
const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient);
const result = await fetchDetectionsMetrics('', '', esClientMock, savedObjectsClient, mlMock);
expect(result).toEqual(
expect.objectContaining({
@ -317,7 +317,7 @@ describe('Detections Usage and Metrics', () => {
jobs: null,
jobStats: null,
} as unknown) as ReturnType<typeof mlMock.anomalyDetectorsProvider>);
const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient);
const result = await fetchDetectionsMetrics('', '', esClientMock, savedObjectsClient, mlMock);
expect(result).toEqual(
expect.objectContaining({
@ -347,7 +347,7 @@ describe('Detections Usage and Metrics', () => {
datafeedStats: mockDatafeedStatsResponse,
} as unknown) as ReturnType<typeof mlMock.anomalyDetectorsProvider>);
const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient);
const result = await fetchDetectionsMetrics('', '', esClientMock, savedObjectsClient, mlMock);
expect(result).toEqual(
expect.objectContaining({

View file

@ -20,12 +20,12 @@ export const fetchDetectionsMetrics = async (
kibanaIndex: string,
signalsIndex: string,
esClient: ElasticsearchClient,
ml: MlPluginSetup | undefined,
savedObjectClient: SavedObjectsClientContract
soClient: SavedObjectsClientContract,
mlClient: MlPluginSetup | undefined
): Promise<DetectionMetrics> => {
const [mlJobMetrics, detectionRuleMetrics] = await Promise.allSettled([
getMlJobMetrics(ml, savedObjectClient),
getDetectionRuleMetrics(kibanaIndex, signalsIndex, esClient, savedObjectClient),
getMlJobMetrics(mlClient, soClient),
getDetectionRuleMetrics(kibanaIndex, signalsIndex, esClient, soClient),
]);
return {

View file

@ -1,259 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObjectsFindResponse } from 'src/core/server';
import { Agent, FLEET_ENDPOINT_PACKAGE } from '../../../../fleet/common';
import { AGENT_EVENT_SAVED_OBJECT_TYPE } from './fleet_saved_objects';
const testAgentId = 'testAgentId';
const testAgentPolicyId = 'testAgentPolicyId';
const testHostId = 'randoHostId';
const testHostName = 'testDesktop';
/** Mock OS Platform for endpoint telemetry */
export const MockOSPlatform = 'somePlatform';
/** Mock OS Name for endpoint telemetry */
export const MockOSName = 'somePlatformName';
/** Mock OS Version for endpoint telemetry */
export const MockOSVersion = '1';
/** Mock OS Full Name for endpoint telemetry */
export const MockOSFullName = 'somePlatformFullName';
/**
*
* @param lastCheckIn - the last time the agent checked in. Defaults to current ISO time.
* @description We request the install and OS related telemetry information from the 'fleet-agents' saved objects in ingest_manager. This mocks that response
*/
export const mockFleetObjectsResponse = (
hasDuplicates = true,
lastCheckIn = new Date().toISOString()
): { agents: Agent[]; total: number; page: number; perPage: number } | undefined => ({
page: 1,
perPage: 20,
total: 1,
agents: [
{
active: true,
id: testAgentId,
policy_id: 'randoAgentPolicyId',
type: 'PERMANENT',
user_provided_metadata: {},
enrolled_at: lastCheckIn,
local_metadata: {
elastic: {
agent: {
id: testAgentId,
},
},
host: {
hostname: testHostName,
name: testHostName,
id: testHostId,
},
os: {
platform: MockOSPlatform,
version: MockOSVersion,
name: MockOSName,
full: MockOSFullName,
},
},
packages: [FLEET_ENDPOINT_PACKAGE, 'system'],
last_checkin: lastCheckIn,
},
{
active: true,
id: 'oldTestAgentId',
policy_id: 'randoAgentPolicyId',
type: 'PERMANENT',
user_provided_metadata: {},
enrolled_at: lastCheckIn,
local_metadata: {
elastic: {
agent: {
id: 'oldTestAgentId',
},
},
host: {
hostname: hasDuplicates ? testHostName : 'oldRandoHostName',
name: hasDuplicates ? testHostName : 'oldRandoHostName',
id: hasDuplicates ? testHostId : 'oldRandoHostId',
},
os: {
platform: MockOSPlatform,
version: MockOSVersion,
name: MockOSName,
full: MockOSFullName,
},
},
packages: [FLEET_ENDPOINT_PACKAGE, 'system'],
last_checkin: lastCheckIn,
},
],
});
const mockPolicyPayload = (
policyStatus: 'success' | 'warning' | 'failure',
policyMode: 'prevent' | 'detect' | 'off' = 'prevent'
) =>
JSON.stringify({
'endpoint-security': {
Endpoint: {
configuration: {
inputs: [
{
id: '0d466df0-c60f-11ea-a5c5-151665e785c4',
policy: {
linux: {
events: {
file: true,
network: true,
process: true,
},
logging: {
file: 'info',
},
},
mac: {
events: {
file: true,
network: true,
process: true,
},
logging: {
file: 'info',
},
malware: {
mode: policyMode,
},
},
windows: {
events: {
dll_and_driver_load: true,
dns: true,
file: true,
network: true,
process: true,
registry: true,
security: true,
},
logging: {
file: 'info',
},
malware: {
mode: policyMode,
},
},
},
},
],
},
policy: {
applied: {
id: '0d466df0-c60f-11ea-a5c5-151665e785c4',
response: {
configurations: {
malware: {
concerned_actions: [
'load_config',
'workflow',
'download_global_artifacts',
'download_user_artifacts',
'configure_malware',
'read_malware_config',
'load_malware_model',
'read_kernel_config',
'configure_kernel',
'detect_process_events',
'detect_file_write_events',
'connect_kernel',
'detect_file_open_events',
'detect_sync_image_load_events',
],
status: `${policyStatus}`,
},
},
},
status: `${policyStatus}`,
},
},
},
agent: {
id: 'testAgentId',
version: '8.0.0-SNAPSHOT',
},
host: {
architecture: 'x86_64',
id: 'a4148b63-1758-ab1f-a6d3-f95075cb1a9c',
os: {
Ext: {
variant: 'Windows 10 Pro',
},
full: 'Windows 10 Pro 2004 (10.0.19041.329)',
name: 'Windows',
version: '2004 (10.0.19041.329)',
},
},
},
});
/**
*
* @param running - allows us to set whether the mocked endpoint is in an active or disabled/failed state
* @param updatedDate - the last time the endpoint was updated. Defaults to current ISO time.
* @description We request the events triggered by the agent and get the most recent endpoint event to confirm it is still running. This allows us to mock both scenarios
*/
export const mockFleetEventsObjectsResponse = (
running?: boolean,
updatedDate = new Date().toISOString(),
policyStatus: 'success' | 'failure' = running ? 'success' : 'failure',
policyMode: 'prevent' | 'detect' | 'off' = 'prevent'
): 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'
}: `,
payload: running ? mockPolicyPayload(policyStatus, policyMode) : undefined,
policy_id: testAgentPolicyId,
},
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',
policy_id: testAgentPolicyId,
},
references: [],
updated_at: updatedDate,
version: 'WzExNywxXQ==',
score: 0,
},
],
};
};

View file

@ -1,394 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks';
import {
mockFleetObjectsResponse,
mockFleetEventsObjectsResponse,
MockOSFullName,
MockOSPlatform,
MockOSVersion,
} from './endpoint.mocks';
import { SavedObjectsClientContract, SavedObjectsFindResponse } from 'src/core/server';
import { Agent } from '../../../../fleet/common';
import * as endpointTelemetry from './index';
import * as fleetSavedObjects from './fleet_saved_objects';
import { createMockEndpointAppContext } from '../../endpoint/mocks';
import { EndpointAppContext } from '../../endpoint/types';
describe('test security solution endpoint telemetry', () => {
let mockSavedObjectsClient: jest.Mocked<SavedObjectsClientContract>;
let mockEndpointAppContext: EndpointAppContext;
let mockEsClient: ReturnType<typeof elasticsearchServiceMock.createElasticsearchClient>;
let getEndpointIntegratedFleetMetadataSpy: jest.SpyInstance<
Promise<{ agents: Agent[]; total: number; page: number; perPage: number } | undefined>
>;
let getLatestFleetEndpointEventSpy: jest.SpyInstance<Promise<SavedObjectsFindResponse>>;
beforeAll(() => {
getLatestFleetEndpointEventSpy = jest.spyOn(fleetSavedObjects, 'getLatestFleetEndpointEvent');
getEndpointIntegratedFleetMetadataSpy = jest.spyOn(
fleetSavedObjects,
'getEndpointIntegratedFleetMetadata'
);
mockSavedObjectsClient = savedObjectsClientMock.create();
mockEndpointAppContext = createMockEndpointAppContext();
mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
});
afterAll(() => {
jest.resetAllMocks();
});
it('should have a default shape', () => {
expect(endpointTelemetry.getDefaultEndpointTelemetry()).toMatchInlineSnapshot(`
Object {
"active_within_last_24_hours": 0,
"os": Array [],
"policies": Object {
"malware": Object {
"active": 0,
"failure": 0,
"inactive": 0,
},
},
"total_installed": 0,
}
`);
});
describe('when a request for endpoint agents fails', () => {
it('should return an empty object', async () => {
getEndpointIntegratedFleetMetadataSpy.mockImplementation(() =>
Promise.reject(Error('No agents for you'))
);
const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet(
mockSavedObjectsClient,
mockEndpointAppContext,
mockEsClient
);
expect(getEndpointIntegratedFleetMetadataSpy).toHaveBeenCalled();
expect(endpointUsage).toEqual({});
});
});
describe('when an agent has not been installed', () => {
it('should return the default shape if no agents are found', async () => {
getEndpointIntegratedFleetMetadataSpy.mockImplementation(() =>
Promise.resolve({ agents: [], total: 0, perPage: 0, page: 0 })
);
const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet(
mockSavedObjectsClient,
mockEndpointAppContext,
mockEsClient
);
expect(getEndpointIntegratedFleetMetadataSpy).toHaveBeenCalled();
expect(endpointUsage).toEqual({
total_installed: 0,
active_within_last_24_hours: 0,
os: [],
policies: {
malware: {
failure: 0,
active: 0,
inactive: 0,
},
},
});
});
});
describe('when agent(s) have been installed', () => {
describe('when a request for events has failed', () => {
it('should show only one endpoint installed but it is inactive', async () => {
getEndpointIntegratedFleetMetadataSpy.mockImplementation(() =>
Promise.resolve(mockFleetObjectsResponse())
);
getLatestFleetEndpointEventSpy.mockImplementation(() =>
Promise.reject(Error('No events for you'))
);
const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet(
mockSavedObjectsClient,
mockEndpointAppContext,
mockEsClient
);
expect(endpointUsage).toEqual({
total_installed: 1,
active_within_last_24_hours: 0,
os: [
{
full_name: MockOSFullName,
platform: MockOSPlatform,
version: MockOSVersion,
count: 1,
},
],
policies: {
malware: {
failure: 0,
active: 0,
inactive: 0,
},
},
});
});
});
describe('when a request for events is successful', () => {
it('should show one endpoint installed but endpoint has failed to run', async () => {
getEndpointIntegratedFleetMetadataSpy.mockImplementation(() =>
Promise.resolve(mockFleetObjectsResponse())
);
getLatestFleetEndpointEventSpy.mockImplementation(() =>
Promise.resolve(mockFleetEventsObjectsResponse())
);
const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet(
mockSavedObjectsClient,
mockEndpointAppContext,
mockEsClient
);
expect(endpointUsage).toEqual({
total_installed: 1,
active_within_last_24_hours: 0,
os: [
{
full_name: MockOSFullName,
platform: MockOSPlatform,
version: MockOSVersion,
count: 1,
},
],
policies: {
malware: {
failure: 0,
active: 0,
inactive: 0,
},
},
});
});
it('should show two endpoints installed but both endpoints have failed to run', async () => {
getEndpointIntegratedFleetMetadataSpy.mockImplementation(() =>
Promise.resolve(mockFleetObjectsResponse(false))
);
getLatestFleetEndpointEventSpy.mockImplementation(() =>
Promise.resolve(mockFleetEventsObjectsResponse())
);
const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet(
mockSavedObjectsClient,
mockEndpointAppContext,
mockEsClient
);
expect(endpointUsage).toEqual({
total_installed: 2,
active_within_last_24_hours: 0,
os: [
{
full_name: MockOSFullName,
platform: MockOSPlatform,
version: MockOSVersion,
count: 2,
},
],
policies: {
malware: {
failure: 0,
active: 0,
inactive: 0,
},
},
});
});
it('should show two endpoints installed but agents have not checked in within past day', async () => {
const twoDaysAgo = new Date();
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
const twoDaysAgoISOString = twoDaysAgo.toISOString();
getEndpointIntegratedFleetMetadataSpy.mockImplementation(() =>
Promise.resolve(mockFleetObjectsResponse(false, twoDaysAgoISOString))
);
getLatestFleetEndpointEventSpy.mockImplementation(
() => Promise.resolve(mockFleetEventsObjectsResponse(true, twoDaysAgoISOString)) // agent_id doesn't matter for mock here
);
const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet(
mockSavedObjectsClient,
mockEndpointAppContext,
mockEsClient
);
expect(endpointUsage).toEqual({
total_installed: 2,
active_within_last_24_hours: 0,
os: [
{
full_name: MockOSFullName,
platform: MockOSPlatform,
version: MockOSVersion,
count: 2,
},
],
policies: {
malware: {
failure: 0,
active: 2,
inactive: 0,
},
},
});
});
it('should show one endpoint installed and endpoint is running', async () => {
getEndpointIntegratedFleetMetadataSpy.mockImplementation(() =>
Promise.resolve(mockFleetObjectsResponse())
);
getLatestFleetEndpointEventSpy.mockImplementation(() =>
Promise.resolve(mockFleetEventsObjectsResponse(true))
);
const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet(
mockSavedObjectsClient,
mockEndpointAppContext,
mockEsClient
);
expect(endpointUsage).toEqual({
total_installed: 1,
active_within_last_24_hours: 1,
os: [
{
full_name: MockOSFullName,
platform: MockOSPlatform,
version: MockOSVersion,
count: 1,
},
],
policies: {
malware: {
failure: 0,
active: 1,
inactive: 0,
},
},
});
});
describe('malware policy', () => {
it('should have failed to enable', async () => {
getEndpointIntegratedFleetMetadataSpy.mockImplementation(() =>
Promise.resolve(mockFleetObjectsResponse())
);
getLatestFleetEndpointEventSpy.mockImplementation(() =>
Promise.resolve(
mockFleetEventsObjectsResponse(true, new Date().toISOString(), 'failure')
)
);
const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet(
mockSavedObjectsClient,
mockEndpointAppContext,
mockEsClient
);
expect(endpointUsage).toEqual({
total_installed: 1,
active_within_last_24_hours: 1,
os: [
{
full_name: MockOSFullName,
platform: MockOSPlatform,
version: MockOSVersion,
count: 1,
},
],
policies: {
malware: {
failure: 1,
active: 0,
inactive: 0,
},
},
});
});
it('should be enabled successfully', async () => {
getEndpointIntegratedFleetMetadataSpy.mockImplementation(() =>
Promise.resolve(mockFleetObjectsResponse())
);
getLatestFleetEndpointEventSpy.mockImplementation(() =>
Promise.resolve(mockFleetEventsObjectsResponse(true))
);
const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet(
mockSavedObjectsClient,
mockEndpointAppContext,
mockEsClient
);
expect(endpointUsage).toEqual({
total_installed: 1,
active_within_last_24_hours: 1,
os: [
{
full_name: MockOSFullName,
platform: MockOSPlatform,
version: MockOSVersion,
count: 1,
},
],
policies: {
malware: {
failure: 0,
active: 1,
inactive: 0,
},
},
});
});
it('should be disabled successfully', async () => {
getEndpointIntegratedFleetMetadataSpy.mockImplementation(() =>
Promise.resolve(mockFleetObjectsResponse())
);
getLatestFleetEndpointEventSpy.mockImplementation(() =>
Promise.resolve(
mockFleetEventsObjectsResponse(true, new Date().toISOString(), 'success', 'off')
)
);
const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet(
mockSavedObjectsClient,
mockEndpointAppContext,
mockEsClient
);
expect(endpointUsage).toEqual({
total_installed: 1,
active_within_last_24_hours: 1,
os: [
{
full_name: MockOSFullName,
platform: MockOSPlatform,
version: MockOSVersion,
count: 1,
},
],
policies: {
malware: {
failure: 0,
active: 0,
inactive: 1,
},
},
});
});
});
});
});
});

View file

@ -1,43 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
ElasticsearchClient,
SavedObjectsClientContract,
SavedObjectsFindResponse,
} from 'src/core/server';
import { AgentService } from '../../../../fleet/server';
import { FLEET_ENDPOINT_PACKAGE } from '../../../../fleet/common';
export const AGENT_EVENT_SAVED_OBJECT_TYPE = 'donotexistsanymore-since-7.13';
export const getEndpointIntegratedFleetMetadata = async (
agentService: AgentService | undefined,
esClient: ElasticsearchClient
) => {
return agentService?.listAgents(esClient, {
kuery: `(packages : ${FLEET_ENDPOINT_PACKAGE})`,
perPage: 10000,
showInactive: false,
sortField: 'enrolled_at',
sortOrder: 'desc',
});
};
/*
TODO: AS OF 7.13, this access will no longer work due to the enabling of fleet server. An alternative route will have
to be discussed to retrieve the policy data we need, as well as when the endpoint was last active, which is obtained
via the last endpoint 'check in' event that was sent to fleet. Also, the only policy currently tracked is `malware`,
but the hope is to add more, so a better/more scalable solution would be desirable.
*/
export const getLatestFleetEndpointEvent = async (
savedObjectsClient: SavedObjectsClientContract,
agentId: string
): Promise<SavedObjectsFindResponse> =>
// Agent events saved object do not exists in Fleet anymore
({ total: 0, saved_objects: [], page: 0, per_page: 0 });

View file

@ -1,284 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { cloneDeep } from 'lodash';
import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server';
import { SavedObject } from './../../../../../../src/core/types/saved_objects';
import { Agent } from './../../../../fleet/common/types/models/agent';
import { AgentMetadata } from '../../../../fleet/common/types/models/agent';
import {
getEndpointIntegratedFleetMetadata,
getLatestFleetEndpointEvent,
} from './fleet_saved_objects';
import { EndpointAppContext } from '../../endpoint/types';
export interface AgentOSMetadataTelemetry {
full_name: string;
platform: string;
version: string;
count: number;
}
export interface PolicyTelemetry {
active: number;
inactive: number;
failure: number;
}
export interface PoliciesTelemetry {
malware: PolicyTelemetry;
}
export interface EndpointUsage {
total_installed: number;
active_within_last_24_hours: number;
os: AgentOSMetadataTelemetry[];
policies: PoliciesTelemetry;
}
type EndpointOSNames = 'Linux' | 'Windows' | 'macOS';
export interface AgentLocalMetadata extends AgentMetadata {
elastic: {
agent: {
id: string;
};
};
host: {
hostname: string;
id: string;
name: string;
};
os: {
name: string;
platform: string;
version: string;
full: string;
};
}
type OSTracker = Record<string, AgentOSMetadataTelemetry>;
/**
* @description returns an empty telemetry object to be incrmented and updated within the `getEndpointTelemetryFromFleet` fn
*/
export const getDefaultEndpointTelemetry = (): EndpointUsage => ({
total_installed: 0,
active_within_last_24_hours: 0,
os: [],
policies: {
malware: {
active: 0,
inactive: 0,
failure: 0,
},
},
});
/**
* @description this function updates the os telemetry. We use the fullName field as the key as it contains the name and version details.
* If it has already been tracked, the count will be updated, otherwise a tracker will be initialized for that fullName.
*/
export const updateEndpointOSTelemetry = (
os: AgentLocalMetadata['os'],
osTracker: OSTracker
): OSTracker => {
let updatedOSTracker = osTracker;
if (os && typeof os === 'object') {
updatedOSTracker = cloneDeep(osTracker);
const { version: osVersion, platform: osPlatform, full: osFullName } = os;
if (osFullName && osVersion) {
if (updatedOSTracker[osFullName]) updatedOSTracker[osFullName].count += 1;
else {
updatedOSTracker[osFullName] = {
full_name: osFullName,
platform: osPlatform,
version: osVersion,
count: 1,
};
}
}
}
return updatedOSTracker;
};
/**
* @description we take the latest endpoint specific agent event, get the status of the endpoint, and if it is running
* and the agent itself has been active within the last 24 hours, we can safely assume the endpoint has been active within
* the same time span.
*/
export const updateEndpointDailyActiveCount = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
latestEndpointEvent: SavedObject<any>, // TODO: This information will be lost in 7.13, need to find an alternative route.
lastAgentCheckin: Agent['last_checkin'],
currentCount: number
) => {
const aDayAgo = new Date();
aDayAgo.setDate(aDayAgo.getDate() - 1);
const agentWasActiveOverLastDay = !!lastAgentCheckin && new Date(lastAgentCheckin) > aDayAgo;
return agentWasActiveOverLastDay && latestEndpointEvent.attributes.subtype === 'RUNNING'
? currentCount + 1
: currentCount;
};
/**
* @description We take the latest endpoint specific agent event, and as long as it provides the payload with policy details, we will parse that policy
* to populate the success of it's application. The policy is provided in the agent health checks.
*/
export const updateEndpointPolicyTelemetry = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
latestEndpointEvent: SavedObject<any>,
policiesTracker: PoliciesTelemetry
): PoliciesTelemetry => {
const policyHostTypeToPolicyType = {
Linux: 'linux',
macOS: 'mac',
Windows: 'windows',
};
const enabledMalwarePolicyTypes = ['prevent', 'detect'];
// The policy details are sent as a string on the 'payload' attribute of the agent event
const { payload } = latestEndpointEvent.attributes;
if (!payload) {
// This payload may not always be provided depending on the state of the endpoint. Guard again situations where it is not sent
return policiesTracker;
}
let endpointPolicyPayload;
try {
endpointPolicyPayload = JSON.parse(latestEndpointEvent.attributes.payload);
} catch (error) {
return policiesTracker;
}
// Get the platform: windows, mac, or linux
const hostType =
policyHostTypeToPolicyType[
endpointPolicyPayload['endpoint-security']?.host?.os?.name as EndpointOSNames
];
// Get whether the malware setting for the platform on the most recently provided config is active (prevent or detect is on) or off
const userDesiredMalwareState =
endpointPolicyPayload['endpoint-security'].Endpoint?.configuration?.inputs[0]?.policy[hostType]
?.malware?.mode;
// Get the status of the application of the malware protection
const malwareStatus =
endpointPolicyPayload['endpoint-security'].Endpoint?.policy?.applied?.response?.configurations
?.malware?.status;
if (!userDesiredMalwareState || !malwareStatus) {
// If we get policy information without the mode or status, then nothing to track or update
return policiesTracker;
}
const updatedPoliciesTracker = {
malware: { ...policiesTracker.malware },
};
const isAnActiveMalwareState = enabledMalwarePolicyTypes.includes(userDesiredMalwareState);
// we only check for 'not failure' as the 'warning' state for malware is still technically actively enabled (with warnings)
const successfullyEnabled = !!malwareStatus && malwareStatus !== 'failure';
const failedToEnable = !!malwareStatus && malwareStatus === 'failure';
if (isAnActiveMalwareState && successfullyEnabled) {
updatedPoliciesTracker.malware.active += 1;
} else if (!isAnActiveMalwareState && successfullyEnabled) {
updatedPoliciesTracker.malware.inactive += 1;
} else if (isAnActiveMalwareState && failedToEnable) {
updatedPoliciesTracker.malware.failure += 1;
}
return updatedPoliciesTracker;
};
/**
* @description This aggregates the telemetry details from the fleet agent service `listAgents` and the fleet saved object `fleet-agent-events` to populate
* the telemetry details for endpoint. Since we cannot access our own indices due to `kibana_system` not having access, this is the best alternative.
* Once the data is requested, we iterate over all agents with endpoints registered, and then request the events for each active agent (within last 24 hours)
* to confirm whether or not the endpoint is still active
*/
export const getEndpointTelemetryFromFleet = async (
soClient: SavedObjectsClientContract,
endpointAppContext: EndpointAppContext,
esClient: ElasticsearchClient
): Promise<EndpointUsage | {}> => {
// Retrieve every agent (max 10000) that references the endpoint as an installed package. It will not be listed if it was never installed
let endpointAgents;
const agentService = endpointAppContext.service.getAgentService();
try {
const response = await getEndpointIntegratedFleetMetadata(agentService, esClient);
endpointAgents = response?.agents ?? [];
} catch (error) {
// Better to provide an empty object rather than default telemetry as this better informs us of an error
return {};
}
const endpointAgentsCount = endpointAgents.length;
const endpointTelemetry = getDefaultEndpointTelemetry();
// If there are no installed endpoints return the default telemetry object
if (!endpointAgents || endpointAgentsCount < 1) return endpointTelemetry;
// Use unique hosts to prevent any potential duplicates
const uniqueHosts: Set<string> = new Set();
let osTracker: OSTracker = {};
let dailyActiveCount = 0;
let policyTracker: PoliciesTelemetry = { malware: { active: 0, inactive: 0, failure: 0 } };
for (let i = 0; i < endpointAgentsCount; i += 1) {
try {
const { last_checkin: lastCheckin, local_metadata: localMetadata } = endpointAgents[i];
const { host, os, elastic } = localMetadata as AgentLocalMetadata;
// Although not perfect, the goal is to dedupe hosts to get the most recent data for a host
// An agent re-installed on the same host will have the same id and hostname
// A cloned VM will have the same id, but "may" have the same hostname, but it's really up to the user.
const compoundUniqueId = `${host?.id}-${host?.hostname}`;
if (!uniqueHosts.has(compoundUniqueId)) {
uniqueHosts.add(compoundUniqueId);
const agentId = elastic?.agent?.id;
osTracker = updateEndpointOSTelemetry(os, osTracker);
if (agentId) {
const { saved_objects: agentEvents } = await getLatestFleetEndpointEvent(
soClient,
agentId
);
// AgentEvents will have a max length of 1
if (agentEvents && agentEvents.length > 0) {
const latestEndpointEvent = agentEvents[0];
dailyActiveCount = updateEndpointDailyActiveCount(
latestEndpointEvent,
lastCheckin,
dailyActiveCount
);
policyTracker = updateEndpointPolicyTelemetry(latestEndpointEvent, policyTracker);
}
}
}
} catch (error) {
// All errors thrown in the loop would be handled here
// Not logging any errors to avoid leaking any potential PII
// Depending on when the error is thrown in the loop some specifics may be missing, but it allows the loop to continue
}
}
// All unique hosts with an endpoint installed, thus all unique endpoint installs
endpointTelemetry.total_installed = uniqueHosts.size;
// Set the daily active count for the endpoints
endpointTelemetry.active_within_last_24_hours = dailyActiveCount;
// Get the objects to populate our OS Telemetry
endpointTelemetry.os = Object.values(osTracker);
// Provide the updated policy information
endpointTelemetry.policies = policyTracker;
return endpointTelemetry;
};

View file

@ -6,12 +6,10 @@
*/
import { CoreSetup } from 'src/core/server';
import { EndpointAppContext } from '../endpoint/types';
import { SetupPlugins } from '../plugin';
export type CollectorDependencies = {
kibanaIndex: string;
signalsIndex: string;
core: CoreSetup;
endpointAppContext: EndpointAppContext;
} & Pick<SetupPlugins, 'ml' | 'usageCollection'>;

View file

@ -2192,7 +2192,9 @@
},
"open_formula_popover": {
"type": "long",
"_meta": { "description": "Number of times the user opened the in-product formula help popover." }
"_meta": {
"description": "Number of times the user opened the in-product formula help popover."
}
},
"toggle_fullscreen_formula": {
"type": "long",
@ -2431,7 +2433,9 @@
},
"open_formula_popover": {
"type": "long",
"_meta": { "description": "Number of times the user opened the in-product formula help popover." }
"_meta": {
"description": "Number of times the user opened the in-product formula help popover."
}
},
"toggle_fullscreen_formula": {
"type": "long",
@ -5429,79 +5433,6 @@
}
}
}
},
"endpoints": {
"properties": {
"total_installed": {
"type": "long",
"_meta": {
"description": "The number of installed endpoints"
}
},
"active_within_last_24_hours": {
"type": "long",
"_meta": {
"description": "The number of active endpoints"
}
},
"os": {
"type": "array",
"items": {
"properties": {
"full_name": {
"type": "keyword",
"_meta": {
"description": "Full name of the operating system"
}
},
"platform": {
"type": "keyword",
"_meta": {
"description": "OS Platform. eg Centos, Ubuntu"
}
},
"version": {
"type": "keyword",
"_meta": {
"description": "The version of the operating system, eg 16.04.7 LTS (Xenial Xerus), 8 (Core)"
}
},
"count": {
"type": "long",
"_meta": {
"description": "The total number of endpoints from that platform"
}
}
}
}
},
"policies": {
"properties": {
"malware": {
"properties": {
"active": {
"type": "long",
"_meta": {
"description": "The total number of active malware policies"
}
},
"inactive": {
"type": "long",
"_meta": {
"description": "The total number of inactive malware policies"
}
},
"failure": {
"type": "long",
"_meta": {
"description": "The total number of failing malware policies"
}
}
}
}
}
}
}
}
}
},