[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:
Konrad Szwarc 2025-02-07 09:26:10 +01:00 committed by GitHub
parent a468965588
commit b750d46c8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 357 additions and 28 deletions

View file

@ -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')]);
});
});

View file

@ -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);
}
}
}

View file

@ -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();
});
});
});

View file

@ -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;
}