mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[EDR Workflows] Workflow Insights - Proper Windows Signer field handling (#209117)
This PR fixes an issue where the Signer was not properly propagated during Trusted Apps creation from Insights. With these changes, we expect process.Ext.code_signature on Windows to be an array (ESS, ESS Cloud) containing signatures, or a single object (Serverless). On macOS, it will continue to be an object. Please refer to the corresponding GitHub issue for the recordings.
This commit is contained in:
parent
a468965588
commit
b750d46c8b
4 changed files with 357 additions and 28 deletions
|
@ -25,9 +25,13 @@ import type { EndpointMetadataService } from '../../metadata';
|
|||
import { groupEndpointIdsByOS } from '../helpers';
|
||||
import { buildIncompatibleAntivirusWorkflowInsights } from './incompatible_antivirus';
|
||||
|
||||
jest.mock('../helpers', () => ({
|
||||
groupEndpointIdsByOS: jest.fn(),
|
||||
}));
|
||||
jest.mock('../helpers', () => {
|
||||
const actualHelpers = jest.requireActual('../helpers');
|
||||
return {
|
||||
...actualHelpers,
|
||||
groupEndpointIdsByOS: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('buildIncompatibleAntivirusWorkflowInsights', () => {
|
||||
const mockEndpointAppContextService = createMockEndpointAppContext().service;
|
||||
|
@ -154,6 +158,45 @@ describe('buildIncompatibleAntivirusWorkflowInsights', () => {
|
|||
});
|
||||
const params = generateParams('test.com');
|
||||
|
||||
params.esClient.search = jest.fn().mockResolvedValue({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_id: 'lqw5opMB9Ke6SNgnxRSZ',
|
||||
_source: {
|
||||
process: {
|
||||
Ext: {
|
||||
code_signature: [
|
||||
{
|
||||
trusted: true,
|
||||
subject_name: 'test.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await buildIncompatibleAntivirusWorkflowInsights(params);
|
||||
|
||||
expect(result).toEqual([
|
||||
buildExpectedInsight('windows', 'process.Ext.code_signature', 'test.com'),
|
||||
]);
|
||||
expect(groupEndpointIdsByOS).toHaveBeenCalledWith(
|
||||
['endpoint-1'],
|
||||
params.endpointMetadataService
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly build workflow insights for Windows with signerId provided as object', async () => {
|
||||
(groupEndpointIdsByOS as jest.Mock).mockResolvedValue({
|
||||
windows: ['endpoint-1'],
|
||||
});
|
||||
const params = generateParams('test.com');
|
||||
|
||||
params.esClient.search = jest.fn().mockResolvedValue({
|
||||
hits: {
|
||||
hits: [
|
||||
|
@ -185,6 +228,68 @@ describe('buildIncompatibleAntivirusWorkflowInsights', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should fallback to createRemediation without signer field when no valid signatures exist for Windows', async () => {
|
||||
(groupEndpointIdsByOS as jest.Mock).mockResolvedValue({
|
||||
windows: ['endpoint-1'],
|
||||
});
|
||||
|
||||
const params = generateParams('test.com');
|
||||
params.esClient.search = jest.fn().mockResolvedValue({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_id: 'lqw5opMB9Ke6SNgnxRSZ',
|
||||
_source: {
|
||||
process: {
|
||||
Ext: {
|
||||
code_signature: [{ trusted: false, subject_name: 'Untrusted Publisher' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await buildIncompatibleAntivirusWorkflowInsights(params);
|
||||
expect(result).toEqual([buildExpectedInsight('windows')]);
|
||||
});
|
||||
|
||||
it('should skip Microsoft Windows Hardware Compatibility Publisher and use the next trusted signature for Windows', async () => {
|
||||
(groupEndpointIdsByOS as jest.Mock).mockResolvedValue({
|
||||
windows: ['endpoint-1'],
|
||||
});
|
||||
|
||||
const params = generateParams();
|
||||
params.esClient.search = jest.fn().mockResolvedValue({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_id: 'lqw5opMB9Ke6SNgnxRSZ',
|
||||
_source: {
|
||||
process: {
|
||||
Ext: {
|
||||
code_signature: [
|
||||
{
|
||||
trusted: true,
|
||||
subject_name: 'Microsoft Windows Hardware Compatibility Publisher',
|
||||
},
|
||||
{ trusted: true, subject_name: 'Next Trusted Publisher' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await buildIncompatibleAntivirusWorkflowInsights(params);
|
||||
expect(result).toEqual([
|
||||
buildExpectedInsight('windows', 'process.Ext.code_signature', 'Next Trusted Publisher'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should correctly build workflow insights for MacOS with signerId provided', async () => {
|
||||
(groupEndpointIdsByOS as jest.Mock).mockResolvedValue({
|
||||
macos: ['endpoint-1'],
|
||||
|
@ -218,4 +323,27 @@ describe('buildIncompatibleAntivirusWorkflowInsights', () => {
|
|||
params.endpointMetadataService
|
||||
);
|
||||
});
|
||||
|
||||
it('should fallback to createRemediation without signer field for macOS when no code_signature exists', async () => {
|
||||
(groupEndpointIdsByOS as jest.Mock).mockResolvedValue({
|
||||
macos: ['endpoint-1'],
|
||||
});
|
||||
|
||||
const params = generateParams();
|
||||
params.esClient.search = jest.fn().mockResolvedValue({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_id: 'lqw5opMB9Ke6SNgnxRSZ',
|
||||
_source: {
|
||||
process: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await buildIncompatibleAntivirusWorkflowInsights(params);
|
||||
expect(result).toEqual([buildExpectedInsight('macos')]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import { get as _get, uniqBy } from 'lodash';
|
||||
import { uniqBy } from 'lodash';
|
||||
|
||||
import type { DefendInsight } from '@kbn/elastic-assistant-common';
|
||||
|
||||
|
@ -23,22 +23,8 @@ import {
|
|||
SourceType,
|
||||
TargetType,
|
||||
} from '../../../../../common/endpoint/types/workflow_insights';
|
||||
import { groupEndpointIdsByOS } from '../helpers';
|
||||
|
||||
interface FileEventDoc {
|
||||
process: {
|
||||
code_signature?: {
|
||||
subject_name: string;
|
||||
trusted: boolean;
|
||||
};
|
||||
Ext?: {
|
||||
code_signature?: {
|
||||
subject_name: string;
|
||||
trusted: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
import type { FileEventDoc } from '../helpers';
|
||||
import { getValidCodeSignature, groupEndpointIdsByOS } from '../helpers';
|
||||
|
||||
export async function buildIncompatibleAntivirusWorkflowInsights(
|
||||
params: BuildWorkflowInsightParams
|
||||
|
@ -163,14 +149,10 @@ export async function buildIncompatibleAntivirusWorkflowInsights(
|
|||
const codeSignatureSearchHit = codeSignaturesHits.find((hit) => hit._id === id);
|
||||
|
||||
if (codeSignatureSearchHit) {
|
||||
const extPath = os === 'windows' ? '.Ext' : '';
|
||||
const field = `process${extPath}.code_signature`;
|
||||
const value = _get(
|
||||
codeSignatureSearchHit,
|
||||
`_source.${field}.subject_name`,
|
||||
'invalid subject name'
|
||||
);
|
||||
return createRemediation(filePath, os, field, value);
|
||||
const signature = getValidCodeSignature(os, codeSignatureSearchHit._source);
|
||||
if (signature) {
|
||||
return createRemediation(filePath, os, signature.field, signature.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
TargetType,
|
||||
} from '../../../../common/endpoint/types/workflow_insights';
|
||||
import type { EndpointMetadataService } from '../metadata';
|
||||
import type { FileEventDoc } from './helpers';
|
||||
import {
|
||||
buildEsQueryParams,
|
||||
checkIfRemediationExists,
|
||||
|
@ -35,6 +36,7 @@ import {
|
|||
createPipeline,
|
||||
generateInsightId,
|
||||
generateTrustedAppsFilter,
|
||||
getValidCodeSignature,
|
||||
groupEndpointIdsByOS,
|
||||
} from './helpers';
|
||||
import {
|
||||
|
@ -369,6 +371,7 @@ describe('helpers', () => {
|
|||
expect(filter).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkIfRemediationExists', () => {
|
||||
it('should return false for non-incompatible_antivirus types', async () => {
|
||||
const insight = getDefaultInsight({
|
||||
|
@ -421,4 +424,155 @@ describe('helpers', () => {
|
|||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValidCodeSignature', () => {
|
||||
it('should return the first trusted signature for Windows', () => {
|
||||
const os = 'windows';
|
||||
const codeSignatureSearchHit = {
|
||||
process: {
|
||||
Ext: {
|
||||
code_signature: [{ subject_name: 'Valid Cert', trusted: true }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getValidCodeSignature(os, codeSignatureSearchHit);
|
||||
expect(result).toEqual({
|
||||
field: 'process.Ext.code_signature',
|
||||
value: 'Valid Cert',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null if no trusted signatures', () => {
|
||||
const os = 'windows';
|
||||
const codeSignatureSearchHit = {
|
||||
process: {
|
||||
Ext: {
|
||||
code_signature: [{ subject_name: 'Valid Cert', trusted: false }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getValidCodeSignature(os, codeSignatureSearchHit);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null if all Windows code signatures are untrusted', () => {
|
||||
const os = 'windows';
|
||||
const codeSignatureSearchHit = {
|
||||
process: {
|
||||
Ext: {
|
||||
code_signature: [
|
||||
{ subject_name: 'Cert 1', trusted: false },
|
||||
{ subject_name: 'Cert 2', trusted: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = getValidCodeSignature(os, codeSignatureSearchHit);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should correctly process a single object code signature for Windows', () => {
|
||||
const os = 'windows';
|
||||
const codeSignatureSearchHit = {
|
||||
process: {
|
||||
Ext: {
|
||||
code_signature: { subject_name: 'Valid Cert', trusted: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getValidCodeSignature(os, codeSignatureSearchHit);
|
||||
expect(result).toEqual({
|
||||
field: 'process.Ext.code_signature',
|
||||
value: 'Valid Cert',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the first trusted signature for Windows, skipping Microsoft Windows Hardware Compatibility Publisher', () => {
|
||||
const os = 'windows';
|
||||
const codeSignatureSearchHit = {
|
||||
process: {
|
||||
Ext: {
|
||||
code_signature: [
|
||||
{ subject_name: 'Microsoft Windows Hardware Compatibility Publisher', trusted: true },
|
||||
{ subject_name: 'Valid Cert', trusted: false },
|
||||
{ subject_name: 'Valid Cert2', trusted: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getValidCodeSignature(os, codeSignatureSearchHit);
|
||||
expect(result).toEqual({
|
||||
field: 'process.Ext.code_signature',
|
||||
value: 'Valid Cert2',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Windows publisher if this is the only signer', () => {
|
||||
const os = 'windows';
|
||||
const codeSignatureSearchHit = {
|
||||
process: {
|
||||
Ext: {
|
||||
code_signature: [
|
||||
{ subject_name: 'Microsoft Windows Hardware Compatibility Publisher', trusted: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = getValidCodeSignature(os, codeSignatureSearchHit);
|
||||
expect(result).toEqual({
|
||||
field: 'process.Ext.code_signature',
|
||||
value: 'Microsoft Windows Hardware Compatibility Publisher',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the subject name for macOS when code signature is present', () => {
|
||||
const os = 'macos';
|
||||
const codeSignatureSearchHit = {
|
||||
process: {
|
||||
code_signature: { subject_name: 'Apple Inc.', trusted: true },
|
||||
},
|
||||
};
|
||||
|
||||
const result = getValidCodeSignature(os, codeSignatureSearchHit);
|
||||
expect(result).toEqual({ field: 'process.code_signature', value: 'Apple Inc.' });
|
||||
});
|
||||
|
||||
it('should return null if no code signature is present for macOS', () => {
|
||||
const os = 'macos';
|
||||
const codeSignatureSearchHit = {
|
||||
process: {},
|
||||
};
|
||||
|
||||
const result = getValidCodeSignature(os, codeSignatureSearchHit);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null if code_signature field is empty for macOS', () => {
|
||||
const os = 'macos';
|
||||
const codeSignatureSearchHit = {
|
||||
process: {
|
||||
code_signature: {},
|
||||
},
|
||||
} as FileEventDoc;
|
||||
|
||||
const result = getValidCodeSignature(os, codeSignatureSearchHit);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for non-Windows when code signature is untrusted', () => {
|
||||
const os = 'macos';
|
||||
const codeSignatureSearchHit = {
|
||||
process: {
|
||||
code_signature: { subject_name: 'Apple Inc.', trusted: false },
|
||||
},
|
||||
};
|
||||
const result = getValidCodeSignature(os, codeSignatureSearchHit);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,6 +31,23 @@ import {
|
|||
} from './constants';
|
||||
import { securityWorkflowInsightsFieldMap } from './field_map_configurations';
|
||||
|
||||
export interface FileEventDoc {
|
||||
process: {
|
||||
code_signature?: {
|
||||
subject_name: string;
|
||||
trusted: boolean;
|
||||
};
|
||||
Ext?: {
|
||||
code_signature?:
|
||||
| Array<{
|
||||
subject_name: string;
|
||||
trusted: boolean;
|
||||
}>
|
||||
| { subject_name: string; trusted: boolean };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function createDatastream(kibanaVersion: string): DataStreamSpacesAdapter {
|
||||
const ds = new DataStreamSpacesAdapter(DATA_STREAM_PREFIX, {
|
||||
kibanaVersion,
|
||||
|
@ -217,3 +234,51 @@ export const checkIfRemediationExists = async ({
|
|||
|
||||
return !!response?.total && response.total > 0;
|
||||
};
|
||||
|
||||
export function getValidCodeSignature(
|
||||
os: string,
|
||||
hit: FileEventDoc | undefined
|
||||
): { field: string; value: string } | null {
|
||||
const WINDOWS_PUBLISHER = 'Microsoft Windows Hardware Compatibility Publisher';
|
||||
|
||||
if (os !== 'windows') {
|
||||
const codeSignature = hit?.process?.code_signature;
|
||||
if (codeSignature?.trusted) {
|
||||
return {
|
||||
field: 'process.code_signature',
|
||||
value: codeSignature.subject_name,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Windows specific code signature
|
||||
const rawSignature = hit?.process?.Ext?.code_signature;
|
||||
if (!rawSignature) return null;
|
||||
|
||||
// In serverless environment, a single item array is flattened to an object.
|
||||
const codeSignatures = Array.isArray(rawSignature) ? rawSignature : [rawSignature];
|
||||
|
||||
// If there's a single trusted signature from Windows publisher, return it
|
||||
if (
|
||||
codeSignatures.length === 1 &&
|
||||
codeSignatures[0].trusted &&
|
||||
codeSignatures[0].subject_name === WINDOWS_PUBLISHER
|
||||
) {
|
||||
return {
|
||||
field: 'process.Ext.code_signature',
|
||||
value: codeSignatures[0].subject_name,
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, return the first trusted signature that is not from the Windows publisher
|
||||
for (const codeSignature of codeSignatures) {
|
||||
if (codeSignature.trusted && codeSignature.subject_name !== WINDOWS_PUBLISHER) {
|
||||
return {
|
||||
field: 'process.Ext.code_signature',
|
||||
value: codeSignature.subject_name,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue