Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EDR Workflows] Workflow Insights - migrate to Signature field #205323

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export const DefendInsightEvent = z.object({
* The value of the event
*/
value: z.string(),
/**
* The signer's ID
*/
signerId: z.string().optional(),
});

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ components:
value:
description: The value of the event
type: string
signerId:
description: The signer's ID
type: string

DefendInsightType:
description: The insight type (ie. incompatible_antivirus)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ export const WorkflowInsightsResults = ({
{insight.message}
</EuiText>
<EuiText size={'xs'} color={'subdued'}>
{item.entries[0].type === 'match' && item.entries[0].value}
{item.entries[0].type === 'match' &&
item.entries[0].field === 'process.executable.caseless' &&
item.entries[0].value}
</EuiText>
</EuiText>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const SIZE = 200;
export function getFileEventsQuery({ endpointIds }: { endpointIds: string[] }): SearchRequest {
return {
allow_no_indices: true,
fields: ['_id', 'agent.id', 'process.executable'],
fields: ['_id', 'agent.id', 'process.executable', 'process.code_signature.signing_id'],
query: {
bool: {
must: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export function getIncompatibleVirusOutputParser() {
.object({
id: z.string().describe('The event ID'),
endpointId: z.string().describe('The endpoint ID'),
signerId: z
.string()
.optional()
.describe('The process.code_signature.signing_id value of the event, if present'),
value: z.string().describe('The process.executable value of the event'),
})
.array()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

export function getIncompatibleAntivirusPrompt({ events }: { events: string[] }): string {
return `You are an Elastic Security user tasked with analyzing file events from Elastic Security to identify antivirus processes. Only focus on detecting antivirus processes. Ignore processes that belong to Elastic Agent or Elastic Defend, that are not antivirus processes, or are typical processes built into the operating system. Accuracy is of the utmost importance, try to minimize false positives. Group the processes by the antivirus program, keeping track of the agent.id and _id associated to each of the individual events as endpointId and eventId respectively. If there are no events, ignore the group field. Escape backslashes to respect JSON validation. New lines must always be escaped with double backslashes, i.e. \\\\n to ensure valid JSON. Only return JSON output, as described above. Do not add any additional text to describe your output.
return `You are an Elastic Security user tasked with analyzing file events from Elastic Security to identify antivirus processes. Only focus on detecting antivirus processes. Ignore processes that belong to Elastic Agent or Elastic Defend, that are not antivirus processes, or are typical processes built into the operating system. Accuracy is of the utmost importance, try to minimize false positives. Group the processes by the antivirus program, keeping track of the agent.id and _id associated to each of the individual events as endpointId and eventId respectively. If there are no events, ignore the group field. If the process.code_signature.signing_id field is present in an event, set its value in the signerId field. If the process.code_signature.signing_id field is absent, ignore the signerId field. Escape backslashes to respect JSON validation. New lines must always be escaped with double backslashes, i.e. \\\\n to ensure valid JSON. Only return JSON output, as described above. Do not add any additional text to describe your output.

Use context from the following process events to provide insights:
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,104 +30,140 @@ jest.mock('../helpers', () => ({
}));

describe('buildIncompatibleAntivirusWorkflowInsights', () => {
let params: BuildWorkflowInsightParams;
const mockEndpointAppContextService = createMockEndpointAppContext().service;
mockEndpointAppContextService.getEndpointMetadataService = jest.fn().mockReturnValue({
getMetadataForEndpoints: jest.fn(),
});
const endpointMetadataService =
mockEndpointAppContextService.getEndpointMetadataService() as jest.Mocked<EndpointMetadataService>;

beforeEach(() => {
const mockEndpointAppContextService = createMockEndpointAppContext().service;
mockEndpointAppContextService.getEndpointMetadataService = jest.fn().mockReturnValue({
getMetadataForEndpoints: jest.fn(),
});
const endpointMetadataService =
mockEndpointAppContextService.getEndpointMetadataService() as jest.Mocked<EndpointMetadataService>;

params = {
defendInsights: [
{
group: 'AVGAntivirus',
events: [
{
id: 'lqw5opMB9Ke6SNgnxRSZ',
endpointId: 'f6e2f338-6fb7-4c85-9c23-d20e9f96a051',
value: '/Applications/AVGAntivirus.app/Contents/Backend/services/com.avg.activity',
},
],
const generateParams = (signerId?: string): BuildWorkflowInsightParams => ({
defendInsights: [
{
group: 'AVGAntivirus',
events: [
{
id: 'lqw5opMB9Ke6SNgnxRSZ',
endpointId: 'f6e2f338-6fb7-4c85-9c23-d20e9f96a051',
value: '/Applications/AVGAntivirus.app/Contents/Backend/services/com.avg.activity',
...(signerId ? { signerId } : {}),
},
],
},
],
request: {
body: {
insightType: 'incompatible_antivirus',
endpointIds: ['endpoint-1'],
apiConfig: {
connectorId: 'connector-id-1',
actionTypeId: 'action-type-id-1',
model: 'model-1',
},
],
request: {
body: {
insightType: 'incompatible_antivirus',
endpointIds: ['endpoint-1'],
apiConfig: {
connectorId: 'connector-id-1',
actionTypeId: 'action-type-id-1',
model: 'model-1',
anonymizationFields: [],
subAction: 'invokeAI',
},
} as unknown as KibanaRequest<unknown, unknown, DefendInsightsPostRequestBody>,
endpointMetadataService,
});

const buildExpectedInsight = (os: string, field: string, value: string) =>
expect.objectContaining({
'@timestamp': expect.any(moment),
message: 'Incompatible antiviruses detected',
category: Category.Endpoint,
type: 'incompatible_antivirus',
source: {
type: SourceType.LlmConnector,
id: 'connector-id-1',
data_range_start: expect.any(moment),
data_range_end: expect.any(moment),
},
target: {
type: TargetType.Endpoint,
ids: ['endpoint-1'],
},
action: {
type: ActionType.Refreshed,
timestamp: expect.any(moment),
},
value: 'AVGAntivirus',
remediation: {
exception_list_items: [
{
list_id: ENDPOINT_ARTIFACT_LISTS.trustedApps.id,
name: 'AVGAntivirus',
description: 'Suggested by Security Workflow Insights',
entries: [
{
field,
operator: 'included',
type: 'match',
value,
},
],
tags: ['policy:all'],
os_types: [os],
},
anonymizationFields: [],
subAction: 'invokeAI',
],
},
metadata: {
notes: {
llm_model: 'model-1',
},
} as unknown as KibanaRequest<unknown, unknown, DefendInsightsPostRequestBody>,
endpointMetadataService,
};
},
});

it('should correctly build workflow insights', async () => {
(groupEndpointIdsByOS as jest.Mock).mockResolvedValue({
windows: ['endpoint-1'],
});
const params = generateParams();
const result = await buildIncompatibleAntivirusWorkflowInsights(params);

expect(result).toEqual([
buildExpectedInsight(
'windows',
'process.executable.caseless',
'/Applications/AVGAntivirus.app/Contents/Backend/services/com.avg.activity'
),
]);
expect(groupEndpointIdsByOS).toHaveBeenCalledWith(
['endpoint-1'],
params.endpointMetadataService
);
});

it('should correctly build workflow insights', async () => {
it('should correctly build workflow insights for Windows with signerId provided', async () => {
(groupEndpointIdsByOS as jest.Mock).mockResolvedValue({
windows: ['endpoint-1'],
});
const params = generateParams('test.com');

const result = await buildIncompatibleAntivirusWorkflowInsights(params);

expect(result).toEqual([
expect.objectContaining({
'@timestamp': expect.any(moment),
message: 'Incompatible antiviruses detected',
category: Category.Endpoint,
type: 'incompatible_antivirus',
source: {
type: SourceType.LlmConnector,
id: 'connector-id-1',
data_range_start: expect.any(moment),
data_range_end: expect.any(moment),
},
target: {
type: TargetType.Endpoint,
ids: ['endpoint-1'],
},
action: {
type: ActionType.Refreshed,
timestamp: expect.any(moment),
},
value: 'AVGAntivirus',
remediation: {
exception_list_items: [
{
list_id: ENDPOINT_ARTIFACT_LISTS.trustedApps.id,
name: 'AVGAntivirus',
description: 'Suggested by Security Workflow Insights',
entries: [
{
field: 'process.executable.caseless',
operator: 'included',
type: 'match',
value:
'/Applications/AVGAntivirus.app/Contents/Backend/services/com.avg.activity',
},
],
tags: ['policy:all'],
os_types: ['windows'],
},
],
},
metadata: {
notes: {
llm_model: 'model-1',
},
},
}),
buildExpectedInsight('windows', 'process.Ext.code_signature', 'test.com'),
]);
expect(groupEndpointIdsByOS).toHaveBeenCalledWith(
['endpoint-1'],
params.endpointMetadataService
);
});

it('should correctly build workflow insights for MacOS with signerId provided', async () => {
(groupEndpointIdsByOS as jest.Mock).mockResolvedValue({
macos: ['endpoint-1'],
});

const params = generateParams('test.com');

const result = await buildIncompatibleAntivirusWorkflowInsights(params);

expect(result).toEqual([buildExpectedInsight('macos', 'process.code_signature', 'test.com')]);
expect(groupEndpointIdsByOS).toHaveBeenCalledWith(
['endpoint-1'],
params.endpointMetadataService
);
});
});
Loading