Skip to content

Commit 6095aa1

Browse files
authored
[7.x] [Security Solution][Telemetry] Concurrent telemetry requests (#73558) (#73893)
1 parent 8108ed0 commit 6095aa1

File tree

6 files changed

+94
-52
lines changed

6 files changed

+94
-52
lines changed

x-pack/plugins/security_solution/server/usage/collector.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { LegacyAPICaller, CoreSetup } from '../../../../../src/core/server';
88
import { CollectorDependencies } from './types';
9-
import { DetectionsUsage, fetchDetectionsUsage } from './detections';
9+
import { DetectionsUsage, fetchDetectionsUsage, defaultDetectionsUsage } from './detections';
1010
import { EndpointUsage, getEndpointTelemetryFromFleet } from './endpoints';
1111

1212
export type RegisterCollector = (deps: CollectorDependencies) => void;
@@ -76,9 +76,14 @@ export const registerCollector: RegisterCollector = ({
7676
isReady: () => kibanaIndex.length > 0,
7777
fetch: async (callCluster: LegacyAPICaller): Promise<UsageData> => {
7878
const savedObjectsClient = await getInternalSavedObjectsClient(core);
79+
const [detections, endpoints] = await Promise.allSettled([
80+
fetchDetectionsUsage(kibanaIndex, callCluster, ml),
81+
getEndpointTelemetryFromFleet(savedObjectsClient),
82+
]);
83+
7984
return {
80-
detections: await fetchDetectionsUsage(kibanaIndex, callCluster, ml),
81-
endpoints: await getEndpointTelemetryFromFleet(savedObjectsClient),
85+
detections: detections.status === 'fulfilled' ? detections.value : defaultDetectionsUsage,
86+
endpoints: endpoints.status === 'fulfilled' ? endpoints.value : {},
8287
};
8388
},
8489
});

x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ interface DetectionsMetric {
2323

2424
const isElasticRule = (tags: string[]) => tags.includes(`${INTERNAL_IMMUTABLE_KEY}:true`);
2525

26-
const initialRulesUsage: DetectionRulesUsage = {
26+
/**
27+
* Default detection rule usage count
28+
*/
29+
export const initialRulesUsage: DetectionRulesUsage = {
2730
custom: {
2831
enabled: 0,
2932
disabled: 0,
@@ -34,7 +37,10 @@ const initialRulesUsage: DetectionRulesUsage = {
3437
},
3538
};
3639

37-
const initialMlJobsUsage: MlJobsUsage = {
40+
/**
41+
* Default ml job usage count
42+
*/
43+
export const initialMlJobsUsage: MlJobsUsage = {
3844
custom: {
3945
enabled: 0,
4046
disabled: 0,

x-pack/plugins/security_solution/server/usage/detections/index.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
*/
66

77
import { LegacyAPICaller } from '../../../../../../src/core/server';
8-
import { getMlJobsUsage, getRulesUsage } from './detections_helpers';
8+
import {
9+
getMlJobsUsage,
10+
getRulesUsage,
11+
initialRulesUsage,
12+
initialMlJobsUsage,
13+
} from './detections_helpers';
914
import { MlPluginSetup } from '../../../../ml/server';
1015

1116
interface FeatureUsage {
@@ -28,12 +33,23 @@ export interface DetectionsUsage {
2833
ml_jobs: MlJobsUsage;
2934
}
3035

36+
export const defaultDetectionsUsage = {
37+
detection_rules: initialRulesUsage,
38+
ml_jobs: initialMlJobsUsage,
39+
};
40+
3141
export const fetchDetectionsUsage = async (
3242
kibanaIndex: string,
3343
callCluster: LegacyAPICaller,
3444
ml: MlPluginSetup | undefined
3545
): Promise<DetectionsUsage> => {
36-
const rulesUsage = await getRulesUsage(kibanaIndex, callCluster);
37-
const mlJobsUsage = await getMlJobsUsage(ml);
38-
return { detection_rules: rulesUsage, ml_jobs: mlJobsUsage };
46+
const [rulesUsage, mlJobsUsage] = await Promise.allSettled([
47+
getRulesUsage(kibanaIndex, callCluster),
48+
getMlJobsUsage(ml),
49+
]);
50+
51+
return {
52+
detection_rules: rulesUsage.status === 'fulfilled' ? rulesUsage.value : initialRulesUsage,
53+
ml_jobs: mlJobsUsage.status === 'fulfilled' ? mlJobsUsage.value : initialMlJobsUsage,
54+
};
3955
};

x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { FLEET_ENDPOINT_PACKAGE_CONSTANT } from './fleet_saved_objects';
1515
const testAgentId = 'testAgentId';
1616
const testConfigId = 'testConfigId';
1717
const testHostId = 'randoHostId';
18+
const testHostName = 'testDesktop';
1819

1920
/** Mock OS Platform for endpoint telemetry */
2021
export const MockOSPlatform = 'somePlatform';
@@ -56,8 +57,8 @@ export const mockFleetObjectsResponse = (
5657
},
5758
},
5859
host: {
59-
hostname: 'testDesktop',
60-
name: 'testDesktop',
60+
hostname: testHostName,
61+
name: testHostName,
6162
id: testHostId,
6263
},
6364
os: {
@@ -93,8 +94,8 @@ export const mockFleetObjectsResponse = (
9394
},
9495
},
9596
host: {
96-
hostname: 'testDesktop',
97-
name: 'testDesktop',
97+
hostname: hasDuplicates ? testHostName : 'oldRandoHostName',
98+
name: hasDuplicates ? testHostName : 'oldRandoHostName',
9899
id: hasDuplicates ? testHostId : 'oldRandoHostId',
99100
},
100101
os: {

x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export const getFleetSavedObjectsMetadata = async (savedObjectsClient: ISavedObj
2323
'last_checkin',
2424
'local_metadata.agent.id',
2525
'local_metadata.host.id',
26+
'local_metadata.host.name',
27+
'local_metadata.host.hostname',
2628
'local_metadata.elastic.agent.id',
2729
'local_metadata.os',
2830
],

x-pack/plugins/security_solution/server/usage/endpoints/index.ts

Lines changed: 51 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ export interface AgentLocalMetadata extends AgentMetadata {
4242
};
4343
};
4444
host: {
45+
hostname: string;
4546
id: string;
47+
name: string;
4648
};
4749
os: {
4850
name: string;
@@ -78,17 +80,20 @@ export const updateEndpointOSTelemetry = (
7880
os: AgentLocalMetadata['os'],
7981
osTracker: OSTracker
8082
): OSTracker => {
81-
const updatedOSTracker = cloneDeep(osTracker);
82-
const { version: osVersion, platform: osPlatform, full: osFullName } = os;
83-
if (osFullName && osVersion) {
84-
if (updatedOSTracker[osFullName]) updatedOSTracker[osFullName].count += 1;
85-
else {
86-
updatedOSTracker[osFullName] = {
87-
full_name: osFullName,
88-
platform: osPlatform,
89-
version: osVersion,
90-
count: 1,
91-
};
83+
let updatedOSTracker = osTracker;
84+
if (os && typeof os === 'object') {
85+
updatedOSTracker = cloneDeep(osTracker);
86+
const { version: osVersion, platform: osPlatform, full: osFullName } = os;
87+
if (osFullName && osVersion) {
88+
if (updatedOSTracker[osFullName]) updatedOSTracker[osFullName].count += 1;
89+
else {
90+
updatedOSTracker[osFullName] = {
91+
full_name: osFullName,
92+
platform: osPlatform,
93+
version: osVersion,
94+
count: 1,
95+
};
96+
}
9297
}
9398
}
9499

@@ -211,46 +216,53 @@ export const getEndpointTelemetryFromFleet = async (
211216
if (!endpointAgents || endpointAgentsCount < 1) return endpointTelemetry;
212217

213218
// Use unique hosts to prevent any potential duplicates
214-
const uniqueHostIds: Set<string> = new Set();
219+
const uniqueHosts: Set<string> = new Set();
215220
let osTracker: OSTracker = {};
216221
let dailyActiveCount = 0;
217222
let policyTracker: PoliciesTelemetry = { malware: { active: 0, inactive: 0, failure: 0 } };
218223

219224
for (let i = 0; i < endpointAgentsCount; i += 1) {
220-
const { attributes: metadataAttributes } = endpointAgents[i];
221-
const { last_checkin: lastCheckin, local_metadata: localMetadata } = metadataAttributes;
222-
const { host, os, elastic } = localMetadata as AgentLocalMetadata; // AgentMetadata is just an empty blob, casting for our use case
223-
224-
if (!uniqueHostIds.has(host.id)) {
225-
uniqueHostIds.add(host.id);
226-
const agentId = elastic?.agent?.id;
227-
osTracker = updateEndpointOSTelemetry(os, osTracker);
228-
229-
if (agentId) {
230-
let agentEvents;
231-
try {
232-
const response = await getLatestFleetEndpointEvent(soClient, agentId);
233-
agentEvents = response.saved_objects;
234-
} catch (error) {
235-
// If the request fails we do not obtain `active within last 24 hours for this agent` or policy specifics
236-
}
237-
238-
// AgentEvents will have a max length of 1
239-
if (agentEvents && agentEvents.length > 0) {
240-
const latestEndpointEvent = agentEvents[0];
241-
dailyActiveCount = updateEndpointDailyActiveCount(
242-
latestEndpointEvent,
243-
lastCheckin,
244-
dailyActiveCount
225+
try {
226+
const { attributes: metadataAttributes } = endpointAgents[i];
227+
const { last_checkin: lastCheckin, local_metadata: localMetadata } = metadataAttributes;
228+
const { host, os, elastic } = localMetadata as AgentLocalMetadata;
229+
230+
// Although not perfect, the goal is to dedupe hosts to get the most recent data for a host
231+
// An agent re-installed on the same host will have the same id and hostname
232+
// A cloned VM will have the same id, but "may" have the same hostname, but it's really up to the user.
233+
const compoundUniqueId = `${host?.id}-${host?.hostname}`;
234+
if (!uniqueHosts.has(compoundUniqueId)) {
235+
uniqueHosts.add(compoundUniqueId);
236+
const agentId = elastic?.agent?.id;
237+
osTracker = updateEndpointOSTelemetry(os, osTracker);
238+
239+
if (agentId) {
240+
const { saved_objects: agentEvents } = await getLatestFleetEndpointEvent(
241+
soClient,
242+
agentId
245243
);
246-
policyTracker = updateEndpointPolicyTelemetry(latestEndpointEvent, policyTracker);
244+
245+
// AgentEvents will have a max length of 1
246+
if (agentEvents && agentEvents.length > 0) {
247+
const latestEndpointEvent = agentEvents[0];
248+
dailyActiveCount = updateEndpointDailyActiveCount(
249+
latestEndpointEvent,
250+
lastCheckin,
251+
dailyActiveCount
252+
);
253+
policyTracker = updateEndpointPolicyTelemetry(latestEndpointEvent, policyTracker);
254+
}
247255
}
248256
}
257+
} catch (error) {
258+
// All errors thrown in the loop would be handled here
259+
// Not logging any errors to avoid leaking any potential PII
260+
// Depending on when the error is thrown in the loop some specifics may be missing, but it allows the loop to continue
249261
}
250262
}
251263

252264
// All unique hosts with an endpoint installed, thus all unique endpoint installs
253-
endpointTelemetry.total_installed = uniqueHostIds.size;
265+
endpointTelemetry.total_installed = uniqueHosts.size;
254266
// Set the daily active count for the endpoints
255267
endpointTelemetry.active_within_last_24_hours = dailyActiveCount;
256268
// Get the objects to populate our OS Telemetry

0 commit comments

Comments
 (0)