[Defend Workflows][Trusted Apps Advanced Mode][API] Updates the validator to use the advanced mode schema (#221980)

## Summary
- [x] Modifies the trusted apps validator to use an advanced mode
"free-for-all" schema identical to what is allowed in the event filters
validator if there is `form_mode:advanced` present in the exception
item's `tags` field.
- [x] Hides the api behind the feature flag: `trustedAppsAdvancedMode`
- [x] Adds api functional tests

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Candace Park 2025-06-05 19:01:08 -04:00 committed by GitHub
parent 12e71e8bab
commit 8f76a4f706
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 224 additions and 10 deletions

View file

@ -263,6 +263,11 @@ export const allowedExperimentalValues = Object.freeze({
* Automatically installs the security AI prompts package
*/
securityAIPromptsEnabled: false,
/**
* Enables advanced mode for Trusted Apps creation and update
*/
trustedAppsAdvancedMode: false,
});
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;

View file

@ -187,6 +187,26 @@ const TrustedAppDataSchema = schema.object(
{ unknowns: 'ignore' }
);
/**
* Schema to validate Trusted Apps in Advanced mode
*/
const TrustedAppAdvancedModeDataSchema = schema.object(
{
entries: schema.arrayOf(
schema.object(
{
field: schema.string(),
},
{ unknowns: 'ignore' }
),
{ minSize: 1 }
),
},
{
unknowns: 'ignore',
}
);
export class TrustedAppValidator extends BaseValidator {
static isTrustedApp(item: { listId: string }): boolean {
return item.listId === ENDPOINT_ARTIFACT_LISTS.trustedApps.id;
@ -270,7 +290,16 @@ export class TrustedAppValidator extends BaseValidator {
await this.validateBasicData(item);
try {
TrustedAppDataSchema.validate(item, { os: item.osTypes[0] });
const isTAAdvancedModeFeatureFlagEnabled =
this.endpointAppContext.experimentalFeatures.trustedAppsAdvancedMode;
const isAdvancedMode = item.tags.includes('form_mode:advanced');
if (!isTAAdvancedModeFeatureFlagEnabled && isAdvancedMode) {
throw new Error('Trusted apps advanced mode feature is not enabled');
} else if (isTAAdvancedModeFeatureFlagEnabled && isAdvancedMode) {
TrustedAppAdvancedModeDataSchema.validate(item);
} else {
TrustedAppDataSchema.validate(item, { os: item.osTypes[0] });
}
} catch (error) {
throw new EndpointArtifactExceptionValidationError(error.message);
}

View file

@ -6,11 +6,15 @@
*/
import { FtrConfigProviderContext } from '@kbn/test';
import type { ExperimentalFeatures as SecuritySolutionExperimentalFeatures } from '@kbn/security-solution-plugin/common';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const functionalConfig = await readConfigFile(
require.resolve('../../../../../config/ess/config.base.edr_workflows.trial')
);
const securitySolutionEnableExperimental: Array<keyof SecuritySolutionExperimentalFeatures> = [
'trustedAppsAdvancedMode',
];
return {
...functionalConfig.getAll(),
@ -18,5 +22,19 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
junit: {
reportName: 'EDR Workflows - Artifacts Integration Tests - ESS Env - Trial License',
},
kbnTestServer: {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs').filter(
// Exclude Security solution experimental features
// properties since we are overriding them here
(arg: string) => !arg.includes('xpack.securitySolution.enableExperimental')
),
// SECURITY SOLUTION: set any experimental feature flags for testing
`--xpack.securitySolution.enableExperimental=${JSON.stringify(
securitySolutionEnableExperimental
)}`,
],
},
};
}

View file

@ -6,11 +6,15 @@
*/
import { FtrConfigProviderContext } from '@kbn/test';
import type { ExperimentalFeatures as SecuritySolutionExperimentalFeatures } from '@kbn/security-solution-plugin/common';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const functionalConfig = await readConfigFile(
require.resolve('../../../../../config/serverless/config.base.edr_workflows')
);
const securitySolutionEnableExperimental: Array<keyof SecuritySolutionExperimentalFeatures> = [
'trustedAppsAdvancedMode',
];
return {
...functionalConfig.getAll(),
@ -18,5 +22,19 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
junit: {
reportName: 'EDR Workflows - Artifacts Integration Tests - Serverless Env - Complete',
},
kbnTestServer: {
...functionalConfig.get('kbnTestServer'),
serverArgs: [
...functionalConfig.get('kbnTestServer.serverArgs').filter(
// Exclude Security solution experimental features
// properties since we are overriding them here
(arg: string) => !arg.includes('xpack.securitySolution.enableExperimental')
),
// SECURITY SOLUTION: set any experimental feature flags for testing
`--xpack.securitySolution.enableExperimental=${JSON.stringify(
securitySolutionEnableExperimental
)}`,
],
},
};
}

View file

@ -241,28 +241,35 @@ export default function ({ getService }: FtrProviderContext) {
});
it('should not error if signer is set for a windows os entry item', async () => {
const body = trustedAppApiCalls[0].getBody();
const body = trustedAppApiCall.getBody();
// Match request version with artifact version
if ('_version' in body) {
body._version = trustedAppData.artifact._version;
}
return body;
body.os_types = ['windows'];
body.entries = exceptionsGenerator.generateTrustedAppSignerEntry();
await endpointPolicyManagerSupertest[trustedAppApiCalls[0].method](
trustedAppApiCalls[0].path
)
await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path)
.set('kbn-xsrf', 'true')
.send(body)
.expect(200);
});
it('should not error if signer is set for a mac os entry item', async () => {
const body = trustedAppApiCalls[0].getBody();
const body = trustedAppApiCall.getBody();
// Match request version with artifact version
if ('_version' in body) {
body._version = trustedAppData.artifact._version;
}
return body;
body.os_types = ['macos'];
body.entries = exceptionsGenerator.generateTrustedAppSignerEntry('mac');
await endpointPolicyManagerSupertest[trustedAppApiCalls[0].method](
trustedAppApiCalls[0].path
)
await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path)
.set('kbn-xsrf', 'true')
.send(body)
.expect(200);
@ -294,6 +301,143 @@ export default function ({ getService }: FtrProviderContext) {
.expect(anEndpointArtifactError)
.expect(anErrorMessageWith(/invalid policy ids/));
});
describe('when in advanced form mode', () => {
const getAdvancedModeBody = () => {
const body = trustedAppApiCall.getBody();
body.tags.push('form_mode:advanced');
// Match request version with artifact version
if ('_version' in body) {
body._version = trustedAppData.artifact._version;
}
return body;
};
it(`should NOT error on [${trustedAppApiCall.method}] if invalid condition entry fields are used`, async () => {
const body = getAdvancedModeBody();
body.entries[0].field = 'some.invalid.field';
await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path)
.set('kbn-xsrf', 'true')
.send(body)
.expect(200);
});
it(`should NOT error on [${trustedAppApiCall.method}] if a condition entry field is used more than once`, async () => {
const body = getAdvancedModeBody();
body.entries.push({ ...body.entries[0] });
await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path)
.set('kbn-xsrf', 'true')
.send(body)
.expect(200);
});
it(`should NOT error on [${trustedAppApiCall.method}] if an invalid hash is used`, async () => {
const body = getAdvancedModeBody();
body.entries = [
{
field: 'process.hash.md5',
operator: 'included',
type: 'match',
value: '1',
},
];
await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path)
.set('kbn-xsrf', 'true')
.send(body)
.expect(200);
});
it(`should NOT error on [${trustedAppApiCall.method}] if signer is set for a non windows os entry item`, async () => {
const body = getAdvancedModeBody();
body.os_types = ['linux'];
body.entries = exceptionsGenerator.generateTrustedAppSignerEntry();
await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path)
.set('kbn-xsrf', 'true')
.send(body)
.expect(200);
});
it(`should NOT error on [${trustedAppApiCall.method} if Mac signer field is used for Windows entry`, async () => {
const body = getAdvancedModeBody();
body.os_types = ['windows'];
body.entries = exceptionsGenerator.generateTrustedAppSignerEntry('mac');
await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path)
.set('kbn-xsrf', 'true')
.send(body)
.expect(200);
});
it(`should NOT error on [${trustedAppApiCall.method} if Windows signer field is used for Mac entry`, async () => {
const body = getAdvancedModeBody();
body.os_types = ['macos'];
body.entries = exceptionsGenerator.generateTrustedAppSignerEntry();
await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path)
.set('kbn-xsrf', 'true')
.send(body)
.expect(200);
});
it('should not error if signer is set for a windows os entry item', async () => {
const body = getAdvancedModeBody();
body.os_types = ['windows'];
body.entries = exceptionsGenerator.generateTrustedAppSignerEntry();
await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path)
.set('kbn-xsrf', 'true')
.send(body)
.expect(200);
});
it('should not error if signer is set for a mac os entry item', async () => {
const body = getAdvancedModeBody();
body.os_types = ['macos'];
body.entries = exceptionsGenerator.generateTrustedAppSignerEntry('mac');
await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path)
.set('kbn-xsrf', 'true')
.send(body)
.expect(200);
});
it(`should error on [${trustedAppApiCall.method}] if more than one OS is set`, async () => {
const body = getAdvancedModeBody();
body.os_types = ['linux', 'windows'];
await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path)
.set('kbn-xsrf', 'true')
.send(body)
.expect(400)
.expect(anEndpointArtifactError)
.expect(anErrorMessageWith(/\[osTypes\]: array size is \[2\]/));
});
it(`should error on [${trustedAppApiCall.method}] if policy id is invalid`, async () => {
const body = getAdvancedModeBody();
body.tags = [`${BY_POLICY_ARTIFACT_TAG_PREFIX}123`];
// Using superuser here as we need custom license for this action
await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path)
.set('kbn-xsrf', 'true')
.send(body)
.expect(400)
.expect(anEndpointArtifactError)
.expect(anErrorMessageWith(/invalid policy ids/));
});
});
}
for (const trustedAppApiCall of [...needsWritePrivilege, ...needsReadPrivilege]) {
it(`should not error on [${trustedAppApiCall.method}] - [${trustedAppApiCall.info}]`, async () => {