[EDR Workflows] Extend Defend Insights telemetry events to include new fields. (#216967)

This PR introduces a new event type,
`endpoint_workflow_insights_remediated_event`, and extends some of the
existing ones. The goal is to enable better monitoring of the **Defend
Insights** feature usage.

### Event Types

- **`defend_insight_success`** – Sent when the Scan button triggers an
API call and an insight is successfully created. This carries most of
the valuable data, such as result contents, duration, etc.
- **`endpoint_workflow_insights_remediated_event`** – Sent when a
Trusted App is added as a result of an insight, and that insight is
marked as remediated.
- **`defend_insight_error`** – Sent when insight generation fails and no
results are returned.

### Data sent to telemetry

**`defend_insight_error`**
```
actionTypeId   – Kibana connector type  
errorMessage   – Error message from ES/LLM  
model          – LLM model  
provider       – Model provider  
```

**`endpoint_workflow_insights_remediated_event`**
```
insightId      – The ID of the action that was sent to the endpoint (currently unused)  
```

**`defend_insight_success`**
```
actionTypeId        – Kibana connector type  
eventsContextCount  – Number of events sent as context to the LLM  
insightsGenerated   – Number of Defend insights generated  
durationMs          – Duration of the request in milliseconds  
model               – LLM model  
provider            – Model provider  
insightType         – Type of Defend insight (e.g., incompatible-antivirus)  
insightsDetails     – Details of the generated insights (e.g., ["ClamAV", "Avast"])  
```
This commit is contained in:
Konrad Szwarc 2025-04-10 10:37:20 +02:00 committed by GitHub
parent 4ca5e062f7
commit 387e2d95ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 78 additions and 8 deletions

View file

@ -456,6 +456,8 @@ export const DEFEND_INSIGHT_SUCCESS_EVENT: EventTypeOpts<{
durationMs: number;
model?: string;
provider?: string;
insightType: string;
insightsDetails: string[];
}> = {
eventType: 'defend_insight_success',
schema: {
@ -501,6 +503,25 @@ export const DEFEND_INSIGHT_SUCCESS_EVENT: EventTypeOpts<{
optional: true,
},
},
insightType: {
type: 'keyword',
_meta: {
description: 'Defend insight type',
optional: false,
},
},
insightsDetails: {
type: 'array',
items: {
type: 'keyword',
_meta: {
description: 'Details of the generated Defend insights',
},
},
_meta: {
description: 'Details of the generated Defend insights',
},
},
},
};

View file

@ -129,6 +129,7 @@ describe('defend insights route helpers', () => {
rawDefendInsights: '{"eventsContextCount": 5, "insights": ["insight1", "insight2"]}',
startTime: moment(),
telemetry: { reportEvent: jest.fn() } as any,
insightType: DefendInsightType.Enum.incompatible_antivirus,
};
await updateDefendInsights(params);

View file

@ -14,7 +14,7 @@ import {
SavedObjectsClientContract,
} from '@kbn/core/server';
import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import type {
import {
ApiConfig,
ContentReferencesStore,
DefendInsightGenerationInterval,
@ -22,6 +22,10 @@ import type {
DefendInsightsPostRequestBody,
DefendInsightsResponse,
Replacements,
DEFEND_INSIGHTS_ID,
DefendInsightStatus,
DefendInsightType,
DefendInsightsGetRequestQuery,
} from '@kbn/elastic-assistant-common';
import type { AnonymizationFieldResponse } from '@kbn/elastic-assistant-common/impl/schemas/anonymization_fields/bulk_crud_anonymization_fields_route.gen';
import type { ActionsClient } from '@kbn/actions-plugin/server';
@ -30,12 +34,6 @@ import { ActionsClientLlm } from '@kbn/langchain/server';
import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith';
import { PublicMethodsOf } from '@kbn/utility-types';
import { transformError } from '@kbn/securitysolution-es-utils';
import {
DEFEND_INSIGHTS_ID,
DefendInsightStatus,
DefendInsightType,
DefendInsightsGetRequestQuery,
} from '@kbn/elastic-assistant-common';
import { getDefendInsightsPrompt } from '../../lib/defend_insights/graphs/default_defend_insights_graph/nodes/helpers/prompts';
import type { GraphState } from '../../lib/defend_insights/graphs/default_defend_insights_graph/types';
@ -269,6 +267,18 @@ export async function createDefendInsight(
};
}
const extractInsightsForTelemetryReporting = (
insightType: DefendInsightType,
insights: DefendInsights
): string[] => {
switch (insightType) {
case DefendInsightType.Enum.incompatible_antivirus:
return insights.map((insight) => insight.group);
default:
return [];
}
};
export async function updateDefendInsights({
anonymizedEvents,
apiConfig,
@ -280,6 +290,7 @@ export async function updateDefendInsights({
logger,
startTime,
telemetry,
insightType,
}: {
anonymizedEvents: Document[];
apiConfig: ApiConfig;
@ -291,6 +302,7 @@ export async function updateDefendInsights({
logger: Logger;
startTime: Moment;
telemetry: AnalyticsServiceSetup;
insightType: DefendInsightType;
}) {
try {
const currentInsight = await dataClient.getDefendInsight({
@ -324,13 +336,19 @@ export async function updateDefendInsights({
defendInsightUpdateProps: updateProps,
authenticatedUser,
});
telemetry.reportEvent(DEFEND_INSIGHT_SUCCESS_EVENT.eventType, {
actionTypeId: apiConfig.actionTypeId,
eventsContextCount: updateProps.eventsContextCount,
insightsGenerated: updateProps.insights?.length ?? 0,
insightsDetails: extractInsightsForTelemetryReporting(
insightType,
updateProps.insights || []
),
durationMs,
model: apiConfig.model,
provider: apiConfig.provider,
insightType,
});
} catch (updateErr) {
logger.error(updateErr);

View file

@ -162,6 +162,7 @@ export const postDefendInsightsRoute = (router: IRouter<ElasticAssistantRequestH
logger,
startTime,
telemetry,
insightType,
}).then(() => insights)
)
.then((insights) =>

View file

@ -41,6 +41,9 @@ describe('Update Insights Route Handler', () => {
service: {
...mockEndpointContext.service,
getEndpointAuthz: jest.fn().mockResolvedValue(authz),
getTelemetryService: jest.fn().mockReturnValue({
reportEvent: jest.fn(),
}),
},
securitySolution: {
getEndpointAuthz: jest.fn().mockResolvedValue(authz),

View file

@ -6,6 +6,7 @@
*/
import type { RequestHandler } from '@kbn/core/server';
import { ENDPOINT_WORKFLOW_INSIGHTS_REMEDIATED_EVENT } from '../../../lib/telemetry/event_based/events';
import type {
UpdateWorkflowInsightsRequestBody,
UpdateWorkflowInsightsRequestParams,
@ -73,9 +74,18 @@ const updateInsightsRouteHandler = (
return async (_, request, response) => {
const { insightId } = request.params;
const { canWriteWorkflowInsights } = await endpointContext.service.getEndpointAuthz(request);
if (!canWriteWorkflowInsights && !isOnlyActionTypeUpdate(request.body)) {
const onlyActionTypeUpdate = isOnlyActionTypeUpdate(request.body);
if (!canWriteWorkflowInsights && !onlyActionTypeUpdate) {
return response.forbidden({ body: 'Unauthorized to update workflow insights' });
}
if (onlyActionTypeUpdate) {
if (request.body.action?.type === 'remediated') {
const telemetry = endpointContext.service.getTelemetryService();
telemetry.reportEvent(ENDPOINT_WORKFLOW_INSIGHTS_REMEDIATED_EVENT.eventType, {
insightId,
});
}
}
logger.debug(`Updating insight ${insightId}`);
try {
const body = await securityWorkflowInsightsService.update(

View file

@ -1241,6 +1241,21 @@ export const SIEM_MIGRATIONS_RULE_TRANSLATION_FAILURE: EventTypeOpts<{
},
};
export const ENDPOINT_WORKFLOW_INSIGHTS_REMEDIATED_EVENT: EventTypeOpts<{
insightId: string;
}> = {
eventType: 'endpoint_workflow_insights_remediated_event',
schema: {
insightId: {
type: 'keyword',
_meta: {
description: 'The ID of the action that was sent to the endpoint',
optional: false,
},
},
},
};
export const events = [
RISK_SCORE_EXECUTION_SUCCESS_EVENT,
RISK_SCORE_EXECUTION_ERROR_EVENT,
@ -1250,6 +1265,7 @@ export const events = [
ENDPOINT_RESPONSE_ACTION_SENT_EVENT,
ENDPOINT_RESPONSE_ACTION_SENT_ERROR_EVENT,
ENDPOINT_RESPONSE_ACTION_STATUS_CHANGE_EVENT,
ENDPOINT_WORKFLOW_INSIGHTS_REMEDIATED_EVENT,
FIELD_RETENTION_ENRICH_POLICY_EXECUTION_EVENT,
ENTITY_STORE_DATA_VIEW_REFRESH_EXECUTION_EVENT,
ENTITY_ENGINE_RESOURCE_INIT_FAILURE_EVENT,