Skip to content

Commit dfe542b

Browse files
authored
[EDR Workflows] Workflow Insights - insights generating script (elastic#213094)
This PR introduces a new script for loading parameterized workflow insights into a data stream. It enables UI/UX testing without requiring an agent installation or generating insights manually. Arguments ``` --endpointId Required. The endpoint ID to use for generating workflow insights. --elasticsearch Optional. The URL to Elasticsearch. Default: http://localhost:9200 --username Optional. The username to use for authentication. Default: elastic --password Optional. The password to use for authentication. Default: changeme --count Optional. The number of workflow insights to generate. Default: 5 --os Optional. The OS to use for generating workflow insights. Default: linux --antivirus Optional. The antivirus to use for generating workflow insights. Default: ClamAV --path Optional. The executable path of the AV to use for generating workflow insights. Default: /usr/bin/clamscan ``` Example usage: * Load 5 workflow insights, using the default values - Linux, ClamAV, /usr/bin/clamscan on the endpoint with ID 8ee2a3a4-ca2b-4884-ae20-8b17d31837b6 `node ./load_workflow_insights.js --endpointId 8ee2a3a4-ca2b-4884-ae20-8b17d31837b6` * Load 10 workflow insights for Malwarebytes with path of C:\\Program Files\\Malwarebytes\\Anti-Malware\\mbam.exe on Windows endpoint with ID 8ee2a3a4-ca2b-4884-ae20-8b17d31837b6 `node ./load_workflow_insights.js --endpointId 8ee2a3a4-ca2b-4884-ae20-8b17d31837b6 --count 10 --os windows --antivirus Malwarebytes --path 'C:\\Program Files\\Malwarebytes\\Anti-Malware\\mbam.exe'`
1 parent 847be91 commit dfe542b

File tree

3 files changed

+302
-0
lines changed

3 files changed

+302
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import moment from 'moment';
9+
10+
import type { ToolingLog } from '@kbn/tooling-log';
11+
import type { Client, estypes } from '@elastic/elasticsearch';
12+
import {
13+
ActionType,
14+
Category,
15+
type SecurityWorkflowInsight,
16+
SourceType,
17+
TargetType,
18+
} from '../types/workflow_insights';
19+
20+
export interface IndexedWorkflowInsights {
21+
data: estypes.BulkResponse;
22+
cleanup: () => Promise<DeletedWorkflowInsights>;
23+
}
24+
25+
export interface DeletedWorkflowInsights {
26+
data: estypes.BulkResponse;
27+
}
28+
29+
export const indexWorkflowInsights = async ({
30+
esClient,
31+
log,
32+
endpointId,
33+
os,
34+
count,
35+
antivirus,
36+
path,
37+
}: {
38+
esClient: Client;
39+
log: ToolingLog;
40+
endpointId: string;
41+
os: 'windows' | 'macos' | 'linux';
42+
count: number;
43+
antivirus: string;
44+
path: string;
45+
}): Promise<IndexedWorkflowInsights> => {
46+
log.debug(`Indexing ${count} workflow insights`);
47+
48+
const operations = Array.from({ length: count }).flatMap((_, i) => {
49+
return [
50+
{
51+
index: {
52+
_index: '.edr-workflow-insights-default',
53+
op_type: 'create',
54+
},
55+
},
56+
generateWorkflowInsightsDoc({ endpointId, os, runNumber: i, antivirus, path }),
57+
];
58+
});
59+
60+
const response = await esClient.bulk({
61+
refresh: 'wait_for',
62+
operations,
63+
});
64+
65+
if (response.errors) {
66+
log.error(
67+
`There was an error indexing workflow insights ${JSON.stringify(response.items, null, 2)}`
68+
);
69+
} else {
70+
log.debug(`Indexed ${count} workflow insights successfully`);
71+
}
72+
73+
return {
74+
data: response,
75+
cleanup: deleteIndexedWorkflowInsights.bind(null, esClient, response, log),
76+
};
77+
};
78+
79+
const deleteIndexedWorkflowInsights = async (
80+
esClient: Client,
81+
indexedWorkflowInsights: IndexedWorkflowInsights['data'],
82+
log: ToolingLog
83+
): Promise<DeletedWorkflowInsights> => {
84+
log.debug(`Deleting ${indexedWorkflowInsights.items.length} indexed workflow insights`);
85+
let response: estypes.BulkResponse = {
86+
took: 0,
87+
errors: false,
88+
items: [],
89+
};
90+
91+
if (indexedWorkflowInsights.items.length) {
92+
const idsToDelete = indexedWorkflowInsights.items
93+
.filter((item) => item.create)
94+
.map((item) => ({
95+
delete: {
96+
_index: item.create?._index,
97+
_id: item.create?._id,
98+
},
99+
}));
100+
101+
if (idsToDelete.length) {
102+
response = await esClient.bulk({
103+
operations: idsToDelete,
104+
});
105+
log.debug('Indexed workflow insights deleted successfully');
106+
}
107+
}
108+
109+
return {
110+
data: response,
111+
};
112+
};
113+
114+
const generateWorkflowInsightsDoc = ({
115+
endpointId,
116+
os,
117+
runNumber,
118+
antivirus,
119+
path,
120+
}: {
121+
endpointId: string;
122+
os: 'linux' | 'windows' | 'macos';
123+
runNumber: number;
124+
antivirus: string;
125+
path: string;
126+
}): SecurityWorkflowInsight => {
127+
const currentTime = moment();
128+
const signatureField =
129+
os === 'linux'
130+
? undefined
131+
: os === 'windows'
132+
? 'process.Ext.code_signature'
133+
: 'process.code_signature';
134+
135+
const signatureValue = os === 'linux' ? undefined : 'Elastic';
136+
return {
137+
remediation: {
138+
exception_list_items: [
139+
{
140+
entries: [
141+
{
142+
field: 'process.executable.caseless',
143+
type: 'match',
144+
value:
145+
os !== 'windows'
146+
? `/${runNumber}${path}`
147+
: (() => {
148+
const parts = path.split('\\'); // Split by Windows path separator
149+
const lastPart = parts.pop(); // Get the last part (executable)
150+
return `${parts.join('\\')}\\${runNumber}\\${lastPart}`; // Reconstruct the path
151+
})(),
152+
operator: 'included',
153+
},
154+
...(signatureField && signatureValue
155+
? [
156+
{
157+
field: signatureField,
158+
operator: 'included' as const,
159+
type: 'match' as const,
160+
value: signatureValue,
161+
},
162+
]
163+
: []),
164+
],
165+
list_id: 'endpoint_trusted_apps',
166+
name: `${antivirus}`,
167+
os_types: [os],
168+
description: 'Suggested by Security Workflow Insights',
169+
tags: ['policy:all'],
170+
},
171+
],
172+
},
173+
metadata: {
174+
notes: {
175+
llm_model: '',
176+
},
177+
display_name: `${antivirus}`,
178+
},
179+
'@timestamp': currentTime,
180+
action: {
181+
type: ActionType.Refreshed,
182+
timestamp: currentTime,
183+
},
184+
source: {
185+
data_range_end: currentTime.clone().add(24, 'hours'),
186+
id: '7184ab52-c318-4c91-b765-805f889e34e2',
187+
type: SourceType.LlmConnector,
188+
data_range_start: currentTime,
189+
},
190+
message: 'Incompatible antiviruses detected',
191+
category: Category.Endpoint,
192+
type: 'incompatible_antivirus',
193+
value: `${antivirus} ${path}${signatureValue ? ` ${signatureValue}` : ''}`,
194+
target: {
195+
ids: [endpointId],
196+
type: TargetType.Endpoint,
197+
},
198+
};
199+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
require('../../../../../../../src/setup_node_env');
9+
require('./workflow_isnights').cli();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import type { RunFn } from '@kbn/dev-cli-runner';
9+
import { run } from '@kbn/dev-cli-runner';
10+
import { createFailError } from '@kbn/dev-cli-errors';
11+
import { ok } from 'assert';
12+
import { indexWorkflowInsights } from '../../../common/endpoint/data_loaders/index_workflow_insights';
13+
import { createEsClient } from '../common/stack_services';
14+
15+
export const cli = () => {
16+
run(
17+
async (options) => {
18+
try {
19+
const totalCount = options.flags.count;
20+
options.log.success(`Loading ${totalCount} workflow insights`);
21+
22+
const startTime = new Date().getTime();
23+
await workflowInsightsLoader(options);
24+
const endTime = new Date().getTime();
25+
26+
options.log.success(`${totalCount} workflow insights loaded`);
27+
options.log.info(`Loading ${totalCount} workflow insights took ${endTime - startTime}ms`);
28+
} catch (e) {
29+
options.log.error(e);
30+
throw createFailError(e.message);
31+
}
32+
},
33+
{
34+
description: 'Workflow Insights ES Loader',
35+
flags: {
36+
string: [
37+
'elasticsearch',
38+
'username',
39+
'password',
40+
'endpointId',
41+
'count',
42+
'os',
43+
'antivirus',
44+
'path',
45+
],
46+
default: {
47+
elasticsearch: 'http://localhost:9200',
48+
username: 'elastic',
49+
password: 'changeme',
50+
count: 5,
51+
os: 'linux',
52+
antivirus: 'ClamAV',
53+
path: '/usr/bin/clamscan',
54+
},
55+
help: `
56+
--endpointId Required. The endpoint ID to use for generating workflow insights.
57+
--elasticsearch Optional. The URL to Elasticsearch. Default: http://localhost:9200
58+
--username Optional. The username to use for authentication. Default: elastic
59+
--password Optional. The password to use for authentication. Default: changeme
60+
--count Optional. The number of workflow insights to generate. Default: 5
61+
--os Optional. The OS to use for generating workflow insights. Default: linux
62+
--antivirus Optional. The antivirus to use for generating workflow insights. Default: ClamAV
63+
--path Optional. The executable path of the AV to use for generating workflow insights. Default: /usr/bin/clamscan
64+
`,
65+
examples: `
66+
Load 5 workflow insights, using the default values - Linux, ClamAV, /usr/bin/clamscan on the endpoint with ID 8ee2a3a4-ca2b-4884-ae20-8b17d31837b6
67+
node ./load_workflow_insights.js --endpointId 8ee2a3a4-ca2b-4884-ae20-8b17d31837b6
68+
Load 10 workflow insights for Malwarebytes with path of C:\\Program Files\\Malwarebytes\\Anti-Malware\\mbam.exe on Windows endpoint with ID 8ee2a3a4-ca2b-4884-ae20-8b17d31837b6
69+
node ./load_workflow_insights.js --endpointId 8ee2a3a4-ca2b-4884-ae20-8b17d31837b6 --count 10 --os windows --antivirus Malwarebytes --path 'C:\\Program Files\\Malwarebytes\\Anti-Malware\\mbam.exe'
70+
`,
71+
},
72+
}
73+
);
74+
};
75+
76+
const workflowInsightsLoader: RunFn = async ({ flags, log }) => {
77+
const url = flags.elasticsearch as string;
78+
const username = flags.username as string;
79+
const password = flags.password as string;
80+
const endpointId = flags.endpointId as string;
81+
const os = flags.os as 'linux' | 'windows' | 'macos';
82+
const antivirus = flags.antivirus as string;
83+
const path = flags.path as string;
84+
const count = Number(flags.count);
85+
86+
const getRequiredArgMessage = (argName: string) => `${argName} argument is required`;
87+
88+
ok(endpointId, getRequiredArgMessage('endpointId'));
89+
if (os) ok(['linux', 'windows', 'macos'].includes(os), getRequiredArgMessage('os'));
90+
91+
const esClient = createEsClient({ url, username, password });
92+
93+
await indexWorkflowInsights({ esClient, log, endpointId, os, count, antivirus, path });
94+
};

0 commit comments

Comments
 (0)