diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx
index 793f0e6c2a420b..e0fb4e554ee3c4 100644
--- a/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx
+++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx
@@ -80,7 +80,6 @@ export const ConfirmDisableUsers: FunctionComponent =
}
confirmButtonColor={isSystemUser ? 'danger' : undefined}
isLoading={state.loading}
- ownFocus
>
{isSystemUser ? (
diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx
index a1aac5bc0a8cb4..2cb4cf8b4a9e2c 100644
--- a/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx
+++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx
@@ -67,7 +67,6 @@ export const ConfirmEnableUsers: FunctionComponent = ({
}
)}
isLoading={state.loading}
- ownFocus
>
diff --git a/x-pack/plugins/security/server/audit/audit_service.test.ts b/x-pack/plugins/security/server/audit/audit_service.test.ts
index 3b07d766d7cb46..f59fd6ecdec919 100644
--- a/x-pack/plugins/security/server/audit/audit_service.test.ts
+++ b/x-pack/plugins/security/server/audit/audit_service.test.ts
@@ -76,9 +76,9 @@ describe('#setup', () => {
config: {
enabled: true,
appender: {
- kind: 'console',
+ type: 'console',
layout: {
- kind: 'pattern',
+ type: 'pattern',
},
},
},
@@ -102,9 +102,9 @@ describe('#setup', () => {
config: {
enabled: true,
appender: {
- kind: 'console',
+ type: 'console',
layout: {
- kind: 'pattern',
+ type: 'pattern',
},
},
},
@@ -251,9 +251,9 @@ describe('#createLoggingConfig', () => {
createLoggingConfig({
enabled: true,
appender: {
- kind: 'console',
+ type: 'console',
layout: {
- kind: 'pattern',
+ type: 'pattern',
},
},
})
@@ -264,10 +264,10 @@ describe('#createLoggingConfig', () => {
Object {
"appenders": Object {
"auditTrailAppender": Object {
- "kind": "console",
"layout": Object {
- "kind": "pattern",
+ "type": "pattern",
},
+ "type": "console",
},
},
"loggers": Array [
@@ -275,8 +275,8 @@ describe('#createLoggingConfig', () => {
"appenders": Array [
"auditTrailAppender",
],
- "context": "audit.ecs",
"level": "info",
+ "name": "audit.ecs",
},
],
}
@@ -293,9 +293,9 @@ describe('#createLoggingConfig', () => {
createLoggingConfig({
enabled: false,
appender: {
- kind: 'console',
+ type: 'console',
layout: {
- kind: 'pattern',
+ type: 'pattern',
},
},
})
@@ -331,9 +331,9 @@ describe('#createLoggingConfig', () => {
createLoggingConfig({
enabled: true,
appender: {
- kind: 'console',
+ type: 'console',
layout: {
- kind: 'pattern',
+ type: 'pattern',
},
},
})
diff --git a/x-pack/plugins/security/server/audit/audit_service.ts b/x-pack/plugins/security/server/audit/audit_service.ts
index 42e36e50d6d42d..99dd2c82ec9fe5 100644
--- a/x-pack/plugins/security/server/audit/audit_service.ts
+++ b/x-pack/plugins/security/server/audit/audit_service.ts
@@ -224,16 +224,16 @@ export const createLoggingConfig = (config: ConfigType['audit']) =>
map, LoggerContextConfigInput>((features) => ({
appenders: {
auditTrailAppender: config.appender ?? {
- kind: 'console',
+ type: 'console',
layout: {
- kind: 'pattern',
+ type: 'pattern',
highlight: true,
},
},
},
loggers: [
{
- context: 'audit.ecs',
+ name: 'audit.ecs',
level: config.enabled && config.appender && features.allowAuditLogging ? 'info' : 'off',
appenders: ['auditTrailAppender'],
},
diff --git a/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap b/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap
index 76d284a21984e9..04190fbf5eacdd 100644
--- a/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap
+++ b/x-pack/plugins/security/server/authorization/__snapshots__/validate_es_response.test.ts.snap
@@ -1,12 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`validateEsPrivilegeResponse fails validation when an action is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: [action3]: expected value of type [boolean] but got [string]"`;
+exports[`validateEsPrivilegeResponse fails validation when an action is malformed in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: expected value of type [boolean] but got [string]"`;
-exports[`validateEsPrivilegeResponse fails validation when an action is missing in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: [action2]: expected value of type [boolean] but got [undefined]"`;
+exports[`validateEsPrivilegeResponse fails validation when an action is missing in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected actions"`;
exports[`validateEsPrivilegeResponse fails validation when an expected resource property is missing from the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected resources"`;
-exports[`validateEsPrivilegeResponse fails validation when an extra action is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: [action4]: definition for this key is missing"`;
+exports[`validateEsPrivilegeResponse fails validation when an extra action is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.foo-application]: Payload did not match expected actions"`;
exports[`validateEsPrivilegeResponse fails validation when an extra application is present in the response 1`] = `"Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.otherApplication]: definition for this key is missing"`;
diff --git a/x-pack/plugins/security/server/authorization/check_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_privileges.test.ts
index 93f5efed58fb8d..5bca46f22a5123 100644
--- a/x-pack/plugins/security/server/authorization/check_privileges.test.ts
+++ b/x-pack/plugins/security/server/authorization/check_privileges.test.ts
@@ -316,7 +316,7 @@ describe('#atSpace', () => {
},
});
expect(result).toMatchInlineSnapshot(
- `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [saved_object:bar-type/get]: definition for this key is missing]`
+ `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]`
);
});
@@ -338,7 +338,7 @@ describe('#atSpace', () => {
},
});
expect(result).toMatchInlineSnapshot(
- `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [saved_object:foo-type/get]: expected value of type [boolean] but got [undefined]]`
+ `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]`
);
});
});
@@ -1092,7 +1092,7 @@ describe('#atSpaces', () => {
},
});
expect(result).toMatchInlineSnapshot(
- `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [mock-action:version]: expected value of type [boolean] but got [undefined]]`
+ `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]`
);
});
@@ -2266,7 +2266,7 @@ describe('#globally', () => {
},
});
expect(result).toMatchInlineSnapshot(
- `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [mock-action:version]: expected value of type [boolean] but got [undefined]]`
+ `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]`
);
});
@@ -2384,7 +2384,7 @@ describe('#globally', () => {
},
});
expect(result).toMatchInlineSnapshot(
- `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [saved_object:bar-type/get]: definition for this key is missing]`
+ `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]`
);
});
@@ -2405,7 +2405,7 @@ describe('#globally', () => {
},
});
expect(result).toMatchInlineSnapshot(
- `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: [saved_object:foo-type/get]: expected value of type [boolean] but got [undefined]]`
+ `[Error: Invalid response received from Elasticsearch has_privilege endpoint. Error: [application.kibana-our_application]: Payload did not match expected actions]`
);
});
});
diff --git a/x-pack/plugins/security/server/authorization/validate_es_response.ts b/x-pack/plugins/security/server/authorization/validate_es_response.ts
index 19afaaf035c15e..270ff26716e3f2 100644
--- a/x-pack/plugins/security/server/authorization/validate_es_response.ts
+++ b/x-pack/plugins/security/server/authorization/validate_es_response.ts
@@ -8,6 +8,11 @@
import { schema } from '@kbn/config-schema';
import { HasPrivilegesResponse } from './types';
+/**
+ * Validates an Elasticsearch "Has privileges" response against the expected application, actions, and resources.
+ *
+ * Note: the `actions` and `resources` parameters must be unique string arrays; any duplicates will cause validation to fail.
+ */
export function validateEsPrivilegeResponse(
response: HasPrivilegesResponse,
application: string,
@@ -24,21 +29,29 @@ export function validateEsPrivilegeResponse(
return response;
}
-function buildActionsValidationSchema(actions: string[]) {
- return schema.object({
- ...actions.reduce>((acc, action) => {
- return {
- ...acc,
- [action]: schema.boolean(),
- };
- }, {}),
- });
-}
-
function buildValidationSchema(application: string, actions: string[], resources: string[]) {
- const actionValidationSchema = buildActionsValidationSchema(actions);
+ const actionValidationSchema = schema.boolean();
+ const actionsValidationSchema = schema.object(
+ {},
+ {
+ unknowns: 'allow',
+ validate: (value) => {
+ const actualActions = Object.keys(value).sort();
+ if (
+ actions.length !== actualActions.length ||
+ ![...actions].sort().every((x, i) => x === actualActions[i])
+ ) {
+ throw new Error('Payload did not match expected actions');
+ }
+
+ Object.values(value).forEach((actionResult) => {
+ actionValidationSchema.validate(actionResult);
+ });
+ },
+ }
+ );
- const resourceValidationSchema = schema.object(
+ const resourcesValidationSchema = schema.object(
{},
{
unknowns: 'allow',
@@ -46,13 +59,13 @@ function buildValidationSchema(application: string, actions: string[], resources
const actualResources = Object.keys(value).sort();
if (
resources.length !== actualResources.length ||
- !resources.sort().every((x, i) => x === actualResources[i])
+ ![...resources].sort().every((x, i) => x === actualResources[i])
) {
throw new Error('Payload did not match expected resources');
}
Object.values(value).forEach((actionResult) => {
- actionValidationSchema.validate(actionResult);
+ actionsValidationSchema.validate(actionResult);
});
},
}
@@ -63,7 +76,7 @@ function buildValidationSchema(application: string, actions: string[], resources
has_all_requested: schema.boolean(),
cluster: schema.object({}, { unknowns: 'allow' }),
application: schema.object({
- [application]: resourceValidationSchema,
+ [application]: resourcesValidationSchema,
}),
index: schema.object({}, { unknowns: 'allow' }),
});
diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts
index d4dcca8bebb0c1..53e4152b3c8fbf 100644
--- a/x-pack/plugins/security/server/config.test.ts
+++ b/x-pack/plugins/security/server/config.test.ts
@@ -1558,21 +1558,21 @@ describe('createConfig()', () => {
ConfigSchema.validate({
audit: {
appender: {
- kind: 'file',
- path: '/path/to/file.txt',
+ type: 'file',
+ fileName: '/path/to/file.txt',
layout: {
- kind: 'json',
+ type: 'json',
},
},
},
}).audit.appender
).toMatchInlineSnapshot(`
Object {
- "kind": "file",
+ "fileName": "/path/to/file.txt",
"layout": Object {
- "kind": "json",
+ "type": "json",
},
- "path": "/path/to/file.txt",
+ "type": "file",
}
`);
});
@@ -1583,12 +1583,12 @@ describe('createConfig()', () => {
audit: {
// no layout configured
appender: {
- kind: 'file',
+ type: 'file',
path: '/path/to/file.txt',
},
},
})
- ).toThrow('[audit.appender.2.kind]: expected value to equal [legacy-appender]');
+ ).toThrow('[audit.appender.2.type]: expected value to equal [legacy-appender]');
});
it('rejects an ignore_filter when no appender is configured', () => {
diff --git a/x-pack/plugins/security/server/config_deprecations.test.ts b/x-pack/plugins/security/server/config_deprecations.test.ts
index bdb02d8ed99750..c4c7f399e7b5d3 100644
--- a/x-pack/plugins/security/server/config_deprecations.test.ts
+++ b/x-pack/plugins/security/server/config_deprecations.test.ts
@@ -52,6 +52,117 @@ describe('Config Deprecations', () => {
`);
});
+ it('renames audit.appender.kind to audit.appender.type', () => {
+ const config = {
+ xpack: {
+ security: {
+ audit: {
+ appender: {
+ kind: 'console',
+ },
+ },
+ },
+ },
+ };
+ const { messages, migrated } = applyConfigDeprecations(cloneDeep(config));
+ expect(migrated.xpack.security.audit.appender.kind).not.toBeDefined();
+ expect(migrated.xpack.security.audit.appender.type).toEqual('console');
+ expect(messages).toMatchInlineSnapshot(`
+ Array [
+ "\\"xpack.security.audit.appender.kind\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.type\\"",
+ ]
+ `);
+ });
+
+ it('renames audit.appender.layout.kind to audit.appender.layout.type', () => {
+ const config = {
+ xpack: {
+ security: {
+ audit: {
+ appender: {
+ layout: { kind: 'pattern' },
+ },
+ },
+ },
+ },
+ };
+ const { messages, migrated } = applyConfigDeprecations(cloneDeep(config));
+ expect(migrated.xpack.security.audit.appender.layout.kind).not.toBeDefined();
+ expect(migrated.xpack.security.audit.appender.layout.type).toEqual('pattern');
+ expect(messages).toMatchInlineSnapshot(`
+ Array [
+ "\\"xpack.security.audit.appender.layout.kind\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.layout.type\\"",
+ ]
+ `);
+ });
+
+ it('renames audit.appender.policy.kind to audit.appender.policy.type', () => {
+ const config = {
+ xpack: {
+ security: {
+ audit: {
+ appender: {
+ policy: { kind: 'time-interval' },
+ },
+ },
+ },
+ },
+ };
+ const { messages, migrated } = applyConfigDeprecations(cloneDeep(config));
+ expect(migrated.xpack.security.audit.appender.policy.kind).not.toBeDefined();
+ expect(migrated.xpack.security.audit.appender.policy.type).toEqual('time-interval');
+ expect(messages).toMatchInlineSnapshot(`
+ Array [
+ "\\"xpack.security.audit.appender.policy.kind\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.policy.type\\"",
+ ]
+ `);
+ });
+
+ it('renames audit.appender.strategy.kind to audit.appender.strategy.type', () => {
+ const config = {
+ xpack: {
+ security: {
+ audit: {
+ appender: {
+ strategy: { kind: 'numeric' },
+ },
+ },
+ },
+ },
+ };
+ const { messages, migrated } = applyConfigDeprecations(cloneDeep(config));
+ expect(migrated.xpack.security.audit.appender.strategy.kind).not.toBeDefined();
+ expect(migrated.xpack.security.audit.appender.strategy.type).toEqual('numeric');
+ expect(messages).toMatchInlineSnapshot(`
+ Array [
+ "\\"xpack.security.audit.appender.strategy.kind\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.strategy.type\\"",
+ ]
+ `);
+ });
+
+ it('renames audit.appender.path to audit.appender.fileName', () => {
+ const config = {
+ xpack: {
+ security: {
+ audit: {
+ appender: {
+ type: 'file',
+ path: './audit.log',
+ },
+ },
+ },
+ },
+ };
+ const { messages, migrated } = applyConfigDeprecations(cloneDeep(config));
+ expect(migrated.xpack.security.audit.appender.path).not.toBeDefined();
+ expect(migrated.xpack.security.audit.appender.fileName).toEqual('./audit.log');
+ expect(messages).toMatchInlineSnapshot(`
+ Array [
+ "\\"xpack.security.audit.appender.path\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.fileName\\"",
+ ]
+ `);
+ });
+
it(`warns that 'authorization.legacyFallback.enabled' is unused`, () => {
const config = {
xpack: {
diff --git a/x-pack/plugins/security/server/config_deprecations.ts b/x-pack/plugins/security/server/config_deprecations.ts
index 65d18f0a4e7eb3..a7bb5e09fb919d 100644
--- a/x-pack/plugins/security/server/config_deprecations.ts
+++ b/x-pack/plugins/security/server/config_deprecations.ts
@@ -12,6 +12,13 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({
unused,
}) => [
rename('sessionTimeout', 'session.idleTimeout'),
+
+ rename('audit.appender.kind', 'audit.appender.type'),
+ rename('audit.appender.layout.kind', 'audit.appender.layout.type'),
+ rename('audit.appender.policy.kind', 'audit.appender.policy.type'),
+ rename('audit.appender.strategy.kind', 'audit.appender.strategy.type'),
+ rename('audit.appender.path', 'audit.appender.fileName'),
+
unused('authorization.legacyFallback.enabled'),
unused('authc.saml.maxRedirectURLSize'),
// Deprecation warning for the old array-based format of `xpack.security.authc.providers`.
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts
index 5d1f7572672990..aade8be4f503fb 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts
@@ -325,7 +325,7 @@ export const job_status = t.keyof({
succeeded: null,
failed: null,
'going to run': null,
- 'partial failure': null,
+ warning: null,
});
export type JobStatus = t.TypeOf;
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts
index b76a762ca6cbf0..981a5422a05949 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts
@@ -55,6 +55,7 @@ import {
threat_filters,
threat_mapping,
threat_language,
+ threat_indicator_path,
} from '../types/threat_mapping';
import {
@@ -133,6 +134,7 @@ export const addPrepackagedRulesSchema = t.intersection([
threat_query, // defaults to "undefined" if not set during decode
threat_index, // defaults to "undefined" if not set during decode
threat_language, // defaults "undefined" if not set during decode
+ threat_indicator_path, // defaults "undefined" if not set during decode
concurrent_searches, // defaults to "undefined" if not set during decode
items_per_search, // defaults to "undefined" if not set during decode
})
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts
index 0a7b8b120ba7ef..8fa5809abe68b4 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts
@@ -62,6 +62,7 @@ import {
threat_filters,
threat_mapping,
threat_language,
+ threat_indicator_path,
} from '../types/threat_mapping';
import {
@@ -152,6 +153,7 @@ export const importRulesSchema = t.intersection([
threat_query, // defaults to "undefined" if not set during decode
threat_index, // defaults to "undefined" if not set during decode
threat_language, // defaults "undefined" if not set during decode
+ threat_indicator_path, // defaults to "undefined" if not set during decode
concurrent_searches, // defaults to "undefined" if not set during decode
items_per_search, // defaults to "undefined" if not set during decode
})
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts
index 9d5331aeab8e4e..920fbaf4915c5c 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts
@@ -57,6 +57,7 @@ import {
threat_filters,
threat_mapping,
threat_language,
+ threat_indicator_path,
} from '../types/threat_mapping';
import { listArrayOrUndefined } from '../types/lists';
@@ -112,6 +113,7 @@ export const patchRulesSchema = t.exact(
threat_filters,
threat_mapping,
threat_language,
+ threat_indicator_path,
concurrent_searches,
items_per_search,
})
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts
index 87e5acb5428df7..fb29e37a53fdbe 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts
@@ -56,6 +56,7 @@ export const getCreateThreatMatchRulesSchemaMock = (
rule_id: ruleId,
threat_query: '*:*',
threat_index: ['list-index'],
+ threat_indicator_path: 'threat.indicator',
threat_mapping: [
{
entries: [
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts
index 14b47c8b2b3280..6b8211b23088ca 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts
@@ -1152,7 +1152,7 @@ describe('create rules schema', () => {
});
});
- describe('threat_mapping', () => {
+ describe('threat_match', () => {
test('You can set a threat query, index, mapping, filters when creating a rule', () => {
const payload = getCreateThreatMatchRulesSchemaMock();
const decoded = createRulesSchema.decode(payload);
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts
index 1c9ebe00333157..5cf2b6242b2f89 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts
@@ -13,6 +13,7 @@ import {
threat_query,
threat_mapping,
threat_index,
+ threat_indicator_path,
concurrent_searches,
items_per_search,
} from '../types/threat_mapping';
@@ -213,6 +214,7 @@ const threatMatchRuleParams = {
filters,
saved_id,
threat_filters,
+ threat_indicator_path,
threat_language: t.keyof({ kuery: null, lucene: null }),
concurrent_searches,
items_per_search,
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts
index b14c646e862d36..cf07389e207b34 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts
@@ -150,6 +150,7 @@ export const getThreatMatchingSchemaPartialMock = (enabled = false): Partial {
expect(fields).toEqual(expected);
});
- test('should return 8 fields for a rule of type "threat_match"', () => {
+ test('should return nine (9) fields for a rule of type "threat_match"', () => {
const fields = addThreatMatchFields({ type: 'threat_match' });
- expect(fields.length).toEqual(8);
+ expect(fields.length).toEqual(9);
});
});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts
index bcdb0fa9b085d6..6bd54973e064f1 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts
@@ -70,6 +70,7 @@ import {
threat_filters,
threat_mapping,
threat_language,
+ threat_indicator_path,
} from '../types/threat_mapping';
import { DefaultListArray } from '../types/lists_default_array';
@@ -151,6 +152,7 @@ export const dependentRulesSchema = t.partial({
items_per_search,
threat_mapping,
threat_language,
+ threat_indicator_path,
});
/**
@@ -286,6 +288,9 @@ export const addThreatMatchFields = (typeAndTimelineOnly: TypeAndTimelineOnly):
t.exact(t.type({ threat_mapping: dependentRulesSchema.props.threat_mapping })),
t.exact(t.partial({ threat_language: dependentRulesSchema.props.threat_language })),
t.exact(t.partial({ threat_filters: dependentRulesSchema.props.threat_filters })),
+ t.exact(
+ t.partial({ threat_indicator_path: dependentRulesSchema.props.threat_indicator_path })
+ ),
t.exact(t.partial({ saved_id: dependentRulesSchema.props.saved_id })),
t.exact(t.partial({ concurrent_searches: dependentRulesSchema.props.concurrent_searches })),
t.exact(
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts
index d3975df488de9e..aab06941686c26 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts
@@ -18,6 +18,11 @@ export type ThreatQuery = t.TypeOf;
export const threatQueryOrUndefined = t.union([threat_query, t.undefined]);
export type ThreatQueryOrUndefined = t.TypeOf;
+export const threat_indicator_path = t.string;
+export type ThreatIndicatorPath = t.TypeOf;
+export const threatIndicatorPathOrUndefined = t.union([threat_indicator_path, t.undefined]);
+export type ThreatIndicatorPathOrUndefined = t.TypeOf;
+
export const threat_filters = t.array(t.unknown); // Filters are not easily type-able yet
export type ThreatFilters = t.TypeOf;
export const threatFiltersOrUndefined = t.union([threat_filters, t.undefined]);
diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts
index 080b704e9c193b..725a2eb9fea7bb 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts
@@ -30,4 +30,5 @@ export const isEqlRule = (ruleType: Type | undefined): boolean => ruleType === '
export const isThresholdRule = (ruleType: Type | undefined): boolean => ruleType === 'threshold';
export const isQueryRule = (ruleType: Type | undefined): boolean =>
ruleType === 'query' || ruleType === 'saved_query';
-export const isThreatMatchRule = (ruleType: Type): boolean => ruleType === 'threat_match';
+export const isThreatMatchRule = (ruleType: Type | undefined): boolean =>
+ ruleType === 'threat_match';
diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
index ffeaf853828f13..8aec9768dd50d2 100644
--- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
@@ -101,6 +101,7 @@ const POLICY_RESPONSE_STATUSES: HostPolicyResponseActionStatus[] = [
HostPolicyResponseActionStatus.success,
HostPolicyResponseActionStatus.failure,
HostPolicyResponseActionStatus.warning,
+ HostPolicyResponseActionStatus.unsupported,
];
const APPLIED_POLICIES: Array<{
@@ -1492,7 +1493,7 @@ export class EndpointDocGenerator {
{
name: 'workflow',
message: 'Failed to apply a portion of the configuration (kernel)',
- status: HostPolicyResponseActionStatus.success,
+ status: HostPolicyResponseActionStatus.unsupported,
},
{
name: 'download_model',
@@ -1637,6 +1638,7 @@ export class EndpointDocGenerator {
HostPolicyResponseActionStatus.failure,
HostPolicyResponseActionStatus.success,
HostPolicyResponseActionStatus.warning,
+ HostPolicyResponseActionStatus.unsupported,
]);
}
diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts
index d361c0d6282a34..94a09b385a08c8 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts
@@ -933,6 +933,7 @@ export enum HostPolicyResponseActionStatus {
success = 'success',
failure = 'failure',
warning = 'warning',
+ unsupported = 'unsupported',
}
/**
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts
index 40e353263bcc8c..7e19944ea5856c 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts
@@ -13,6 +13,7 @@ export enum HostPolicyResponseActionStatus {
success = 'success',
failure = 'failure',
warning = 'warning',
+ unsupported = 'unsupported',
}
export enum HostsFields {
diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts
index d6ec668e1b0f9f..988f0ad0c125d4 100644
--- a/x-pack/plugins/security_solution/common/shared_imports.ts
+++ b/x-pack/plugins/security_solution/common/shared_imports.ts
@@ -43,6 +43,7 @@ export {
ExceptionListType,
Type,
ENDPOINT_LIST_ID,
+ ENDPOINT_TRUSTED_APPS_LIST_ID,
osTypeArray,
OsTypeArray,
} from '../../lists/common';
diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts
index cee8ccdea3e9e1..58e3b9824d8fdb 100644
--- a/x-pack/plugins/security_solution/common/types/timeline/index.ts
+++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts
@@ -281,7 +281,7 @@ export enum TimelineId {
active = 'timeline-1',
casePage = 'timeline-case',
test = 'test', // Reserved for testing purposes
- test2 = 'test2',
+ alternateTest = 'alternateTest',
}
export const TimelineIdLiteralRt = runtimeTypes.union([
diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
index ca4c869e0f2d38..c001f1fc2bc47d 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts
@@ -99,7 +99,7 @@ export const goToQueryTab = () => {
export const addNotesToTimeline = (notes: string) => {
goToNotesTab();
cy.get(NOTES_TEXT_AREA).type(notes);
- cy.get(ADD_NOTE_BUTTON).click();
+ cy.get(ADD_NOTE_BUTTON).click({ force: true });
cy.get(QUERY_TAB_BUTTON).click();
};
diff --git a/x-pack/plugins/security_solution/public/cases/components/confirm_delete_case/index.tsx b/x-pack/plugins/security_solution/public/cases/components/confirm_delete_case/index.tsx
index 1b67aaeb795dd1..eb75d896ae7788 100644
--- a/x-pack/plugins/security_solution/public/cases/components/confirm_delete_case/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/confirm_delete_case/index.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
+import { EuiConfirmModal } from '@elastic/eui';
import * as i18n from './translations';
interface ConfirmDeleteCaseModalProps {
@@ -28,20 +28,18 @@ const ConfirmDeleteCaseModalComp: React.FC = ({
return null;
}
return (
-
-
- {isPlural ? i18n.CONFIRM_QUESTION_PLURAL : i18n.CONFIRM_QUESTION}
-
-
+
+ {isPlural ? i18n.CONFIRM_QUESTION_PLURAL : i18n.CONFIRM_QUESTION}
+
);
};
diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx
index 1dfabda8068f17..eda8ed8cdfbcd5 100644
--- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx
@@ -6,13 +6,7 @@
*/
import React, { memo } from 'react';
-import {
- EuiModal,
- EuiModalBody,
- EuiModalHeader,
- EuiModalHeaderTitle,
- EuiOverlayMask,
-} from '@elastic/eui';
+import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui';
import { useGetUserSavedObjectPermissions } from '../../../common/lib/kibana';
import { Case } from '../../containers/types';
@@ -34,16 +28,14 @@ const AllCasesModalComponent: React.FC = ({
const userCanCrud = userPermissions?.crud ?? false;
return isModalOpen ? (
-
-
-
- {i18n.SELECT_CASE_TITLE}
-
-
-
-
-
-
+
+
+ {i18n.SELECT_CASE_TITLE}
+
+
+
+
+
) : null;
};
diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx
index 3595f2c916af71..8dd5080666cb38 100644
--- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx
@@ -7,13 +7,7 @@
import React, { memo } from 'react';
import styled from 'styled-components';
-import {
- EuiModal,
- EuiModalBody,
- EuiModalHeader,
- EuiModalHeaderTitle,
- EuiOverlayMask,
-} from '@elastic/eui';
+import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui';
import { FormContext } from '../create/form_context';
import { CreateCaseForm } from '../create/form';
@@ -40,21 +34,19 @@ const CreateModalComponent: React.FC = ({
onSuccess,
}) => {
return isModalOpen ? (
-
-
-
- {i18n.CREATE_TITLE}
-
-
-
-
-
-
-
-
-
-
-
+
+
+ {i18n.CREATE_TITLE}
+
+
+
+
+
+
+
+
+
+
) : null;
};
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
index a37528fcb24d7c..3ecc17589fe084 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
@@ -201,7 +201,7 @@ describe('EventsViewer', () => {
testProps = {
...testProps,
// Update with a new id, to force columns back to default.
- id: TimelineId.test2,
+ id: TimelineId.alternateTest,
};
const wrapper = mount(
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
index dc7388438c012a..5ea11f61f9a7e5 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
@@ -14,7 +14,6 @@ import {
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalFooter,
- EuiOverlayMask,
EuiButton,
EuiButtonEmpty,
EuiHorizontalRule,
@@ -348,133 +347,129 @@ export const AddExceptionModal = memo(function AddExceptionModal({
}, [maybeRule]);
return (
-
-
-
- {addExceptionMessage}
-
- {ruleName}
-
-
-
- {fetchOrCreateListError != null && (
-
-
-
+
+
+ {addExceptionMessage}
+
+ {ruleName}
+
+
+
+ {fetchOrCreateListError != null && (
+
+
+
+ )}
+ {fetchOrCreateListError == null &&
+ (isLoadingExceptionList ||
+ isIndexPatternLoading ||
+ isSignalIndexLoading ||
+ isSignalIndexPatternLoading) && (
+
)}
- {fetchOrCreateListError == null &&
- (isLoadingExceptionList ||
- isIndexPatternLoading ||
- isSignalIndexLoading ||
- isSignalIndexPatternLoading) && (
-
- )}
- {fetchOrCreateListError == null &&
- !isSignalIndexLoading &&
- !isSignalIndexPatternLoading &&
- !isLoadingExceptionList &&
- !isIndexPatternLoading &&
- !isRuleLoading &&
- !mlJobLoading &&
- ruleExceptionList && (
- <>
-
- {isRuleEQLSequenceStatement && (
- <>
-
-
- >
- )}
- {i18n.EXCEPTION_BUILDER_INFO}
-
-
-
-
-
-
-
-
-
- {alertData !== undefined && alertStatus !== 'closed' && (
-
-
-
- )}
+ {fetchOrCreateListError == null &&
+ !isSignalIndexLoading &&
+ !isSignalIndexPatternLoading &&
+ !isLoadingExceptionList &&
+ !isIndexPatternLoading &&
+ !isRuleLoading &&
+ !mlJobLoading &&
+ ruleExceptionList && (
+ <>
+
+ {isRuleEQLSequenceStatement && (
+ <>
+
+
+ >
+ )}
+ {i18n.EXCEPTION_BUILDER_INFO}
+
+
+
+
+
+
+
+
+
+ {alertData !== undefined && alertStatus !== 'closed' && (
- {exceptionListType === 'endpoint' && (
- <>
-
-
- {i18n.ENDPOINT_QUARANTINE_TEXT}
-
- >
- )}
-
- >
- )}
- {fetchOrCreateListError == null && (
-
- {i18n.CANCEL}
-
-
- {addExceptionMessage}
-
-
+ )}
+
+
+
+ {exceptionListType === 'endpoint' && (
+ <>
+
+
+ {i18n.ENDPOINT_QUARANTINE_TEXT}
+
+ >
+ )}
+
+ >
)}
-
-
+ {fetchOrCreateListError == null && (
+
+ {i18n.CANCEL}
+
+
+ {addExceptionMessage}
+
+
+ )}
+
);
});
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
index 75b7bf2aabd7fd..336732016e9369 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
@@ -12,7 +12,6 @@ import {
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalFooter,
- EuiOverlayMask,
EuiButton,
EuiButtonEmpty,
EuiHorizontalRule,
@@ -281,125 +280,121 @@ export const EditExceptionModal = memo(function EditExceptionModal({
}, [maybeRule]);
return (
-
-
-
-
- {exceptionListType === 'endpoint'
- ? i18n.EDIT_ENDPOINT_EXCEPTION_TITLE
- : i18n.EDIT_EXCEPTION_TITLE}
-
-
- {ruleName}
-
-
- {(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && (
-
- )}
- {!isSignalIndexLoading &&
- !addExceptionIsLoading &&
- !isIndexPatternLoading &&
- !isRuleLoading &&
- !mlJobLoading && (
- <>
-
- {isRuleEQLSequenceStatement && (
- <>
-
-
- >
- )}
- {i18n.EXCEPTION_BUILDER_INFO}
-
-
-
-
-
-
-
-
-
-
-
+
+
+ {exceptionListType === 'endpoint'
+ ? i18n.EDIT_ENDPOINT_EXCEPTION_TITLE
+ : i18n.EDIT_EXCEPTION_TITLE}
+
+
+ {ruleName}
+
+
+ {(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && (
+
+ )}
+ {!isSignalIndexLoading &&
+ !addExceptionIsLoading &&
+ !isIndexPatternLoading &&
+ !isRuleLoading &&
+ !mlJobLoading && (
+ <>
+
+ {isRuleEQLSequenceStatement && (
+ <>
+
-
- {exceptionListType === 'endpoint' && (
- <>
-
-
- {i18n.ENDPOINT_QUARANTINE_TEXT}
-
- >
- )}
-
- >
- )}
- {updateError != null && (
-
-
-
- )}
- {hasVersionConflict && (
-
-
- {i18n.VERSION_CONFLICT_ERROR_DESCRIPTION}
-
-
- )}
- {updateError == null && (
-
- {i18n.CANCEL}
-
-
- {i18n.EDIT_EXCEPTION_SAVE_BUTTON}
-
-
+
+ >
+ )}
+ {i18n.EXCEPTION_BUILDER_INFO}
+
+
+
+
+
+
+
+
+
+
+
+
+ {exceptionListType === 'endpoint' && (
+ <>
+
+
+ {i18n.ENDPOINT_QUARANTINE_TEXT}
+
+ >
+ )}
+
+ >
)}
-
-
+ {updateError != null && (
+
+
+
+ )}
+ {hasVersionConflict && (
+
+
+ {i18n.VERSION_CONFLICT_ERROR_DESCRIPTION}
+
+
+ )}
+ {updateError == null && (
+
+ {i18n.CANCEL}
+
+
+ {i18n.EDIT_EXCEPTION_SAVE_BUTTON}
+
+
+ )}
+
);
});
diff --git a/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap
index 6503dd8dfb5086..d1a41b1c32c102 100644
--- a/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap
@@ -2,64 +2,62 @@
exports[`ImportDataModal renders correctly against snapshot 1`] = `
-
-
-
-
- title
-
-
-
-
-
- description
-
-
-
-
-
-
-
-
-
- Cancel
-
-
- submitBtnText
-
-
-
-
+
+
+
+ title
+
+
+
+
+
+ description
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ submitBtnText
+
+
+
`;
diff --git a/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx
index 8a29ce3799321f..4c3dc2a249b4ff 100644
--- a/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx
@@ -15,7 +15,6 @@ import {
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
- EuiOverlayMask,
EuiSpacer,
EuiText,
} from '@elastic/eui';
@@ -132,51 +131,49 @@ export const ImportDataModalComponent = ({
return (
<>
{showModal && (
-
-
-
- {title}
-
-
-
-
- {description}
-
-
-
- {
- setSelectedFiles(files && files.length > 0 ? files : null);
- }}
- display={'large'}
- fullWidth={true}
- isLoading={isImporting}
+
+
+ {title}
+
+
+
+
+ {description}
+
+
+
+ {
+ setSelectedFiles(files && files.length > 0 ? files : null);
+ }}
+ display={'large'}
+ fullWidth={true}
+ isLoading={isImporting}
+ />
+
+ {showCheckBox && (
+ setOverwrite(!overwrite)}
/>
-
- {showCheckBox && (
- setOverwrite(!overwrite)}
- />
- )}
-
-
-
- {i18n.CANCEL_BUTTON}
-
- {submitBtnText}
-
-
-
-
+ )}
+
+
+
+ {i18n.CANCEL_BUTTON}
+
+ {submitBtnText}
+
+
+
)}
>
);
diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx
index ece29cd360ce71..a5c0144531110a 100644
--- a/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx
@@ -15,7 +15,6 @@ import {
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalFooter,
- EuiOverlayMask,
EuiSpacer,
EuiTabbedContent,
} from '@elastic/eui';
@@ -211,24 +210,22 @@ export const ModalInspectQuery = ({
];
return (
-
-
-
-
- {i18n.INSPECT} {title}
-
-
-
-
-
-
-
-
-
- {i18n.CLOSE}
-
-
-
-
+
+
+
+ {i18n.INSPECT} {title}
+
+
+
+
+
+
+
+
+
+ {i18n.CLOSE}
+
+
+
);
};
diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap
index 778916ad2d07ac..be5702550a44c8 100644
--- a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap
@@ -246,6 +246,12 @@ exports[`Paginated Table Component rendering it renders the default load more ta
},
"euiFilePickerTallHeight": "128px",
"euiFlyoutBorder": "1px solid #343741",
+ "euiFlyoutPaddingModifiers": Object {
+ "paddingLarge": "24px",
+ "paddingMedium": "16px",
+ "paddingNone": 0,
+ "paddingSmall": "8px",
+ },
"euiFocusBackgroundColor": "#08334a",
"euiFocusRingAnimStartColor": "rgba(27, 169, 245, 0)",
"euiFocusRingAnimStartSize": "6px",
@@ -357,6 +363,7 @@ exports[`Paginated Table Component rendering it renders the default load more ta
},
"euiMarkdownEditorMinHeight": "150px",
"euiPageBackgroundColor": "#1a1b20",
+ "euiPageDefaultMaxWidth": "1000px",
"euiPaletteColorBlind": Object {
"euiColorVis0": Object {
"behindText": "#6dccb1",
@@ -534,6 +541,7 @@ exports[`Paginated Table Component rendering it renders the default load more ta
"euiSwitchWidthCompressed": "28px",
"euiSwitchWidthMini": "22px",
"euiTabFontSize": "16px",
+ "euiTabFontSizeL": "18px",
"euiTabFontSizeS": "14px",
"euiTableActionsAreaWidth": "40px",
"euiTableActionsBorderColor": "rgba(83, 89, 102, 0.09999999999999998)",
diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap
index f7924f37d2c173..5e008e28073de1 100644
--- a/x-pack/plugins/security_solution/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap
@@ -1,50 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Modal all errors rendering it renders the default all errors modal when isShowing is positive 1`] = `
-
-
-
-
- Your visualization has error(s)
-
-
-
-
-
-
-
- Error 1, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
-
-
-
-
-
- Close
-
-
-
-
+
+
+
+ Your visualization has error(s)
+
+
+
+
+
+
+
+ Error 1, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
+
+
+
+ Close
+
+
+
`;
diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.tsx b/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.tsx
index 873ebe97317f4f..0a78139f5fe3a1 100644
--- a/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.tsx
@@ -7,7 +7,6 @@
import {
EuiButton,
- EuiOverlayMask,
EuiModal,
EuiModalHeader,
EuiModalHeaderTitle,
@@ -36,36 +35,34 @@ const ModalAllErrorsComponent: React.FC = ({ isShowing, toast, t
if (!isShowing || toast == null) return null;
return (
-
-
-
- {i18n.TITLE_ERROR_MODAL}
-
+
+
+ {i18n.TITLE_ERROR_MODAL}
+
-
-
-
- {toast.errors != null &&
- toast.errors.map((error, index) => (
- 100 ? `${error.substring(0, 100)} ...` : error}
- data-test-subj="modal-all-errors-accordion"
- >
- {error}
-
- ))}
-
+
+
+
+ {toast.errors != null &&
+ toast.errors.map((error, index) => (
+ 100 ? `${error.substring(0, 100)} ...` : error}
+ data-test-subj="modal-all-errors-accordion"
+ >
+ {error}
+
+ ))}
+
-
-
- {i18n.CLOSE_ERROR_MODAL}
-
-
-
-
+
+
+ {i18n.CLOSE_ERROR_MODAL}
+
+
+
);
};
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts
index 4ed971ea6a936e..cca745659d2ccf 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/helpers.ts
@@ -14,6 +14,9 @@ export const getStatusColor = (status: RuleStatusType | string | null) =>
? 'success'
: status === 'failed'
? 'danger'
- : status === 'executing' || status === 'going to run' || status === 'partial failure'
+ : status === 'executing' ||
+ status === 'going to run' ||
+ status === 'partial failure' ||
+ status === 'warning'
? 'warning'
: 'subdued';
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx
index 6292cc5b530b04..677e6de0ff485d 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_status/index.tsx
@@ -16,7 +16,11 @@ import {
import React, { memo, useCallback, useEffect, useState } from 'react';
import deepEqual from 'fast-deep-equal';
-import { useRuleStatus, RuleInfoStatus } from '../../../containers/detection_engine/rules';
+import {
+ useRuleStatus,
+ RuleInfoStatus,
+ RuleStatusType,
+} from '../../../containers/detection_engine/rules';
import { FormattedDate } from '../../../../common/components/formatted_date';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
import { getStatusColor } from './helpers';
@@ -55,6 +59,19 @@ const RuleStatusComponent: React.FC = ({ ruleId, ruleEnabled })
}
}, [fetchRuleStatus, ruleId]);
+ const getStatus = useCallback((status: RuleStatusType | null | undefined) => {
+ if (status == null) {
+ return getEmptyTagValue();
+ } else if (status != null && status === 'partial failure') {
+ // Temporary fix if on upgrade a rule has a status of 'partial failure' we want to display that text as 'warning'
+ // On the next subsequent rule run, that 'partial failure' status will be re-written as a 'warning' status
+ // and this code will no longer be necessary
+ // TODO: remove this code in 8.0.0
+ return 'warning';
+ }
+ return status;
+ }, []);
+
return (
@@ -71,7 +88,7 @@ const RuleStatusComponent: React.FC = ({ ruleId, ruleEnabled })
- {currentStatus?.status ?? getEmptyTagValue()}
+ {getStatus(currentStatus?.status)}
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts
index 08feb5f2e51660..f73b2ccfb02ae6 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/default_value.ts
@@ -29,6 +29,7 @@ export const stepAboutDefaultValue: AboutStepRule = {
license: '',
ruleNameOverride: '',
tags: [],
+ threatIndicatorPath: '',
timestampOverride: '',
threat: threatDefault,
note: '',
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
index 209071d27536d5..25295a823ea66e 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
@@ -40,6 +40,7 @@ import { SeverityField } from '../severity_mapping';
import { RiskScoreField } from '../risk_score_mapping';
import { AutocompleteField } from '../autocomplete_field';
import { useFetchIndex } from '../../../../common/containers/source';
+import { isThreatMatchRule } from '../../../../../common/detection_engine/utils';
const CommonUseField = getUseField({ component: Field });
@@ -298,6 +299,23 @@ const StepAboutRuleComponent: FC = ({
/>
+ {isThreatMatchRule(defineRuleData?.ruleType) && (
+ <>
+
+ >
+ )}
+
= {
),
labelAppend: OptionalFieldLabel,
},
+ threatIndicatorPath: {
+ type: FIELD_TYPES.TEXT,
+ label: i18n.translate(
+ 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThreatIndicatorPathLabel',
+ {
+ defaultMessage: 'Threat Indicator Path',
+ }
+ ),
+ helpText: i18n.translate(
+ 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThreatIndicatorPathHelpText',
+ {
+ defaultMessage:
+ 'Specify the document path containing your threat indicator fields. Used for enrichment of indicator match alerts. Defaults to threat.indicator unless otherwise specified.',
+ }
+ ),
+ labelAppend: OptionalFieldLabel,
+ },
timestampOverride: {
type: FIELD_TYPES.TEXT,
label: i18n.translate(
diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx
index adc46f08272d7e..aefa447269f46c 100644
--- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/modal.tsx
@@ -15,7 +15,6 @@ import {
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
- EuiOverlayMask,
EuiPanel,
EuiSpacer,
EuiText,
@@ -211,7 +210,7 @@ export const ValueListsModalComponent: React.FC = ({
const columns = buildColumns(handleExport, handleDelete);
return (
-
+ <>
{i18n.MODAL_TITLE}
@@ -255,7 +254,7 @@ export const ValueListsModalComponent: React.FC = ({
name={exportDownload.name}
onDownload={() => setExportDownload({})}
/>
-
+ >
);
};
diff --git a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/reference_error_modal/reference_error_modal.tsx b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/reference_error_modal/reference_error_modal.tsx
index 20744c3a22515f..e4d8e2cee32685 100644
--- a/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/reference_error_modal/reference_error_modal.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/value_lists_management_modal/reference_error_modal/reference_error_modal.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import { EuiConfirmModal, EuiListGroup, EuiListGroupItem, EuiOverlayMask } from '@elastic/eui';
+import { EuiConfirmModal, EuiListGroup, EuiListGroupItem } from '@elastic/eui';
import styled from 'styled-components';
import { rgba } from 'polished';
@@ -59,28 +59,26 @@ export const ReferenceErrorModalComponent: React.FC =
}
return (
-
-
- {contentText}
-
-
- {references.map((r, index) => (
-
- ))}
-
-
-
-
+
+ {contentText}
+
+
+ {references.map((r, index) => (
+
+ ))}
+
+
+
);
};
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts
index 591432829d90a0..b8f6c4bde3e8f7 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts
@@ -24,6 +24,7 @@ import {
listArray,
threat_query,
threat_index,
+ threat_indicator_path,
threat_mapping,
threat_language,
threat_filters,
@@ -77,6 +78,7 @@ const StatusTypes = t.union([
t.literal('failed'),
t.literal('going to run'),
t.literal('partial failure'),
+ t.literal('warning'),
]);
// TODO: make a ticket
@@ -132,6 +134,7 @@ export const RuleSchema = t.intersection([
threat_query,
threat_filters,
threat_index,
+ threat_indicator_path,
threat_mapping,
threat_language,
timeline_id: t.string,
@@ -252,7 +255,13 @@ export interface RuleStatus {
failures: RuleInfoStatus[];
}
-export type RuleStatusType = 'executing' | 'failed' | 'going to run' | 'succeeded';
+export type RuleStatusType =
+ | 'executing'
+ | 'failed'
+ | 'going to run'
+ | 'succeeded'
+ | 'partial failure'
+ | 'warning';
export interface RuleInfoStatus {
alert_id: string;
status_date: string;
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
index 425848cd09af0d..d110f2d52b3c5e 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx
@@ -313,7 +313,11 @@ export const getMonitoringColumns = (
}}
href={formatUrl(getRuleDetailsUrl(item.id))}
>
- {value}
+ {/* Temporary fix if on upgrade a rule has a status of 'partial failure' we want to display that text as 'warning' */}
+ {/* On the next subsequent rule run, that 'partial failure' status will be re-written as a 'warning' status */}
+ {/* and this code will no longer be necessary */}
+ {/* TODO: remove this code in 8.0.0 */}
+ {value === 'partial failure' ? 'warning' : value}
);
},
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx
index 89785efbb5047a..04bf3c544030a4 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx
@@ -9,7 +9,6 @@ import {
EuiBasicTable,
EuiLoadingContent,
EuiProgress,
- EuiOverlayMask,
EuiConfirmModal,
EuiWindowEvent,
} from '@elastic/eui';
@@ -490,18 +489,16 @@ export const RulesTables = React.memo(
)}
{showIdleModal && (
-
-
- {i18n.REFRESH_PROMPT_BODY}
-
-
+
+ {i18n.REFRESH_PROMPT_BODY}
+
)}
{shouldShowRulesTable && (
<>
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts
index 5e2aeb4ead9340..fdb0513d7b7082 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts
@@ -493,6 +493,15 @@ describe('helpers', () => {
expect(result.exceptions_list).toEqual([getListMock()]);
});
+ test('returns a threat indicator path', () => {
+ mockData = {
+ ...mockData,
+ threatIndicatorPath: 'my_custom.path',
+ };
+ const result = formatAboutStepData(mockData);
+ expect(result.threat_indicator_path).toEqual('my_custom.path');
+ });
+
test('returns formatted object with both exceptions_lists', () => {
const result = formatAboutStepData(
{
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
index c09f85ce7edccc..7c447214cfdebb 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts
@@ -288,6 +288,7 @@ export const formatAboutStepData = (
isBuildingBlock,
note,
ruleNameOverride,
+ threatIndicatorPath,
timestampOverride,
...rest
} = aboutStepData;
@@ -330,6 +331,7 @@ export const formatAboutStepData = (
...singleThreat,
framework: 'MITRE ATT&CK',
})),
+ threat_indicator_path: threatIndicatorPath,
timestamp_override: timestampOverride !== '' ? timestampOverride : undefined,
...(!isEmpty(note) ? { note } : {}),
...rest,
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
index 5836cac09e9b81..ed88ca41146f18 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
@@ -314,7 +314,7 @@ const RuleDetailsPageComponent = () => {
/>
);
} else if (
- rule?.status === 'partial failure' &&
+ (rule?.status === 'warning' || rule?.status === 'partial failure') &&
ruleDetailTab === RuleDetailTabs.alerts &&
rule?.last_success_at != null
) {
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts
index 4e6d8f4d567b18..1d100fb9109d07 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts
@@ -52,7 +52,7 @@ export const ERROR_CALLOUT_TITLE = i18n.translate(
export const PARTIAL_FAILURE_CALLOUT_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleDetails.partialErrorCalloutTitle',
{
- defaultMessage: 'Partial rule failure at',
+ defaultMessage: 'Warning at',
}
);
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx
index f0511602bd67f7..111eb8a5594a8d 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx
@@ -116,6 +116,7 @@ describe('rule helpers', () => {
severity: { value: 'low', mapping: fillEmptySeverityMappings([]), isMappingChecked: false },
tags: ['tag1', 'tag2'],
threat: getThreatMock(),
+ threatIndicatorPath: '',
timestampOverride: 'event.ingested',
};
const scheduleRuleStepData = { from: '0s', interval: '5m' };
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx
index 35f9f0c658a6af..d37c2d9141f5d4 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx
@@ -153,6 +153,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu
risk_score: riskScore,
tags,
threat,
+ threat_indicator_path: threatIndicatorPath,
} = rule;
return {
@@ -179,6 +180,7 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu
},
falsePositives,
threat: threat as Threats,
+ threatIndicatorPath: threatIndicatorPath ?? '',
};
};
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts
index 218d8c0178a2b0..94fdcc4069fc23 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts
@@ -101,6 +101,7 @@ export interface AboutStepRule {
ruleNameOverride: string;
tags: string[];
timestampOverride: string;
+ threatIndicatorPath?: string;
threat: Threats;
note: string;
}
@@ -186,6 +187,7 @@ export interface AboutStepRuleJson {
rule_name_override?: RuleNameOverride;
tags: string[];
threat: Threats;
+ threat_indicator_path?: string;
timestamp_override?: TimestampOverride;
note?: string;
}
diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts
index 4397573217312d..f70cd37b8da94e 100644
--- a/x-pack/plugins/security_solution/public/graphql/types.ts
+++ b/x-pack/plugins/security_solution/public/graphql/types.ts
@@ -273,6 +273,7 @@ export enum HostPolicyResponseActionStatus {
success = 'success',
failure = 'failure',
warning = 'warning',
+ unsupported = 'unsupported',
}
export enum TimelineType {
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts
index 37a26d88053521..4745cd9de249db 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts
@@ -25,6 +25,7 @@ export const POLICY_STATUS_TO_HEALTH_COLOR = Object.freeze<
success: 'success',
warning: 'warning',
failure: 'danger',
+ unsupported: 'subdued',
});
export const POLICY_STATUS_TO_TEXT = Object.freeze<
@@ -39,4 +40,7 @@ export const POLICY_STATUS_TO_TEXT = Object.freeze<
failure: i18n.translate('xpack.securitySolution.policyStatusText.failure', {
defaultMessage: 'Failure',
}),
+ unsupported: i18n.translate('xpack.securitySolution.policyStatusText.unsupported', {
+ defaultMessage: 'Unsupported',
+ }),
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx
index ec0198de585589..e14f56881d6733 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx
@@ -12,7 +12,6 @@ import {
EuiButton,
EuiButtonEmpty,
EuiSpacer,
- EuiOverlayMask,
EuiConfirmModal,
EuiCallOut,
EuiLoadingSpinner,
@@ -234,59 +233,54 @@ const ConfirmUpdate = React.memo<{
onCancel: () => void;
}>(({ hostCount, onCancel, onConfirm }) => {
return (
-
-
- {hostCount > 0 && (
- <>
-
-
-
-
- >
- )}
-
-
-
-
-
+
+ {hostCount > 0 && (
+ <>
+
+
+
+
+ >
+ )}
+
+
+
+
);
});
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx
index 4e3dc953b539e2..bffd9806103721 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx
@@ -19,7 +19,6 @@ import {
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
- EuiOverlayMask,
EuiText,
} from '@elastic/eui';
@@ -100,36 +99,34 @@ export const TrustedAppDeletionDialog = memo(() => {
if (useTrustedAppsSelector(isDeletionDialogOpen)) {
return (
-
-
-
- {translations.title}
-
+
+
+ {translations.title}
+
-
-
- {translations.mainMessage}
- {translations.subMessage}
-
-
+
+
+ {translations.mainMessage}
+ {translations.subMessage}
+
+
-
-
- {translations.cancelButton}
-
+
+
+ {translations.cancelButton}
+
-
- {translations.confirmButton}
-
-
-
-
+
+ {translations.confirmButton}
+
+
+
);
} else {
return <>>;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx
index 35dcd88b77e04e..6713be176586cc 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.test.tsx
@@ -9,6 +9,7 @@ import React from 'react';
import { useKibana } from '../../../../common/lib/kibana';
import { TestProviders, mockIndexNames, mockIndexPattern } from '../../../../common/mock';
+import { TimelineId } from '../../../../../common/types/timeline';
import { useTimelineKpis } from '../../../containers/kpis';
import { FlyoutHeader } from '.';
import { useSourcererScope } from '../../../../common/containers/sourcerer';
@@ -33,6 +34,14 @@ const mockUseTimelineKpiResponse = {
hostCount: 1,
destinationIpCount: 1,
};
+
+const mockUseTimelineLargeKpiResponse = {
+ processCount: 1000,
+ userCount: 1000000,
+ sourceIpCount: 1000000000,
+ hostCount: 999,
+ destinationIpCount: 1,
+};
const defaultMocks = {
browserFields: mockBrowserFields,
docValueFields: mockDocValueFields,
@@ -65,7 +74,7 @@ describe('Timeline KPIs', () => {
it('renders the component, labels and values succesfully', async () => {
const wrapper = mount(
-
+
);
expect(wrapper.find('[data-test-subj="siem-timeline-kpis"]').exists()).toEqual(true);
@@ -87,7 +96,7 @@ describe('Timeline KPIs', () => {
it('renders a loading indicator for values', async () => {
const wrapper = mount(
-
+
);
expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
@@ -103,7 +112,7 @@ describe('Timeline KPIs', () => {
it('renders labels and the default empty string', async () => {
const wrapper = mount(
-
+
);
@@ -115,4 +124,29 @@ describe('Timeline KPIs', () => {
);
});
});
+
+ describe('when the response contains numbers larger than one thousand', () => {
+ beforeEach(() => {
+ mockUseTimelineKpis.mockReturnValue([false, mockUseTimelineLargeKpiResponse]);
+ });
+ it('formats the numbers correctly', async () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find('[data-test-subj="siem-timeline-process-kpi"]').first().text()).toEqual(
+ expect.stringContaining('1k')
+ );
+ expect(wrapper.find('[data-test-subj="siem-timeline-user-kpi"]').first().text()).toEqual(
+ expect.stringContaining('1m')
+ );
+ expect(wrapper.find('[data-test-subj="siem-timeline-source-ip-kpi"]').first().text()).toEqual(
+ expect.stringContaining('1b')
+ );
+ expect(wrapper.find('[data-test-subj="siem-timeline-host-kpi"]').first().text()).toEqual(
+ expect.stringContaining('999')
+ );
+ });
+ });
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis.tsx
index 3b0a86432aa969..e487fe70fdc94c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/kpis.tsx
@@ -5,61 +5,95 @@
* 2.0.
*/
-import React from 'react';
+import React, { useMemo } from 'react';
-import { EuiStat, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
+import { EuiStat, EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui';
+import numeral from '@elastic/numeral';
+import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants';
+import { useUiSetting$ } from '../../../../common/lib/kibana';
import { TimelineKpiStrategyResponse } from '../../../../../common/search_strategy';
import { getEmptyValue } from '../../../../common/components/empty_value';
import * as i18n from './translations';
export const TimelineKPIs = React.memo(
({ kpis, isLoading }: { kpis: TimelineKpiStrategyResponse | null; isLoading: boolean }) => {
+ const kpiFormat = '0,0.[000]a';
+ const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT);
+ const formattedKpis = useMemo(() => {
+ return {
+ process: kpis === null ? getEmptyValue() : numeral(kpis.processCount).format(kpiFormat),
+ user: kpis === null ? getEmptyValue() : numeral(kpis.userCount).format(kpiFormat),
+ host: kpis === null ? getEmptyValue() : numeral(kpis.hostCount).format(kpiFormat),
+ sourceIp: kpis === null ? getEmptyValue() : numeral(kpis.sourceIpCount).format(kpiFormat),
+ destinationIp:
+ kpis === null ? getEmptyValue() : numeral(kpis.destinationIpCount).format(kpiFormat),
+ };
+ }, [kpis]);
+ const formattedKpiToolTips = useMemo(() => {
+ return {
+ process: numeral(kpis?.processCount).format(defaultNumberFormat),
+ user: numeral(kpis?.userCount).format(defaultNumberFormat),
+ host: numeral(kpis?.hostCount).format(defaultNumberFormat),
+ sourceIp: numeral(kpis?.sourceIpCount).format(defaultNumberFormat),
+ destinationIp: numeral(kpis?.destinationIpCount).format(defaultNumberFormat),
+ };
+ }, [kpis, defaultNumberFormat]);
return (
-
+
+
+
-
+
+
+
-
+
+
+
-
+
+
+
-
+
+
+
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.tsx
index a87f486a9d5d1f..7dde3fbe4cd2a6 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/delete_timeline_modal/index.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EuiModal, EuiOverlayMask } from '@elastic/eui';
+import { EuiModal } from '@elastic/eui';
import React, { useCallback } from 'react';
import { createGlobalStyle } from 'styled-components';
@@ -46,16 +46,14 @@ export const DeleteTimelineModalOverlay = React.memo(
<>
{isModalOpen && }
{isModalOpen ? (
-
-
-
-
-
+
+
+
) : null}
>
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.tsx
index 5b7fbcffd14ad7..c23cffa854514f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EuiModal, EuiOverlayMask } from '@elastic/eui';
+import { EuiModal } from '@elastic/eui';
import React from 'react';
import { TimelineModel } from '../../../../timelines/store/timeline/model';
@@ -26,22 +26,20 @@ const OPEN_TIMELINE_MODAL_WIDTH = 1100; // px
export const OpenTimelineModal = React.memo(
({ hideActions = [], modalTitle, onClose, onOpen }) => (
-
-
-
-
-
+
+
+
)
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap
index 124c8012fd533c..aece377ee4f2dc 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap
@@ -287,7 +287,7 @@ Array [
data-eui="EuiFocusTrap"
>
-
- {isSaving && (
-
+
+ {isSaving && (
+
+ )}
+ {modalHeader}
+
+
+ {showWarning && (
+
+
+
+
)}
- {modalHeader}
-
-
- {showWarning && (
-
-
-
-
- )}
-
-
-
-
+
+
+
);
}
);
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts
index 2833a5ad24f2a3..88bf7941c84643 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts
@@ -10,12 +10,17 @@ import { listMock } from '../../../../../lists/server/mocks';
import { getFoundExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock';
import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { EntriesArray, EntryList } from '../../../../../lists/common/schemas/types';
-import { buildArtifact, getFullEndpointExceptionList } from './lists';
+import {
+ buildArtifact,
+ getEndpointExceptionList,
+ getEndpointTrustedAppsList,
+ getFilteredEndpointExceptionList,
+} from './lists';
import { TranslatedEntry, TranslatedExceptionListItem } from '../../schemas/artifacts';
import { ArtifactConstants } from './common';
-import { ENDPOINT_LIST_ID } from '../../../../../lists/common';
+import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common';
-describe('buildEventTypeSignal', () => {
+describe('artifacts lists', () => {
let mockExceptionClient: ExceptionListClient;
beforeEach(() => {
@@ -23,214 +28,384 @@ describe('buildEventTypeSignal', () => {
mockExceptionClient = listMock.getExceptionListClient();
});
- test('it should convert the exception lists response to the proper endpoint format', async () => {
- const expectedEndpointExceptions = {
- type: 'simple',
- entries: [
- {
- entries: [
- {
- field: 'nested.field',
- operator: 'included',
- type: 'exact_cased',
- value: 'some value',
- },
- ],
- field: 'some.parentField',
- type: 'nested',
- },
- {
- field: 'some.not.nested.field',
- operator: 'included',
- type: 'exact_cased',
- value: 'some value',
- },
- ],
- };
-
- const first = getFoundExceptionListItemSchemaMock();
- mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
- const resp = await getFullEndpointExceptionList(
- mockExceptionClient,
- 'linux',
- 'v1',
- ENDPOINT_LIST_ID
- );
- expect(resp).toEqual({
- entries: [expectedEndpointExceptions],
+ describe('getFilteredEndpointExceptionList', () => {
+ const TEST_FILTER = 'exception-list-agnostic.attributes.os_types:"linux"';
+
+ test('it should convert the exception lists response to the proper endpoint format', async () => {
+ const expectedEndpointExceptions = {
+ type: 'simple',
+ entries: [
+ {
+ entries: [
+ {
+ field: 'nested.field',
+ operator: 'included',
+ type: 'exact_cased',
+ value: 'some value',
+ },
+ ],
+ field: 'some.parentField',
+ type: 'nested',
+ },
+ {
+ field: 'some.not.nested.field',
+ operator: 'included',
+ type: 'exact_cased',
+ value: 'some value',
+ },
+ ],
+ };
+
+ const first = getFoundExceptionListItemSchemaMock();
+ mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
+ const resp = await getFilteredEndpointExceptionList(
+ mockExceptionClient,
+ 'v1',
+ TEST_FILTER,
+ ENDPOINT_LIST_ID
+ );
+ expect(resp).toEqual({
+ entries: [expectedEndpointExceptions],
+ });
});
- });
- test('it should convert simple fields', async () => {
- const testEntries: EntriesArray = [
- { field: 'host.os.full', operator: 'included', type: 'match', value: 'windows' },
- { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' },
- { field: 'host.hostname', operator: 'included', type: 'match', value: 'estc' },
- ];
+ test('it should convert simple fields', async () => {
+ const testEntries: EntriesArray = [
+ { field: 'host.os.full', operator: 'included', type: 'match', value: 'windows' },
+ { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' },
+ { field: 'host.hostname', operator: 'included', type: 'match', value: 'estc' },
+ ];
- const expectedEndpointExceptions = {
- type: 'simple',
- entries: [
- {
- field: 'host.os.full',
- operator: 'included',
- type: 'exact_cased',
- value: 'windows',
- },
+ const expectedEndpointExceptions = {
+ type: 'simple',
+ entries: [
+ {
+ field: 'host.os.full',
+ operator: 'included',
+ type: 'exact_cased',
+ value: 'windows',
+ },
+ {
+ field: 'server.ip',
+ operator: 'included',
+ type: 'exact_cased',
+ value: '192.168.1.1',
+ },
+ {
+ field: 'host.hostname',
+ operator: 'included',
+ type: 'exact_cased',
+ value: 'estc',
+ },
+ ],
+ };
+
+ const first = getFoundExceptionListItemSchemaMock();
+ first.data[0].entries = testEntries;
+ mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
+
+ const resp = await getFilteredEndpointExceptionList(
+ mockExceptionClient,
+ 'v1',
+ TEST_FILTER,
+ ENDPOINT_LIST_ID
+ );
+ expect(resp).toEqual({
+ entries: [expectedEndpointExceptions],
+ });
+ });
+
+ test('it should convert fields case sensitive', async () => {
+ const testEntries: EntriesArray = [
+ { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' },
+ { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' },
{
- field: 'server.ip',
+ field: 'host.hostname.caseless',
operator: 'included',
- type: 'exact_cased',
- value: '192.168.1.1',
+ type: 'match_any',
+ value: ['estc', 'kibana'],
},
+ ];
+
+ const expectedEndpointExceptions = {
+ type: 'simple',
+ entries: [
+ {
+ field: 'host.os.full',
+ operator: 'included',
+ type: 'exact_caseless',
+ value: 'windows',
+ },
+ {
+ field: 'server.ip',
+ operator: 'included',
+ type: 'exact_cased',
+ value: '192.168.1.1',
+ },
+ {
+ field: 'host.hostname',
+ operator: 'included',
+ type: 'exact_caseless_any',
+ value: ['estc', 'kibana'],
+ },
+ ],
+ };
+
+ const first = getFoundExceptionListItemSchemaMock();
+ first.data[0].entries = testEntries;
+ mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
+
+ const resp = await getFilteredEndpointExceptionList(
+ mockExceptionClient,
+ 'v1',
+ TEST_FILTER,
+ ENDPOINT_LIST_ID
+ );
+ expect(resp).toEqual({
+ entries: [expectedEndpointExceptions],
+ });
+ });
+
+ test('it should deduplicate exception entries', async () => {
+ const testEntries: EntriesArray = [
+ { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' },
+ { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' },
+ { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' },
+ { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' },
{
field: 'host.hostname',
operator: 'included',
- type: 'exact_cased',
- value: 'estc',
+ type: 'match_any',
+ value: ['estc', 'kibana'],
},
- ],
- };
-
- const first = getFoundExceptionListItemSchemaMock();
- first.data[0].entries = testEntries;
- mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
-
- const resp = await getFullEndpointExceptionList(
- mockExceptionClient,
- 'linux',
- 'v1',
- ENDPOINT_LIST_ID
- );
- expect(resp).toEqual({
- entries: [expectedEndpointExceptions],
- });
- });
+ ];
- test('it should convert fields case sensitive', async () => {
- const testEntries: EntriesArray = [
- { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' },
- { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' },
- {
- field: 'host.hostname.caseless',
- operator: 'included',
- type: 'match_any',
- value: ['estc', 'kibana'],
- },
- ];
+ const expectedEndpointExceptions = {
+ type: 'simple',
+ entries: [
+ {
+ field: 'host.os.full',
+ operator: 'included',
+ type: 'exact_caseless',
+ value: 'windows',
+ },
+ {
+ field: 'server.ip',
+ operator: 'included',
+ type: 'exact_cased',
+ value: '192.168.1.1',
+ },
+ {
+ field: 'host.hostname',
+ operator: 'included',
+ type: 'exact_cased_any',
+ value: ['estc', 'kibana'],
+ },
+ ],
+ };
+
+ const first = getFoundExceptionListItemSchemaMock();
+ first.data[0].entries = testEntries;
+ mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
+
+ const resp = await getFilteredEndpointExceptionList(
+ mockExceptionClient,
+ 'v1',
+ TEST_FILTER,
+ ENDPOINT_LIST_ID
+ );
+ expect(resp).toEqual({
+ entries: [expectedEndpointExceptions],
+ });
+ });
- const expectedEndpointExceptions = {
- type: 'simple',
- entries: [
- {
- field: 'host.os.full',
- operator: 'included',
- type: 'exact_caseless',
- value: 'windows',
- },
- {
- field: 'server.ip',
- operator: 'included',
- type: 'exact_cased',
- value: '192.168.1.1',
- },
+ test('it should not deduplicate exception entries across nested boundaries', async () => {
+ const testEntries: EntriesArray = [
{
- field: 'host.hostname',
- operator: 'included',
- type: 'exact_caseless_any',
- value: ['estc', 'kibana'],
+ entries: [
+ { field: 'nested.field', operator: 'included', type: 'match', value: 'some value' },
+ ],
+ field: 'some.parentField',
+ type: 'nested',
},
- ],
- };
-
- const first = getFoundExceptionListItemSchemaMock();
- first.data[0].entries = testEntries;
- mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
-
- const resp = await getFullEndpointExceptionList(
- mockExceptionClient,
- 'linux',
- 'v1',
- ENDPOINT_LIST_ID
- );
- expect(resp).toEqual({
- entries: [expectedEndpointExceptions],
+ // Same as above but not inside the nest
+ { field: 'nested.field', operator: 'included', type: 'match', value: 'some value' },
+ ];
+
+ const expectedEndpointExceptions = {
+ type: 'simple',
+ entries: [
+ {
+ entries: [
+ {
+ field: 'nested.field',
+ operator: 'included',
+ type: 'exact_cased',
+ value: 'some value',
+ },
+ ],
+ field: 'some.parentField',
+ type: 'nested',
+ },
+ {
+ field: 'nested.field',
+ operator: 'included',
+ type: 'exact_cased',
+ value: 'some value',
+ },
+ ],
+ };
+
+ const first = getFoundExceptionListItemSchemaMock();
+ first.data[0].entries = testEntries;
+ mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
+
+ const resp = await getFilteredEndpointExceptionList(
+ mockExceptionClient,
+ 'v1',
+ TEST_FILTER,
+ ENDPOINT_LIST_ID
+ );
+ expect(resp).toEqual({
+ entries: [expectedEndpointExceptions],
+ });
});
- });
- test('it should deduplicate exception entries', async () => {
- const testEntries: EntriesArray = [
- { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' },
- { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' },
- { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' },
- { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' },
- {
- field: 'host.hostname',
- operator: 'included',
- type: 'match_any',
- value: ['estc', 'kibana'],
- },
- ];
+ test('it should deduplicate exception items', async () => {
+ const testEntries: EntriesArray = [
+ { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' },
+ { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' },
+ ];
- const expectedEndpointExceptions = {
- type: 'simple',
- entries: [
+ const expectedEndpointExceptions = {
+ type: 'simple',
+ entries: [
+ {
+ field: 'host.os.full',
+ operator: 'included',
+ type: 'exact_caseless',
+ value: 'windows',
+ },
+ {
+ field: 'server.ip',
+ operator: 'included',
+ type: 'exact_cased',
+ value: '192.168.1.1',
+ },
+ ],
+ };
+
+ const first = getFoundExceptionListItemSchemaMock();
+ first.data[0].entries = testEntries;
+
+ // Create a second exception item with the same entries
+ first.data[1] = getExceptionListItemSchemaMock();
+ first.data[1].entries = testEntries;
+ mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
+
+ const resp = await getFilteredEndpointExceptionList(
+ mockExceptionClient,
+ 'v1',
+ TEST_FILTER,
+ ENDPOINT_LIST_ID
+ );
+ expect(resp).toEqual({
+ entries: [expectedEndpointExceptions],
+ });
+ });
+
+ test('it should ignore unsupported entries', async () => {
+ // Lists and exists are not supported by the Endpoint
+ const testEntries: EntriesArray = [
+ { field: 'host.os.full', operator: 'included', type: 'match', value: 'windows' },
{
field: 'host.os.full',
operator: 'included',
- type: 'exact_caseless',
- value: 'windows',
- },
- {
- field: 'server.ip',
- operator: 'included',
- type: 'exact_cased',
- value: '192.168.1.1',
- },
- {
- field: 'host.hostname',
- operator: 'included',
- type: 'exact_cased_any',
- value: ['estc', 'kibana'],
- },
- ],
- };
-
- const first = getFoundExceptionListItemSchemaMock();
- first.data[0].entries = testEntries;
- mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
-
- const resp = await getFullEndpointExceptionList(
- mockExceptionClient,
- 'linux',
- 'v1',
- ENDPOINT_LIST_ID
- );
- expect(resp).toEqual({
- entries: [expectedEndpointExceptions],
- });
- });
+ type: 'list',
+ list: {
+ id: 'lists_not_supported',
+ type: 'keyword',
+ },
+ } as EntryList,
+ { field: 'server.ip', operator: 'included', type: 'exists' },
+ ];
- test('it should not deduplicate exception entries across nested boundaries', async () => {
- const testEntries: EntriesArray = [
- {
+ const expectedEndpointExceptions = {
+ type: 'simple',
entries: [
- { field: 'nested.field', operator: 'included', type: 'match', value: 'some value' },
+ {
+ field: 'host.os.full',
+ operator: 'included',
+ type: 'exact_cased',
+ value: 'windows',
+ },
],
- field: 'some.parentField',
- type: 'nested',
- },
- // Same as above but not inside the nest
- { field: 'nested.field', operator: 'included', type: 'match', value: 'some value' },
- ];
+ };
+
+ const first = getFoundExceptionListItemSchemaMock();
+ first.data[0].entries = testEntries;
+ mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
+
+ const resp = await getFilteredEndpointExceptionList(
+ mockExceptionClient,
+ 'v1',
+ TEST_FILTER,
+ ENDPOINT_LIST_ID
+ );
+ expect(resp).toEqual({
+ entries: [expectedEndpointExceptions],
+ });
+ });
+
+ test('it should convert the exception lists response to the proper endpoint format while paging', async () => {
+ // The first call returns two exceptions
+ const first = getFoundExceptionListItemSchemaMock();
+ first.per_page = 2;
+ first.total = 4;
+ first.data.push(getExceptionListItemSchemaMock());
+
+ // The second call returns two exceptions
+ const second = getFoundExceptionListItemSchemaMock();
+ second.per_page = 2;
+ second.total = 4;
+ second.data.push(getExceptionListItemSchemaMock());
+
+ mockExceptionClient.findExceptionListItem = jest
+ .fn()
+ .mockReturnValueOnce(first)
+ .mockReturnValueOnce(second);
+
+ const resp = await getFilteredEndpointExceptionList(
+ mockExceptionClient,
+ 'v1',
+ TEST_FILTER,
+ ENDPOINT_LIST_ID
+ );
+
+ // Expect 2 exceptions, the first two calls returned the same exception list items
+ expect(resp.entries.length).toEqual(2);
+ });
+
+ test('it should handle no exceptions', async () => {
+ const exceptionsResponse = getFoundExceptionListItemSchemaMock();
+ exceptionsResponse.data = [];
+ exceptionsResponse.total = 0;
+ mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(exceptionsResponse);
+ const resp = await getFilteredEndpointExceptionList(
+ mockExceptionClient,
+ 'v1',
+ TEST_FILTER,
+ ENDPOINT_LIST_ID
+ );
+ expect(resp.entries.length).toEqual(0);
+ });
- const expectedEndpointExceptions = {
- type: 'simple',
- entries: [
+ test('it should return a stable hash regardless of order of entries', async () => {
+ const translatedEntries: TranslatedEntry[] = [
{
entries: [
{
- field: 'nested.field',
+ field: 'some.nested.field',
operator: 'included',
type: 'exact_cased',
value: 'some value',
@@ -245,218 +420,107 @@ describe('buildEventTypeSignal', () => {
type: 'exact_cased',
value: 'some value',
},
- ],
- };
-
- const first = getFoundExceptionListItemSchemaMock();
- first.data[0].entries = testEntries;
- mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
-
- const resp = await getFullEndpointExceptionList(
- mockExceptionClient,
- 'linux',
- 'v1',
- ENDPOINT_LIST_ID
- );
- expect(resp).toEqual({
- entries: [expectedEndpointExceptions],
- });
- });
-
- test('it should deduplicate exception items', async () => {
- const testEntries: EntriesArray = [
- { field: 'host.os.full.caseless', operator: 'included', type: 'match', value: 'windows' },
- { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' },
- ];
-
- const expectedEndpointExceptions = {
- type: 'simple',
- entries: [
- {
- field: 'host.os.full',
- operator: 'included',
- type: 'exact_caseless',
- value: 'windows',
- },
- {
- field: 'server.ip',
- operator: 'included',
- type: 'exact_cased',
- value: '192.168.1.1',
- },
- ],
- };
-
- const first = getFoundExceptionListItemSchemaMock();
- first.data[0].entries = testEntries;
-
- // Create a second exception item with the same entries
- first.data[1] = getExceptionListItemSchemaMock();
- first.data[1].entries = testEntries;
- mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
-
- const resp = await getFullEndpointExceptionList(
- mockExceptionClient,
- 'linux',
- 'v1',
- ENDPOINT_LIST_ID
- );
- expect(resp).toEqual({
- entries: [expectedEndpointExceptions],
- });
- });
-
- test('it should ignore unsupported entries', async () => {
- // Lists and exists are not supported by the Endpoint
- const testEntries: EntriesArray = [
- { field: 'host.os.full', operator: 'included', type: 'match', value: 'windows' },
- {
- field: 'host.os.full',
- operator: 'included',
- type: 'list',
- list: {
- id: 'lists_not_supported',
- type: 'keyword',
- },
- } as EntryList,
- { field: 'server.ip', operator: 'included', type: 'exists' },
- ];
-
- const expectedEndpointExceptions = {
- type: 'simple',
- entries: [
- {
- field: 'host.os.full',
- operator: 'included',
- type: 'exact_cased',
- value: 'windows',
- },
- ],
- };
-
- const first = getFoundExceptionListItemSchemaMock();
- first.data[0].entries = testEntries;
- mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first);
-
- const resp = await getFullEndpointExceptionList(
- mockExceptionClient,
- 'linux',
- 'v1',
- ENDPOINT_LIST_ID
- );
- expect(resp).toEqual({
- entries: [expectedEndpointExceptions],
- });
- });
+ ];
+ const translatedEntriesReversed = translatedEntries.reverse();
- test('it should convert the exception lists response to the proper endpoint format while paging', async () => {
- // The first call returns two exceptions
- const first = getFoundExceptionListItemSchemaMock();
- first.per_page = 2;
- first.total = 4;
- first.data.push(getExceptionListItemSchemaMock());
-
- // The second call returns two exceptions
- const second = getFoundExceptionListItemSchemaMock();
- second.per_page = 2;
- second.total = 4;
- second.data.push(getExceptionListItemSchemaMock());
-
- mockExceptionClient.findExceptionListItem = jest
- .fn()
- .mockReturnValueOnce(first)
- .mockReturnValueOnce(second);
-
- const resp = await getFullEndpointExceptionList(
- mockExceptionClient,
- 'linux',
- 'v1',
- ENDPOINT_LIST_ID
- );
-
- // Expect 2 exceptions, the first two calls returned the same exception list items
- expect(resp.entries.length).toEqual(2);
- });
-
- test('it should handle no exceptions', async () => {
- const exceptionsResponse = getFoundExceptionListItemSchemaMock();
- exceptionsResponse.data = [];
- exceptionsResponse.total = 0;
- mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(exceptionsResponse);
- const resp = await getFullEndpointExceptionList(
- mockExceptionClient,
- 'linux',
- 'v1',
- ENDPOINT_LIST_ID
- );
- expect(resp.entries.length).toEqual(0);
- });
+ const translatedExceptionList = {
+ entries: [
+ {
+ type: 'simple',
+ entries: translatedEntries,
+ },
+ ],
+ };
- test('it should return a stable hash regardless of order of entries', async () => {
- const translatedEntries: TranslatedEntry[] = [
- {
+ const translatedExceptionListReversed = {
entries: [
{
- field: 'some.nested.field',
- operator: 'included',
- type: 'exact_cased',
- value: 'some value',
+ type: 'simple',
+ entries: translatedEntriesReversed,
},
],
- field: 'some.parentField',
- type: 'nested',
- },
- {
- field: 'nested.field',
- operator: 'included',
- type: 'exact_cased',
- value: 'some value',
- },
- ];
- const translatedEntriesReversed = translatedEntries.reverse();
+ };
+
+ const artifact1 = await buildArtifact(
+ translatedExceptionList,
+ 'v1',
+ 'linux',
+ ArtifactConstants.GLOBAL_ALLOWLIST_NAME
+ );
+ const artifact2 = await buildArtifact(
+ translatedExceptionListReversed,
+ 'v1',
+ 'linux',
+ ArtifactConstants.GLOBAL_ALLOWLIST_NAME
+ );
+ expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256);
+ });
- const translatedExceptionList = {
- entries: [
+ test('it should return a stable hash regardless of order of items', async () => {
+ const translatedItems: TranslatedExceptionListItem[] = [
{
type: 'simple',
- entries: translatedEntries,
+ entries: [
+ {
+ entries: [
+ {
+ field: 'some.nested.field',
+ operator: 'included',
+ type: 'exact_cased',
+ value: 'some value',
+ },
+ ],
+ field: 'some.parentField',
+ type: 'nested',
+ },
+ ],
},
- ],
- };
-
- const translatedExceptionListReversed = {
- entries: [
{
type: 'simple',
- entries: translatedEntriesReversed,
+ entries: [
+ {
+ field: 'nested.field',
+ operator: 'included',
+ type: 'exact_cased',
+ value: 'some value',
+ },
+ ],
},
- ],
- };
-
- const artifact1 = await buildArtifact(
- translatedExceptionList,
- 'linux',
- 'v1',
- ArtifactConstants.GLOBAL_ALLOWLIST_NAME
- );
- const artifact2 = await buildArtifact(
- translatedExceptionListReversed,
- 'linux',
- 'v1',
- ArtifactConstants.GLOBAL_ALLOWLIST_NAME
- );
- expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256);
+ ];
+
+ const translatedExceptionList = {
+ entries: translatedItems,
+ };
+
+ const translatedExceptionListReversed = {
+ entries: translatedItems.reverse(),
+ };
+
+ const artifact1 = await buildArtifact(
+ translatedExceptionList,
+ 'v1',
+ 'linux',
+ ArtifactConstants.GLOBAL_ALLOWLIST_NAME
+ );
+ const artifact2 = await buildArtifact(
+ translatedExceptionListReversed,
+ 'v1',
+ 'linux',
+ ArtifactConstants.GLOBAL_ALLOWLIST_NAME
+ );
+ expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256);
+ });
});
- test('it should return a stable hash regardless of order of items', async () => {
- const translatedItems: TranslatedExceptionListItem[] = [
+ const TEST_EXCEPTION_LIST_ITEM = {
+ entries: [
{
type: 'simple',
entries: [
{
entries: [
{
- field: 'some.nested.field',
+ field: 'nested.field',
operator: 'included',
type: 'exact_cased',
value: 'some value',
@@ -465,41 +529,87 @@ describe('buildEventTypeSignal', () => {
field: 'some.parentField',
type: 'nested',
},
- ],
- },
- {
- type: 'simple',
- entries: [
{
- field: 'nested.field',
+ field: 'some.not.nested.field',
operator: 'included',
type: 'exact_cased',
value: 'some value',
},
],
},
- ];
-
- const translatedExceptionList = {
- entries: translatedItems,
- };
-
- const translatedExceptionListReversed = {
- entries: translatedItems.reverse(),
- };
-
- const artifact1 = await buildArtifact(
- translatedExceptionList,
- 'linux',
- 'v1',
- ArtifactConstants.GLOBAL_ALLOWLIST_NAME
- );
- const artifact2 = await buildArtifact(
- translatedExceptionListReversed,
- 'linux',
- 'v1',
- ArtifactConstants.GLOBAL_ALLOWLIST_NAME
- );
- expect(artifact1.decodedSha256).toEqual(artifact2.decodedSha256);
+ ],
+ };
+
+ describe('getEndpointExceptionList', () => {
+ test('it should build proper kuery', async () => {
+ mockExceptionClient.findExceptionListItem = jest
+ .fn()
+ .mockReturnValueOnce(getFoundExceptionListItemSchemaMock());
+
+ const resp = await getEndpointExceptionList(mockExceptionClient, 'v1', 'windows');
+
+ expect(resp).toEqual(TEST_EXCEPTION_LIST_ITEM);
+
+ expect(mockExceptionClient.findExceptionListItem).toHaveBeenCalledWith({
+ listId: ENDPOINT_LIST_ID,
+ namespaceType: 'agnostic',
+ filter: 'exception-list-agnostic.attributes.os_types:"windows"',
+ perPage: 100,
+ page: 1,
+ sortField: 'created_at',
+ sortOrder: 'desc',
+ });
+ });
+ });
+
+ describe('getEndpointTrustedAppsList', () => {
+ test('it should build proper kuery without policy', async () => {
+ mockExceptionClient.findExceptionListItem = jest
+ .fn()
+ .mockReturnValueOnce(getFoundExceptionListItemSchemaMock());
+
+ const resp = await getEndpointTrustedAppsList(mockExceptionClient, 'v1', 'macos');
+
+ expect(resp).toEqual(TEST_EXCEPTION_LIST_ITEM);
+
+ expect(mockExceptionClient.findExceptionListItem).toHaveBeenCalledWith({
+ listId: ENDPOINT_TRUSTED_APPS_LIST_ID,
+ namespaceType: 'agnostic',
+ filter:
+ 'exception-list-agnostic.attributes.os_types:"macos" and (exception-list-agnostic.attributes.tags:"policy:all")',
+ perPage: 100,
+ page: 1,
+ sortField: 'created_at',
+ sortOrder: 'desc',
+ });
+ });
+
+ test('it should build proper kuery with policy', async () => {
+ mockExceptionClient.findExceptionListItem = jest
+ .fn()
+ .mockReturnValueOnce(getFoundExceptionListItemSchemaMock());
+
+ const resp = await getEndpointTrustedAppsList(
+ mockExceptionClient,
+ 'v1',
+ 'macos',
+ 'c6d16e42-c32d-4dce-8a88-113cfe276ad1'
+ );
+
+ expect(resp).toEqual(TEST_EXCEPTION_LIST_ITEM);
+
+ expect(mockExceptionClient.findExceptionListItem).toHaveBeenCalledWith({
+ listId: ENDPOINT_TRUSTED_APPS_LIST_ID,
+ namespaceType: 'agnostic',
+ filter:
+ 'exception-list-agnostic.attributes.os_types:"macos" and ' +
+ '(exception-list-agnostic.attributes.tags:"policy:all" or ' +
+ 'exception-list-agnostic.attributes.tags:"policy:c6d16e42-c32d-4dce-8a88-113cfe276ad1")',
+ perPage: 100,
+ page: 1,
+ sortField: 'created_at',
+ sortOrder: 'desc',
+ });
+ });
});
});
diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
index 6cc6a821eba334..322bb2ca47a45c 100644
--- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts
@@ -12,7 +12,7 @@ import { validate } from '../../../../common/validate';
import { Entry, EntryNested } from '../../../../../lists/common/schemas/types';
import { ExceptionListClient } from '../../../../../lists/server';
-import { ENDPOINT_LIST_ID } from '../../../../common/shared_imports';
+import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../common/shared_imports';
import {
InternalArtifactSchema,
TranslatedEntry,
@@ -28,12 +28,11 @@ import {
internalArtifactCompleteSchema,
InternalArtifactCompleteSchema,
} from '../../schemas';
-import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants';
export async function buildArtifact(
exceptions: WrappedTranslatedExceptionList,
- os: string,
schemaVersion: string,
+ os: string,
name: string
): Promise
{
const exceptionsBuffer = Buffer.from(JSON.stringify(exceptions));
@@ -74,10 +73,10 @@ export function isCompressed(artifact: InternalArtifactSchema) {
return artifact.compressionAlgorithm === 'zlib';
}
-export async function getFullEndpointExceptionList(
+export async function getFilteredEndpointExceptionList(
eClient: ExceptionListClient,
- os: string,
schemaVersion: string,
+ filter: string,
listId: typeof ENDPOINT_LIST_ID | typeof ENDPOINT_TRUSTED_APPS_LIST_ID
): Promise {
const exceptions: WrappedTranslatedExceptionList = { entries: [] };
@@ -88,7 +87,7 @@ export async function getFullEndpointExceptionList(
const response = await eClient.findExceptionListItem({
listId,
namespaceType: 'agnostic',
- filter: `exception-list-agnostic.attributes.os_types:\"${os}\"`,
+ filter,
perPage: 100,
page,
sortField: 'created_at',
@@ -114,6 +113,35 @@ export async function getFullEndpointExceptionList(
return validated as WrappedTranslatedExceptionList;
}
+export async function getEndpointExceptionList(
+ eClient: ExceptionListClient,
+ schemaVersion: string,
+ os: string
+): Promise {
+ const filter = `exception-list-agnostic.attributes.os_types:\"${os}\"`;
+
+ return getFilteredEndpointExceptionList(eClient, schemaVersion, filter, ENDPOINT_LIST_ID);
+}
+
+export async function getEndpointTrustedAppsList(
+ eClient: ExceptionListClient,
+ schemaVersion: string,
+ os: string,
+ policyId?: string
+): Promise {
+ const osFilter = `exception-list-agnostic.attributes.os_types:\"${os}\"`;
+ const policyFilter = `(exception-list-agnostic.attributes.tags:\"policy:all\"${
+ policyId ? ` or exception-list-agnostic.attributes.tags:\"policy:${policyId}\"` : ''
+ })`;
+
+ return getFilteredEndpointExceptionList(
+ eClient,
+ schemaVersion,
+ `${osFilter} and ${policyFilter}`,
+ ENDPOINT_TRUSTED_APPS_LIST_ID
+ );
+}
+
/**
* Translates Exception list items to Exceptions the endpoint can understand
* @param exceptions
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts
index 972c4f3153a1c8..b8b1e13f2052b5 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts
@@ -35,7 +35,7 @@ const createExceptionListItemOptions = (
name: '',
namespaceType: 'agnostic',
osTypes: [],
- tags: [],
+ tags: ['policy:all'],
type: 'simple',
...options,
});
@@ -56,7 +56,7 @@ const exceptionListItemSchema = (
name: '',
namespace_type: 'agnostic',
os_types: [],
- tags: [],
+ tags: ['policy:all'],
type: 'simple',
tie_breaker_id: '123',
updated_at: '11/11/2011T11:11:11.111',
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts
index 4d2238ea96ee15..41b4b7b1d55fdd 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts
@@ -15,7 +15,7 @@ import {
ExceptionListItemSchema,
NestedEntriesArray,
} from '../../../../../lists/common/shared_exports';
-import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants';
+import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common';
import { CreateExceptionListItemOptions } from '../../../../../lists/server';
import {
ConditionEntry,
@@ -184,7 +184,7 @@ export const newTrustedAppToCreateExceptionListItemOptions = ({
name,
namespaceType: 'agnostic',
osTypes: [OPERATING_SYSTEM_TO_OS_TYPE[os]],
- tags: [],
+ tags: ['policy:all'],
type: 'simple',
};
};
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts
index dc3c369494d4e2..97a8451bf25d83 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts
@@ -6,7 +6,7 @@
*/
import { ExceptionListClient } from '../../../../../lists/server';
-import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants';
+import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common';
import {
DeleteTrustedAppsRequestParams,
diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts
index dedbcc25e2373e..1975c2a92cc16f 100644
--- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts
@@ -36,8 +36,8 @@ export const getInternalArtifactMock = async (
): Promise => {
const artifact = await buildArtifact(
getTranslatedExceptionListMock(),
- os,
schemaVersion,
+ os,
artifactName
);
return opts?.compress ? compressArtifact(artifact) : artifact;
@@ -49,7 +49,7 @@ export const getEmptyInternalArtifactMock = async (
opts?: { compress: boolean },
artifactName: string = ArtifactConstants.GLOBAL_ALLOWLIST_NAME
): Promise => {
- const artifact = await buildArtifact({ entries: [] }, os, schemaVersion, artifactName);
+ const artifact = await buildArtifact({ entries: [] }, schemaVersion, os, artifactName);
return opts?.compress ? compressArtifact(artifact) : artifact;
};
@@ -62,8 +62,8 @@ export const getInternalArtifactMockWithDiffs = async (
mock.entries.pop();
const artifact = await buildArtifact(
mock,
- os,
schemaVersion,
+ os,
ArtifactConstants.GLOBAL_ALLOWLIST_NAME
);
return opts?.compress ? compressArtifact(artifact) : artifact;
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts
index a8bbfca0d41e58..b0e0d5d8ebfbea 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts
@@ -33,17 +33,24 @@ export const createExceptionListResponse = (data: ExceptionListItemSchema[], tot
type FindExceptionListItemOptions = Parameters[0];
-const FILTER_REGEXP = /^exception-list-agnostic\.attributes\.os_types:"(\w+)"$/;
+const FILTER_PROPERTY_PREFIX = 'exception-list-agnostic\\.attributes';
+const FILTER_REGEXP = new RegExp(
+ `^${FILTER_PROPERTY_PREFIX}\.os_types:"([^"]+)"( and \\(${FILTER_PROPERTY_PREFIX}\.tags:"policy:all"( or ${FILTER_PROPERTY_PREFIX}\.tags:"policy:([^"]+)")?\\))?$`
+);
export const mockFindExceptionListItemResponses = (
responses: Record>
) => {
return jest.fn().mockImplementation((options: FindExceptionListItemOptions) => {
- const os = FILTER_REGEXP.test(options.filter || '')
- ? options.filter!.match(FILTER_REGEXP)![1]
- : '';
-
- return createExceptionListResponse(responses[options.listId]?.[os] || []);
+ const matches = options.filter!.match(FILTER_REGEXP) || [];
+
+ if (matches[4] && responses[options.listId]?.[`${matches![1]}-${matches[4]}`]) {
+ return createExceptionListResponse(
+ responses[options.listId]?.[`${matches![1]}-${matches[4]}`] || []
+ );
+ } else {
+ return createExceptionListResponse(responses[options.listId]?.[matches![1] || ''] || []);
+ }
});
};
@@ -118,7 +125,7 @@ export const getManifestManagerMock = (
context.exceptionListClient.findExceptionListItem = jest
.fn()
.mockRejectedValue(new Error('unexpected thing happened'));
- return super.buildExceptionListArtifacts('v1');
+ return super.buildExceptionListArtifacts();
case ManifestManagerMockType.NormalFlow:
return getMockArtifactsWithDiff();
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts
index 52897f473189fb..26db49be459fa2 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts
@@ -8,8 +8,7 @@
import { inflateSync } from 'zlib';
import { SavedObjectsErrorHelpers } from 'src/core/server';
import { savedObjectsClientMock } from 'src/core/server/mocks';
-import { ENDPOINT_LIST_ID } from '../../../../../../lists/common';
-import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../../lists/common/constants';
+import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../../lists/common';
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { PackagePolicy } from '../../../../../../fleet/common/types/models';
import { getEmptyInternalArtifactMock } from '../../../schemas/artifacts/saved_objects.mock';
@@ -211,10 +210,19 @@ describe('ManifestManager', () => {
ARTIFACT_NAME_TRUSTED_APPS_LINUX,
];
- const getArtifactIds = (artifacts: InternalArtifactSchema[]) =>
- artifacts.map((artifact) => artifact.identifier);
+ const getArtifactIds = (artifacts: InternalArtifactSchema[]) => [
+ ...new Set(artifacts.map((artifact) => artifact.identifier)).values(),
+ ];
+
+ const mockPolicyListIdsResponse = (items: string[]) =>
+ jest.fn().mockResolvedValue({
+ items,
+ page: 1,
+ per_page: 100,
+ total: items.length,
+ });
- test('Fails when exception list list client fails', async () => {
+ test('Fails when exception list client fails', async () => {
const context = buildManifestManagerContextMock({});
const manifestManager = new ManifestManager(context);
@@ -228,6 +236,7 @@ describe('ManifestManager', () => {
const manifestManager = new ManifestManager(context);
context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({});
+ context.packagePolicyService.listIds = mockPolicyListIdsResponse([TEST_POLICY_ID_1]);
const manifest = await manifestManager.buildNewManifest();
@@ -237,11 +246,16 @@ describe('ManifestManager', () => {
const artifacts = manifest.getAllArtifacts();
+ expect(artifacts.length).toBe(5);
expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES);
expect(artifacts.every(isCompressed)).toBe(true);
for (const artifact of artifacts) {
expect(await uncompressArtifact(artifact)).toStrictEqual({ entries: [] });
+ expect(manifest.isDefaultArtifact(artifact)).toBe(true);
+ expect(manifest.getArtifactTargetPolicies(artifact)).toStrictEqual(
+ new Set([TEST_POLICY_ID_1])
+ );
}
});
@@ -255,6 +269,7 @@ describe('ManifestManager', () => {
[ENDPOINT_LIST_ID]: { macos: [exceptionListItem] },
[ENDPOINT_TRUSTED_APPS_LIST_ID]: { linux: [trustedAppListItem] },
});
+ context.packagePolicyService.listIds = mockPolicyListIdsResponse([TEST_POLICY_ID_1]);
const manifest = await manifestManager.buildNewManifest();
@@ -264,21 +279,25 @@ describe('ManifestManager', () => {
const artifacts = manifest.getAllArtifacts();
+ expect(artifacts.length).toBe(5);
expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES);
expect(artifacts.every(isCompressed)).toBe(true);
+ expect(await uncompressArtifact(artifacts[0])).toStrictEqual({
+ entries: translateToEndpointExceptions([exceptionListItem], 'v1'),
+ });
+ expect(await uncompressArtifact(artifacts[1])).toStrictEqual({ entries: [] });
+ expect(await uncompressArtifact(artifacts[2])).toStrictEqual({ entries: [] });
+ expect(await uncompressArtifact(artifacts[3])).toStrictEqual({ entries: [] });
+ expect(await uncompressArtifact(artifacts[4])).toStrictEqual({
+ entries: translateToEndpointExceptions([trustedAppListItem], 'v1'),
+ });
+
for (const artifact of artifacts) {
- if (artifact.identifier === ARTIFACT_NAME_EXCEPTIONS_MACOS) {
- expect(await uncompressArtifact(artifact)).toStrictEqual({
- entries: translateToEndpointExceptions([exceptionListItem], 'v1'),
- });
- } else if (artifact.identifier === 'endpoint-trustlist-linux-v1') {
- expect(await uncompressArtifact(artifact)).toStrictEqual({
- entries: translateToEndpointExceptions([trustedAppListItem], 'v1'),
- });
- } else {
- expect(await uncompressArtifact(artifact)).toStrictEqual({ entries: [] });
- }
+ expect(manifest.isDefaultArtifact(artifact)).toBe(true);
+ expect(manifest.getArtifactTargetPolicies(artifact)).toStrictEqual(
+ new Set([TEST_POLICY_ID_1])
+ );
}
});
@@ -291,6 +310,7 @@ describe('ManifestManager', () => {
context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({
[ENDPOINT_LIST_ID]: { macos: [exceptionListItem] },
});
+ context.packagePolicyService.listIds = mockPolicyListIdsResponse([TEST_POLICY_ID_1]);
const oldManifest = await manifestManager.buildNewManifest();
@@ -307,20 +327,89 @@ describe('ManifestManager', () => {
const artifacts = manifest.getAllArtifacts();
+ expect(artifacts.length).toBe(5);
expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES);
expect(artifacts.every(isCompressed)).toBe(true);
+ expect(artifacts[0]).toStrictEqual(oldManifest.getAllArtifacts()[0]);
+ expect(await uncompressArtifact(artifacts[1])).toStrictEqual({ entries: [] });
+ expect(await uncompressArtifact(artifacts[2])).toStrictEqual({ entries: [] });
+ expect(await uncompressArtifact(artifacts[3])).toStrictEqual({ entries: [] });
+ expect(await uncompressArtifact(artifacts[4])).toStrictEqual({
+ entries: translateToEndpointExceptions([trustedAppListItem], 'v1'),
+ });
+
for (const artifact of artifacts) {
- if (artifact.identifier === ARTIFACT_NAME_EXCEPTIONS_MACOS) {
- expect(artifact).toStrictEqual(oldManifest.getAllArtifacts()[0]);
- } else if (artifact.identifier === 'endpoint-trustlist-linux-v1') {
- expect(await uncompressArtifact(artifact)).toStrictEqual({
- entries: translateToEndpointExceptions([trustedAppListItem], 'v1'),
- });
- } else {
- expect(await uncompressArtifact(artifact)).toStrictEqual({ entries: [] });
- }
+ expect(manifest.isDefaultArtifact(artifact)).toBe(true);
+ expect(manifest.getArtifactTargetPolicies(artifact)).toStrictEqual(
+ new Set([TEST_POLICY_ID_1])
+ );
+ }
+ });
+
+ test('Builds manifest with policy specific exception list items for trusted apps', async () => {
+ const exceptionListItem = getExceptionListItemSchemaMock({ os_types: ['macos'] });
+ const trustedAppListItem = getExceptionListItemSchemaMock({ os_types: ['linux'] });
+ const trustedAppListItemPolicy2 = getExceptionListItemSchemaMock({
+ os_types: ['linux'],
+ entries: [
+ { field: 'other.field', operator: 'included', type: 'match', value: 'other value' },
+ ],
+ });
+ const context = buildManifestManagerContextMock({});
+ const manifestManager = new ManifestManager(context);
+
+ context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({
+ [ENDPOINT_LIST_ID]: { macos: [exceptionListItem] },
+ [ENDPOINT_TRUSTED_APPS_LIST_ID]: {
+ linux: [trustedAppListItem],
+ [`linux-${TEST_POLICY_ID_2}`]: [trustedAppListItem, trustedAppListItemPolicy2],
+ },
+ });
+ context.packagePolicyService.listIds = mockPolicyListIdsResponse([
+ TEST_POLICY_ID_1,
+ TEST_POLICY_ID_2,
+ ]);
+
+ const manifest = await manifestManager.buildNewManifest();
+
+ expect(manifest?.getSchemaVersion()).toStrictEqual('v1');
+ expect(manifest?.getSemanticVersion()).toStrictEqual('1.0.0');
+ expect(manifest?.getSavedObjectVersion()).toBeUndefined();
+
+ const artifacts = manifest.getAllArtifacts();
+
+ expect(artifacts.length).toBe(6);
+ expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES);
+ expect(artifacts.every(isCompressed)).toBe(true);
+
+ expect(await uncompressArtifact(artifacts[0])).toStrictEqual({
+ entries: translateToEndpointExceptions([exceptionListItem], 'v1'),
+ });
+ expect(await uncompressArtifact(artifacts[1])).toStrictEqual({ entries: [] });
+ expect(await uncompressArtifact(artifacts[2])).toStrictEqual({ entries: [] });
+ expect(await uncompressArtifact(artifacts[3])).toStrictEqual({ entries: [] });
+ expect(await uncompressArtifact(artifacts[4])).toStrictEqual({
+ entries: translateToEndpointExceptions([trustedAppListItem], 'v1'),
+ });
+ expect(await uncompressArtifact(artifacts[5])).toStrictEqual({
+ entries: translateToEndpointExceptions(
+ [trustedAppListItem, trustedAppListItemPolicy2],
+ 'v1'
+ ),
+ });
+
+ for (const artifact of artifacts.slice(0, 4)) {
+ expect(manifest.isDefaultArtifact(artifact)).toBe(true);
+ expect(manifest.getArtifactTargetPolicies(artifact)).toStrictEqual(
+ new Set([TEST_POLICY_ID_1, TEST_POLICY_ID_2])
+ );
}
+
+ expect(manifest.isDefaultArtifact(artifacts[5])).toBe(false);
+ expect(manifest.getArtifactTargetPolicies(artifacts[5])).toStrictEqual(
+ new Set([TEST_POLICY_ID_2])
+ );
});
});
diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
index 6b9cbb55415a01..f49f2a3e226eef 100644
--- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts
@@ -9,6 +9,7 @@ import semver from 'semver';
import LRU from 'lru-cache';
import { isEqual } from 'lodash';
import { Logger, SavedObjectsClientContract } from 'src/core/server';
+import { ListResult } from '../../../../../../fleet/common';
import { PackagePolicyServiceInterface } from '../../../../../../fleet/server';
import { ExceptionListClient } from '../../../../../../lists/server';
import { ManifestSchemaVersion } from '../../../../../common/endpoint/schema/common';
@@ -21,7 +22,8 @@ import {
ArtifactConstants,
buildArtifact,
getArtifactId,
- getFullEndpointExceptionList,
+ getEndpointExceptionList,
+ getEndpointTrustedAppsList,
isCompressed,
Manifest,
maybeCompressArtifact,
@@ -32,9 +34,45 @@ import {
} from '../../../schemas/artifacts';
import { ArtifactClient } from '../artifact_client';
import { ManifestClient } from '../manifest_client';
-import { ENDPOINT_LIST_ID } from '../../../../../../lists/common';
-import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../../lists/common/constants';
-import { PackagePolicy } from '../../../../../../fleet/common/types/models';
+
+interface ArtifactsBuildResult {
+ defaultArtifacts: InternalArtifactCompleteSchema[];
+ policySpecificArtifacts: Record;
+}
+
+const iterateArtifactsBuildResult = async (
+ result: ArtifactsBuildResult,
+ callback: (artifact: InternalArtifactCompleteSchema, policyId?: string) => Promise
+) => {
+ for (const artifact of result.defaultArtifacts) {
+ await callback(artifact);
+ }
+
+ for (const policyId of Object.keys(result.policySpecificArtifacts)) {
+ for (const artifact of result.policySpecificArtifacts[policyId]) {
+ await callback(artifact, policyId);
+ }
+ }
+};
+
+const iterateAllListItems = async (
+ pageSupplier: (page: number) => Promise>,
+ itemCallback: (item: T) => void
+) => {
+ let paging = true;
+ let page = 1;
+
+ while (paging) {
+ const { items, total } = await pageSupplier(page);
+
+ for (const item of items) {
+ await itemCallback(item);
+ }
+
+ paging = (page - 1) * 20 + items.length < total;
+ page++;
+ }
+};
export interface ManifestManagerContext {
savedObjectsClient: SavedObjectsClientContract;
@@ -81,6 +119,19 @@ export class ManifestManager {
return new ManifestClient(this.savedObjectsClient, this.schemaVersion);
}
+ /**
+ * Builds an artifact (one per supported OS) based on the current
+ * state of exception-list-agnostic SOs.
+ */
+ protected async buildExceptionListArtifact(os: string): Promise {
+ return buildArtifact(
+ await getEndpointExceptionList(this.exceptionListClient, this.schemaVersion, os),
+ this.schemaVersion,
+ os,
+ ArtifactConstants.GLOBAL_ALLOWLIST_NAME
+ );
+ }
+
/**
* Builds an array of artifacts (one per supported OS) based on the current
* state of exception-list-agnostic SOs.
@@ -88,54 +139,60 @@ export class ManifestManager {
* @returns {Promise} An array of uncompressed artifacts built from exception-list-agnostic SOs.
* @throws Throws/rejects if there are errors building the list.
*/
- protected async buildExceptionListArtifacts(
- artifactSchemaVersion?: string
- ): Promise {
- const artifacts: InternalArtifactCompleteSchema[] = [];
+ protected async buildExceptionListArtifacts(): Promise {
+ const defaultArtifacts: InternalArtifactCompleteSchema[] = [];
+ const policySpecificArtifacts: Record = {};
+
for (const os of ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS) {
- const exceptionList = await getFullEndpointExceptionList(
- this.exceptionListClient,
- os,
- artifactSchemaVersion ?? 'v1',
- ENDPOINT_LIST_ID
- );
- const artifact = await buildArtifact(
- exceptionList,
- os,
- artifactSchemaVersion ?? 'v1',
- ArtifactConstants.GLOBAL_ALLOWLIST_NAME
- );
- artifacts.push(artifact);
+ defaultArtifacts.push(await this.buildExceptionListArtifact(os));
}
- return artifacts;
+
+ await iterateAllListItems(
+ (page) => this.listEndpointPolicyIds(page),
+ async (policyId) => {
+ policySpecificArtifacts[policyId] = defaultArtifacts;
+ }
+ );
+
+ return { defaultArtifacts, policySpecificArtifacts };
+ }
+
+ /**
+ * Builds an artifact (one per supported OS) based on the current state of the
+ * Trusted Apps list (which uses the `exception-list-agnostic` SO type)
+ */
+ protected async buildTrustedAppsArtifact(os: string, policyId?: string) {
+ return buildArtifact(
+ await getEndpointTrustedAppsList(this.exceptionListClient, this.schemaVersion, os, policyId),
+ this.schemaVersion,
+ os,
+ ArtifactConstants.GLOBAL_TRUSTED_APPS_NAME
+ );
}
/**
* Builds an array of artifacts (one per supported OS) based on the current state of the
* Trusted Apps list (which uses the `exception-list-agnostic` SO type)
- * @param artifactSchemaVersion
*/
- protected async buildTrustedAppsArtifacts(
- artifactSchemaVersion?: string
- ): Promise {
- const artifacts: InternalArtifactCompleteSchema[] = [];
+ protected async buildTrustedAppsArtifacts(): Promise {
+ const defaultArtifacts: InternalArtifactCompleteSchema[] = [];
+ const policySpecificArtifacts: Record = {};
for (const os of ArtifactConstants.SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS) {
- const trustedApps = await getFullEndpointExceptionList(
- this.exceptionListClient,
- os,
- artifactSchemaVersion ?? 'v1',
- ENDPOINT_TRUSTED_APPS_LIST_ID
- );
- const artifact = await buildArtifact(
- trustedApps,
- os,
- 'v1',
- ArtifactConstants.GLOBAL_TRUSTED_APPS_NAME
- );
- artifacts.push(artifact);
+ defaultArtifacts.push(await this.buildTrustedAppsArtifact(os));
}
- return artifacts;
+
+ await iterateAllListItems(
+ (page) => this.listEndpointPolicyIds(page),
+ async (policyId) => {
+ for (const os of ArtifactConstants.SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS) {
+ policySpecificArtifacts[policyId] = policySpecificArtifacts[policyId] || [];
+ policySpecificArtifacts[policyId].push(await this.buildTrustedAppsArtifact(os, policyId));
+ }
+ }
+ );
+
+ return { defaultArtifacts, policySpecificArtifacts };
}
/**
@@ -251,32 +308,33 @@ export class ManifestManager {
public async buildNewManifest(
baselineManifest: Manifest = Manifest.getDefault(this.schemaVersion)
): Promise {
- // Build new exception list artifacts
- const artifacts = (
- await Promise.all([this.buildExceptionListArtifacts(), this.buildTrustedAppsArtifacts()])
- ).flat();
+ const results = await Promise.all([
+ this.buildExceptionListArtifacts(),
+ this.buildTrustedAppsArtifacts(),
+ ]);
- // Build new manifest
const manifest = new Manifest({
schemaVersion: this.schemaVersion,
semanticVersion: baselineManifest.getSemanticVersion(),
soVersion: baselineManifest.getSavedObjectVersion(),
});
- for (const artifact of artifacts) {
- let artifactToAdd = baselineManifest.getArtifact(getArtifactId(artifact)) || artifact;
-
- if (!isCompressed(artifactToAdd)) {
- artifactToAdd = await maybeCompressArtifact(artifactToAdd);
+ for (const result of results) {
+ await iterateArtifactsBuildResult(result, async (artifact, policyId) => {
+ let artifactToAdd = baselineManifest.getArtifact(getArtifactId(artifact)) || artifact;
if (!isCompressed(artifactToAdd)) {
- throw new Error(`Unable to compress artifact: ${getArtifactId(artifactToAdd)}`);
- } else if (!internalArtifactCompleteSchema.is(artifactToAdd)) {
- throw new Error(`Incomplete artifact detected: ${getArtifactId(artifactToAdd)}`);
+ artifactToAdd = await maybeCompressArtifact(artifactToAdd);
+
+ if (!isCompressed(artifactToAdd)) {
+ throw new Error(`Unable to compress artifact: ${getArtifactId(artifactToAdd)}`);
+ } else if (!internalArtifactCompleteSchema.is(artifactToAdd)) {
+ throw new Error(`Incomplete artifact detected: ${getArtifactId(artifactToAdd)}`);
+ }
}
- }
- manifest.addEntry(artifactToAdd);
+ manifest.addEntry(artifactToAdd, policyId);
+ });
}
return manifest;
@@ -292,49 +350,52 @@ export class ManifestManager {
public async tryDispatch(manifest: Manifest): Promise {
const errors: Error[] = [];
- await this.forEachPolicy(async (packagePolicy) => {
- // eslint-disable-next-line @typescript-eslint/naming-convention
- const { id, revision, updated_at, updated_by, ...newPackagePolicy } = packagePolicy;
- if (newPackagePolicy.inputs.length > 0 && newPackagePolicy.inputs[0].config !== undefined) {
- const oldManifest = newPackagePolicy.inputs[0].config.artifact_manifest ?? {
- value: {},
- };
-
- const newManifestVersion = manifest.getSemanticVersion();
- if (semver.gt(newManifestVersion, oldManifest.value.manifest_version)) {
- const serializedManifest = manifest.toPackagePolicyManifest(packagePolicy.id);
-
- if (!manifestDispatchSchema.is(serializedManifest)) {
- errors.push(new Error(`Invalid manifest for policy ${packagePolicy.id}`));
- } else if (!manifestsEqual(serializedManifest, oldManifest.value)) {
- newPackagePolicy.inputs[0].config.artifact_manifest = { value: serializedManifest };
-
- try {
- await this.packagePolicyService.update(
- this.savedObjectsClient,
- // @ts-ignore
- undefined,
- id,
- newPackagePolicy
- );
+ await iterateAllListItems(
+ (page) => this.listEndpointPolicies(page),
+ async (packagePolicy) => {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ const { id, revision, updated_at, updated_by, ...newPackagePolicy } = packagePolicy;
+ if (newPackagePolicy.inputs.length > 0 && newPackagePolicy.inputs[0].config !== undefined) {
+ const oldManifest = newPackagePolicy.inputs[0].config.artifact_manifest ?? {
+ value: {},
+ };
+
+ const newManifestVersion = manifest.getSemanticVersion();
+ if (semver.gt(newManifestVersion, oldManifest.value.manifest_version)) {
+ const serializedManifest = manifest.toPackagePolicyManifest(packagePolicy.id);
+
+ if (!manifestDispatchSchema.is(serializedManifest)) {
+ errors.push(new Error(`Invalid manifest for policy ${packagePolicy.id}`));
+ } else if (!manifestsEqual(serializedManifest, oldManifest.value)) {
+ newPackagePolicy.inputs[0].config.artifact_manifest = { value: serializedManifest };
+
+ try {
+ await this.packagePolicyService.update(
+ this.savedObjectsClient,
+ // @ts-ignore
+ undefined,
+ id,
+ newPackagePolicy
+ );
+ this.logger.debug(
+ `Updated package policy ${id} with manifest version ${manifest.getSemanticVersion()}`
+ );
+ } catch (err) {
+ errors.push(err);
+ }
+ } else {
this.logger.debug(
- `Updated package policy ${id} with manifest version ${manifest.getSemanticVersion()}`
+ `No change in manifest content for package policy: ${id}. Staying on old version`
);
- } catch (err) {
- errors.push(err);
}
} else {
- this.logger.debug(
- `No change in manifest content for package policy: ${id}. Staying on old version`
- );
+ this.logger.debug(`No change in manifest version for package policy: ${id}`);
}
} else {
- this.logger.debug(`No change in manifest version for package policy: ${id}`);
+ errors.push(new Error(`Package Policy ${id} has no config.`));
}
- } else {
- errors.push(new Error(`Package Policy ${id} has no config.`));
}
- });
+ );
return errors;
}
@@ -363,23 +424,19 @@ export class ManifestManager {
this.logger.info(`Committed manifest ${manifest.getSemanticVersion()}`);
}
- private async forEachPolicy(callback: (policy: PackagePolicy) => Promise) {
- let paging = true;
- let page = 1;
-
- while (paging) {
- const { items, total } = await this.packagePolicyService.list(this.savedObjectsClient, {
- page,
- perPage: 20,
- kuery: 'ingest-package-policies.package.name:endpoint',
- });
-
- for (const packagePolicy of items) {
- await callback(packagePolicy);
- }
+ private async listEndpointPolicies(page: number) {
+ return this.packagePolicyService.list(this.savedObjectsClient, {
+ page,
+ perPage: 20,
+ kuery: 'ingest-package-policies.package.name:endpoint',
+ });
+ }
- paging = (page - 1) * 20 + items.length < total;
- page++;
- }
+ private async listEndpointPolicyIds(page: number) {
+ return this.packagePolicyService.listIds(this.savedObjectsClient, {
+ page,
+ perPage: 20,
+ kuery: 'ingest-package-policies.package.name:endpoint',
+ });
}
}
diff --git a/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts
index f3d553936bac53..c3a5c4e3b23cf1 100644
--- a/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts
+++ b/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts
@@ -50,6 +50,7 @@ export const hostsSchema = gql`
success
failure
warning
+ unsupported
}
type EndpointFields {
diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts
index d1e646e73cf12c..0d6a0e63455b00 100644
--- a/x-pack/plugins/security_solution/server/graphql/types.ts
+++ b/x-pack/plugins/security_solution/server/graphql/types.ts
@@ -275,6 +275,7 @@ export enum HostPolicyResponseActionStatus {
success = 'success',
failure = 'failure',
warning = 'warning',
+ unsupported = 'unsupported',
}
export enum TimelineType {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts
index dc6f7d35a6395f..cf6ea572aa8561 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts
@@ -393,6 +393,7 @@ export const getResult = (): RuleAlertType => ({
threatMapping: undefined,
threatLanguage: undefined,
threatIndex: undefined,
+ threatIndicatorPath: undefined,
threatQuery: undefined,
references: ['http://www.example.com', 'https://ww.example.com'],
note: '# Investigative notes',
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts
index fb62a8bc6a14b3..27231ab896b7e4 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts
@@ -174,6 +174,7 @@ export const importRulesRoute = (
threat_query: threatQuery,
threat_mapping: threatMapping,
threat_language: threatLanguage,
+ threat_indicator_path: threatIndicatorPath,
concurrent_searches: concurrentSearches,
items_per_search: itemsPerSearch,
threshold,
@@ -239,6 +240,7 @@ export const importRulesRoute = (
threshold,
threatFilters,
threatIndex,
+ threatIndicatorPath,
threatQuery,
threatMapping,
threatLanguage,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts
index defff7235dcba0..45665c61ea3f04 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts
@@ -150,6 +150,7 @@ export const transformAlertToRule = (
threshold: alert.params.threshold,
threat_filters: alert.params.threatFilters,
threat_index: alert.params.threatIndex,
+ threat_indicator_path: alert.params.threatIndicatorPath,
threat_query: alert.params.threatQuery,
threat_mapping: alert.params.threatMapping,
threat_language: alert.params.threatLanguage,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts
index e36b7b3079eb56..1232971c7baf83 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts
@@ -48,6 +48,7 @@ export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({
itemsPerSearch: undefined,
threatQuery: undefined,
threatIndex: undefined,
+ threatIndicatorPath: undefined,
threshold: undefined,
timestampOverride: undefined,
to: 'now',
@@ -94,6 +95,7 @@ export const getCreateMlRulesOptionsMock = (): CreateRulesOptions => ({
threat: [],
threatFilters: undefined,
threatIndex: undefined,
+ threatIndicatorPath: undefined,
threatMapping: undefined,
threatQuery: undefined,
threatLanguage: undefined,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
index 9726df176e93bc..3683cd377e672d 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
@@ -47,6 +47,7 @@ export const createRules = async ({
threat,
threatFilters,
threatIndex,
+ threatIndicatorPath,
threatLanguage,
concurrentSearches,
itemsPerSearch,
@@ -102,6 +103,7 @@ export const createRules = async ({
*/
threatFilters: threatFilters as PartialFilter[] | undefined,
threatIndex,
+ threatIndicatorPath,
threatQuery,
concurrentSearches,
itemsPerSearch,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts
index cd1935ef50c10a..0d046bb6ab211e 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts
@@ -56,6 +56,7 @@ export const installPrepackagedRules = (
items_per_search: itemsPerSearch,
threat_query: threatQuery,
threat_index: threatIndex,
+ threat_indicator_path: threatIndicatorPath,
threshold,
timestamp_override: timestampOverride,
references,
@@ -110,6 +111,7 @@ export const installPrepackagedRules = (
itemsPerSearch,
threatQuery,
threatIndex,
+ threatIndicatorPath,
threshold,
timestampOverride,
references,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts
index 07eb665c8cbd62..22c7dcc3a86169 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts
@@ -119,6 +119,7 @@ const rule: SanitizedAlert = {
threshold: undefined,
threatFilters: undefined,
threatIndex: undefined,
+ threatIndicatorPath: undefined,
threatQuery: undefined,
threatMapping: undefined,
threatLanguage: undefined,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
index 72798c35339820..e8be32111a0e10 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
@@ -96,6 +96,7 @@ import {
ThreatLanguageOrUndefined,
ConcurrentSearchesOrUndefined,
ItemsPerSearchOrUndefined,
+ ThreatIndicatorPathOrUndefined,
} from '../../../../common/detection_engine/schemas/types/threat_mapping';
import { AlertsClient, PartialAlert } from '../../../../../alerts/server';
@@ -238,6 +239,7 @@ export interface CreateRulesOptions {
threshold: ThresholdOrUndefined;
threatFilters: ThreatFiltersOrUndefined;
threatIndex: ThreatIndexOrUndefined;
+ threatIndicatorPath: ThreatIndicatorPathOrUndefined;
threatQuery: ThreatQueryOrUndefined;
threatMapping: ThreatMappingOrUndefined;
concurrentSearches: ConcurrentSearchesOrUndefined;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts
index 691ac818100a2e..e9a75af14310e9 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts
@@ -53,6 +53,7 @@ export const typeSpecificSnakeToCamel = (params: CreateTypeSpecific): TypeSpecif
threatMapping: params.threat_mapping,
threatLanguage: params.threat_language,
threatIndex: params.threat_index,
+ threatIndicatorPath: params.threat_indicator_path,
concurrentSearches: params.concurrent_searches,
itemsPerSearch: params.items_per_search,
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts
index f31d6af1a0d7a3..abbcfcaa791075 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_schemas.ts
@@ -14,6 +14,7 @@ import {
threat_query,
concurrentSearchesOrUndefined,
itemsPerSearchOrUndefined,
+ threatIndicatorPathOrUndefined,
} from '../../../../common/detection_engine/schemas/types/threat_mapping';
import {
authorOrUndefined,
@@ -116,6 +117,7 @@ const threatSpecificRuleParams = t.type({
threatMapping: threat_mapping,
threatLanguage: t.union([nonEqlLanguages, t.undefined]),
threatIndex: threat_index,
+ threatIndicatorPath: threatIndicatorPathOrUndefined,
concurrentSearches: concurrentSearchesOrUndefined,
itemsPerSearch: itemsPerSearchOrUndefined,
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
index 6011c67376973c..6177fc4cd46614 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
@@ -59,6 +59,7 @@ export const sampleRuleAlertParams = (
threatQuery: undefined,
threatMapping: undefined,
threatIndex: undefined,
+ threatIndicatorPath: undefined,
threatLanguage: undefined,
timelineId: undefined,
timelineTitle: undefined,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts
index 5586d9e19f7c1b..8f3fda800d7266 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts
@@ -170,6 +170,7 @@ export const buildRuleWithoutOverrides = (
threat_query: ruleParams.threatQuery,
threat_mapping: ruleParams.threatMapping,
threat_language: ruleParams.threatLanguage,
+ threat_indicator_path: ruleParams.threatIndicatorPath,
};
return removeInternalTagsFromRule(rule);
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts
index 04f2b6ff799da9..5893b05a1d811b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.mock.ts
@@ -23,7 +23,7 @@ export const ruleStatusServiceFactoryMock = async ({
success: jest.fn(),
- partialFailure: jest.fn(),
+ warning: jest.fn(),
error: jest.fn(),
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts
index f60591422e1ee6..7f2962ae0a6c89 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts
@@ -54,13 +54,13 @@ describe('buildRuleStatusAttributes', () => {
expect(result.statusDate).toEqual(result.lastSuccessAt);
});
- it('returns partial failure fields if "partial failure"', () => {
+ it('returns warning fields if "warning"', () => {
const result = buildRuleStatusAttributes(
- 'partial failure',
+ 'warning',
'some indices missing timestamp override field'
);
expect(result).toEqual({
- status: 'partial failure',
+ status: 'warning',
statusDate: expectIsoDateString,
lastSuccessAt: expectIsoDateString,
lastSuccessMessage: 'some indices missing timestamp override field',
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts
index f4abf9aa5ced8a..6e93ed256321e4 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts
@@ -24,7 +24,7 @@ interface Attributes {
export interface RuleStatusService {
goingToRun: () => Promise;
success: (message: string, attributes?: Attributes) => Promise;
- partialFailure: (message: string, attributes?: Attributes) => Promise;
+ warning: (message: string, attributes?: Attributes) => Promise;
error: (message: string, attributes?: Attributes) => Promise;
}
@@ -48,7 +48,7 @@ export const buildRuleStatusAttributes: (
lastSuccessMessage: message,
};
}
- case 'partial failure': {
+ case 'warning': {
return {
...baseAttributes,
lastSuccessAt: now,
@@ -102,7 +102,7 @@ export const ruleStatusServiceFactory = async ({
});
},
- partialFailure: async (message, attributes) => {
+ warning: async (message, attributes) => {
const [currentStatus] = await getOrCreateRuleStatuses({
alertId,
ruleStatusClient,
@@ -110,7 +110,7 @@ export const ruleStatusServiceFactory = async ({
await ruleStatusClient.update(currentStatus.id, {
...currentStatus.attributes,
- ...buildRuleStatusAttributes('partial failure', message, attributes),
+ ...buildRuleStatusAttributes('warning', message, attributes),
});
},
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts
index 2599f7db49f572..da7ee8796afbfc 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts
@@ -52,6 +52,7 @@ export const signalSchema = schema.object({
exceptionsList: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))),
threatFilters: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))),
threatIndex: schema.maybe(schema.arrayOf(schema.string())),
+ threatIndicatorPath: schema.maybe(schema.string()),
threatQuery: schema.maybe(schema.string()),
threatMapping: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))),
threatLanguage: schema.maybe(schema.string()),
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
index 02a0582e540f45..a79961eb716fdd 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
@@ -110,7 +110,7 @@ describe('rules_notification_alert_type', () => {
find: jest.fn(),
goingToRun: jest.fn(),
error: jest.fn(),
- partialFailure: jest.fn(),
+ warning: jest.fn(),
};
(ruleStatusServiceFactory as jest.Mock).mockReturnValue(ruleStatusService);
(getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(0));
@@ -207,7 +207,7 @@ describe('rules_notification_alert_type', () => {
});
});
- it('should set a partial failure for when rules cannot read ALL provided indices', async () => {
+ it('should set a warning for when rules cannot read ALL provided indices', async () => {
(checkPrivileges as jest.Mock).mockResolvedValueOnce({
username: 'elastic',
has_all_requested: false,
@@ -227,8 +227,8 @@ describe('rules_notification_alert_type', () => {
});
payload.params.index = ['some*', 'myfa*', 'anotherindex*'];
await alert.executor(payload);
- expect(ruleStatusService.partialFailure).toHaveBeenCalled();
- expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain(
+ expect(ruleStatusService.warning).toHaveBeenCalled();
+ expect(ruleStatusService.warning.mock.calls[0][0]).toContain(
'Missing required read privileges on the following indices: ["some*"]'
);
});
@@ -250,8 +250,8 @@ describe('rules_notification_alert_type', () => {
});
payload.params.index = ['some*', 'myfa*'];
await alert.executor(payload);
- expect(ruleStatusService.partialFailure).toHaveBeenCalled();
- expect(ruleStatusService.partialFailure.mock.calls[0][0]).toContain(
+ expect(ruleStatusService.warning).toHaveBeenCalled();
+ expect(ruleStatusService.warning.mock.calls[0][0]).toContain(
'This rule may not have the required read privileges to the following indices: ["myfa*","some*"]'
);
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
index d7fce9d83a490b..ecb36a8b050d98 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
@@ -137,6 +137,7 @@ export const signalRulesAlertType = ({
threatFilters,
threatQuery,
threatIndex,
+ threatIndicatorPath,
threatMapping,
threatLanguage,
timestampOverride,
@@ -180,7 +181,7 @@ export const signalRulesAlertType = ({
logger.debug(buildRuleMessage('[+] Starting Signal Rule execution'));
logger.debug(buildRuleMessage(`interval: ${interval}`));
- let wrotePartialFailureStatus = false;
+ let wroteWarningStatus = false;
await ruleStatusService.goingToRun();
// check if rule has permissions to access given index pattern
@@ -201,7 +202,7 @@ export const signalRulesAlertType = ({
}),
]);
- wrotePartialFailureStatus = await flow(
+ wroteWarningStatus = await flow(
() =>
tryCatch(
() =>
@@ -508,6 +509,7 @@ export const signalRulesAlertType = ({
threatLanguage,
buildRuleMessage,
threatIndex,
+ threatIndicatorPath,
concurrentSearches: concurrentSearches ?? 1,
itemsPerSearch: itemsPerSearch ?? 9000,
});
@@ -657,7 +659,7 @@ export const signalRulesAlertType = ({
`[+] Finished indexing ${result.createdSignalsCount} signals into ${outputIndex}`
)
);
- if (!hasError && !wrotePartialFailureStatus) {
+ if (!hasError && !wroteWarningStatus) {
await ruleStatusService.success('succeeded', {
bulkCreateTimeDurations: result.bulkCreateTimes,
searchAfterTimeDurations: result.searchAfterTimes,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts
index b14d1482189388..4f38f2db9230a9 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts
@@ -10,6 +10,8 @@ import { enrichSignalThreatMatches } from './enrich_signal_threat_matches';
import { getThreatList } from './get_threat_list';
import { BuildThreatEnrichmentOptions, GetMatchedThreats } from './types';
+const DEFAULT_INDICATOR_PATH = 'threat.indicator';
+
export const buildThreatEnrichment = ({
buildRuleMessage,
exceptionItems,
@@ -18,6 +20,7 @@ export const buildThreatEnrichment = ({
services,
threatFilters,
threatIndex,
+ threatIndicatorPath,
threatLanguage,
threatQuery,
}: BuildThreatEnrichmentOptions): SignalsEnrichment => {
@@ -50,6 +53,7 @@ export const buildThreatEnrichment = ({
return threatResponse.hits.hits;
};
+ const defaultedIndicatorPath = threatIndicatorPath ? threatIndicatorPath : DEFAULT_INDICATOR_PATH;
return (signals: SignalSearchResponse): Promise =>
- enrichSignalThreatMatches(signals, getMatchedThreats);
+ enrichSignalThreatMatches(signals, getMatchedThreats, defaultedIndicatorPath);
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts
index 7690eb5eb1d554..e45aea29c423f0 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts
@@ -48,6 +48,7 @@ export const createThreatSignals = async ({
threatLanguage,
buildRuleMessage,
threatIndex,
+ threatIndicatorPath,
name,
concurrentSearches,
itemsPerSearch,
@@ -99,6 +100,7 @@ export const createThreatSignals = async ({
services,
threatFilters,
threatIndex,
+ threatIndicatorPath,
threatLanguage,
threatQuery,
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts
index 3c0765b56ae20b..fada3141168711 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts
@@ -93,6 +93,7 @@ describe('buildMatchedIndicator', () => {
const indicators = buildMatchedIndicator({
queries: [],
threats,
+ indicatorPath: 'threat.indicator',
});
expect(indicators).toEqual([]);
@@ -102,6 +103,7 @@ describe('buildMatchedIndicator', () => {
const [indicator] = buildMatchedIndicator({
queries,
threats,
+ indicatorPath: 'threat.indicator',
});
expect(get(indicator, 'matched.atomic')).toEqual('domain_1');
@@ -111,6 +113,7 @@ describe('buildMatchedIndicator', () => {
const [indicator] = buildMatchedIndicator({
queries,
threats,
+ indicatorPath: 'threat.indicator',
});
expect(get(indicator, 'matched.field')).toEqual('event.field');
@@ -120,6 +123,7 @@ describe('buildMatchedIndicator', () => {
const [indicator] = buildMatchedIndicator({
queries,
threats,
+ indicatorPath: 'threat.indicator',
});
expect(get(indicator, 'matched.type')).toEqual('type_1');
@@ -148,6 +152,7 @@ describe('buildMatchedIndicator', () => {
const indicators = buildMatchedIndicator({
queries,
threats,
+ indicatorPath: 'threat.indicator',
});
expect(indicators).toHaveLength(queries.length);
@@ -157,6 +162,7 @@ describe('buildMatchedIndicator', () => {
const indicators = buildMatchedIndicator({
queries,
threats,
+ indicatorPath: 'threat.indicator',
});
expect(indicators).toEqual([
@@ -192,9 +198,9 @@ describe('buildMatchedIndicator', () => {
];
const indicators = buildMatchedIndicator({
- indicatorPath: 'custom.indicator.path',
queries,
threats,
+ indicatorPath: 'custom.indicator.path',
});
expect(indicators).toEqual([
@@ -221,6 +227,7 @@ describe('buildMatchedIndicator', () => {
const indicators = buildMatchedIndicator({
queries,
threats,
+ indicatorPath: 'threat.indicator',
});
expect(indicators).toEqual([
@@ -245,6 +252,7 @@ describe('buildMatchedIndicator', () => {
const indicators = buildMatchedIndicator({
queries,
threats,
+ indicatorPath: 'threat.indicator',
});
expect(indicators).toEqual([
@@ -276,6 +284,7 @@ describe('buildMatchedIndicator', () => {
const indicators = buildMatchedIndicator({
queries,
threats,
+ indicatorPath: 'threat.indicator',
});
expect(indicators).toEqual([
@@ -307,6 +316,7 @@ describe('buildMatchedIndicator', () => {
buildMatchedIndicator({
queries,
threats,
+ indicatorPath: 'threat.indicator',
})
).toThrowError('Expected indicator field to be an object, but found: not an object');
});
@@ -327,6 +337,7 @@ describe('buildMatchedIndicator', () => {
buildMatchedIndicator({
queries,
threats,
+ indicatorPath: 'threat.indicator',
})
).toThrowError('Expected indicator field to be an object, but found: not an object');
});
@@ -352,7 +363,11 @@ describe('enrichSignalThreatMatches', () => {
it('performs no enrichment if there are no signals', async () => {
const signals = getSignalsResponseMock([]);
- const enrichedSignals = await enrichSignalThreatMatches(signals, getMatchedThreats);
+ const enrichedSignals = await enrichSignalThreatMatches(
+ signals,
+ getMatchedThreats,
+ 'threat.indicator'
+ );
expect(enrichedSignals.hits.hits).toEqual([]);
});
@@ -363,7 +378,11 @@ describe('enrichSignalThreatMatches', () => {
matched_queries: [matchedQuery],
});
const signals = getSignalsResponseMock([signalHit]);
- const enrichedSignals = await enrichSignalThreatMatches(signals, getMatchedThreats);
+ const enrichedSignals = await enrichSignalThreatMatches(
+ signals,
+ getMatchedThreats,
+ 'threat.indicator'
+ );
const [enrichedHit] = enrichedSignals.hits.hits;
const indicators = get(enrichedHit._source, 'threat.indicator');
@@ -384,7 +403,11 @@ describe('enrichSignalThreatMatches', () => {
matched_queries: [matchedQuery],
});
const signals = getSignalsResponseMock([signalHit]);
- const enrichedSignals = await enrichSignalThreatMatches(signals, getMatchedThreats);
+ const enrichedSignals = await enrichSignalThreatMatches(
+ signals,
+ getMatchedThreats,
+ 'threat.indicator'
+ );
const [enrichedHit] = enrichedSignals.hits.hits;
const indicators = get(enrichedHit._source, 'threat.indicator');
@@ -401,7 +424,11 @@ describe('enrichSignalThreatMatches', () => {
matched_queries: [matchedQuery],
});
const signals = getSignalsResponseMock([signalHit]);
- const enrichedSignals = await enrichSignalThreatMatches(signals, getMatchedThreats);
+ const enrichedSignals = await enrichSignalThreatMatches(
+ signals,
+ getMatchedThreats,
+ 'threat.indicator'
+ );
const [enrichedHit] = enrichedSignals.hits.hits;
const indicators = get(enrichedHit._source, 'threat.indicator');
@@ -422,9 +449,53 @@ describe('enrichSignalThreatMatches', () => {
matched_queries: [matchedQuery],
});
const signals = getSignalsResponseMock([signalHit]);
- await expect(() => enrichSignalThreatMatches(signals, getMatchedThreats)).rejects.toThrowError(
- 'Expected threat field to be an object, but found: whoops'
+ await expect(() =>
+ enrichSignalThreatMatches(signals, getMatchedThreats, 'threat.indicator')
+ ).rejects.toThrowError('Expected threat field to be an object, but found: whoops');
+ });
+
+ it('enriches from a configured indicator path, if specified', async () => {
+ getMatchedThreats = async () => [
+ getThreatListItemMock({
+ _id: '123',
+ _source: {
+ custom_threat: {
+ custom_indicator: {
+ domain: 'custom_domain',
+ other: 'custom_other',
+ type: 'custom_type',
+ },
+ },
+ },
+ }),
+ ];
+ matchedQuery = encodeThreatMatchNamedQuery(
+ getNamedQueryMock({
+ id: '123',
+ field: 'event.field',
+ value: 'custom_threat.custom_indicator.domain',
+ })
+ );
+ const signalHit = getSignalHitMock({
+ matched_queries: [matchedQuery],
+ });
+ const signals = getSignalsResponseMock([signalHit]);
+ const enrichedSignals = await enrichSignalThreatMatches(
+ signals,
+ getMatchedThreats,
+ 'custom_threat.custom_indicator'
);
+ const [enrichedHit] = enrichedSignals.hits.hits;
+ const indicators = get(enrichedHit._source, 'threat.indicator');
+
+ expect(indicators).toEqual([
+ {
+ domain: 'custom_domain',
+ matched: { atomic: 'custom_domain', field: 'event.field', type: 'custom_type' },
+ other: 'custom_other',
+ type: 'custom_type',
+ },
+ ]);
});
it('merges duplicate matched signals into a single signal with multiple indicators', async () => {
@@ -455,7 +526,11 @@ describe('enrichSignalThreatMatches', () => {
],
});
const signals = getSignalsResponseMock([signalHit, otherSignalHit]);
- const enrichedSignals = await enrichSignalThreatMatches(signals, getMatchedThreats);
+ const enrichedSignals = await enrichSignalThreatMatches(
+ signals,
+ getMatchedThreats,
+ 'threat.indicator'
+ );
expect(enrichedSignals.hits.total).toEqual(expect.objectContaining({ value: 1 }));
expect(enrichedSignals.hits.hits).toHaveLength(1);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts
index c298ef98ebcd53..c5b032207f1c5a 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts
@@ -16,7 +16,6 @@ import type {
} from './types';
import { extractNamedQueries } from './utils';
-const DEFAULT_INDICATOR_PATH = 'threat.indicator';
const getSignalId = (signal: SignalSourceHit): string => signal._id;
export const groupAndMergeSignalMatches = (signalHits: SignalSourceHit[]): SignalSourceHit[] => {
@@ -43,11 +42,11 @@ export const groupAndMergeSignalMatches = (signalHits: SignalSourceHit[]): Signa
export const buildMatchedIndicator = ({
queries,
threats,
- indicatorPath = DEFAULT_INDICATOR_PATH,
+ indicatorPath,
}: {
queries: ThreatMatchNamedQuery[];
threats: ThreatListItem[];
- indicatorPath?: string;
+ indicatorPath: string;
}): ThreatIndicator[] =>
queries.map((query) => {
const matchedThreat = threats.find((threat) => threat._id === query.id);
@@ -67,7 +66,8 @@ export const buildMatchedIndicator = ({
export const enrichSignalThreatMatches = async (
signals: SignalSearchResponse,
- getMatchedThreats: GetMatchedThreats
+ getMatchedThreats: GetMatchedThreats,
+ indicatorPath: string
): Promise => {
const signalHits = signals.hits.hits;
if (signalHits.length === 0) {
@@ -79,7 +79,11 @@ export const enrichSignalThreatMatches = async (
const matchedThreatIds = [...new Set(signalMatches.flat().map(({ id }) => id))];
const matchedThreats = await getMatchedThreats(matchedThreatIds);
const matchedIndicators = signalMatches.map((queries) =>
- buildMatchedIndicator({ queries, threats: matchedThreats })
+ buildMatchedIndicator({
+ indicatorPath,
+ queries,
+ threats: matchedThreats,
+ })
);
const enrichedSignals: SignalSourceHit[] = uniqueHits.map((signalHit, i) => {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts
index b80d3faf9b61c7..a022cbbdd40428 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts
@@ -21,6 +21,7 @@ import {
ThreatLanguageOrUndefined,
ConcurrentSearches,
ItemsPerSearch,
+ ThreatIndicatorPathOrUndefined,
} from '../../../../../common/detection_engine/schemas/types/threat_mapping';
import { PartialFilter, RuleTypeParams } from '../../types';
import {
@@ -70,6 +71,7 @@ export interface CreateThreatSignalsOptions {
threatQuery: ThreatQuery;
buildRuleMessage: BuildRuleMessage;
threatIndex: ThreatIndex;
+ threatIndicatorPath: ThreatIndicatorPathOrUndefined;
threatLanguage: ThreatLanguageOrUndefined;
name: string;
concurrentSearches: ConcurrentSearches;
@@ -214,6 +216,7 @@ export interface BuildThreatEnrichmentOptions {
services: AlertServices;
threatFilters: PartialFilter[];
threatIndex: ThreatIndex;
+ threatIndicatorPath: ThreatIndicatorPathOrUndefined;
threatLanguage: ThreatLanguageOrUndefined;
threatQuery: ThreatQuery;
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts
index 75bd9f593a6ac1..f7e1eb7622779c 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts
@@ -69,7 +69,7 @@ const ruleStatusServiceMock = {
find: jest.fn(),
goingToRun: jest.fn(),
error: jest.fn(),
- partialFailure: jest.fn(),
+ warning: jest.fn(),
};
describe('utils', () => {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts
index 72e5bc0c5b879b..f6bd5c8a325be1 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts
@@ -79,12 +79,12 @@ export const hasReadIndexPrivileges = async (
if (indexesWithReadPrivileges.length > 0 && indexesWithNoReadPrivileges.length > 0) {
// some indices have read privileges others do not.
- // set a partial failure status
+ // set a warning status
const errorString = `Missing required read privileges on the following indices: ${JSON.stringify(
indexesWithNoReadPrivileges
)}`;
logger.error(buildRuleMessage(errorString));
- await ruleStatusService.partialFailure(errorString);
+ await ruleStatusService.warning(errorString);
return true;
} else if (
indexesWithReadPrivileges.length === 0 &&
@@ -96,7 +96,7 @@ export const hasReadIndexPrivileges = async (
indexesWithNoReadPrivileges
)}`;
logger.error(buildRuleMessage(errorString));
- await ruleStatusService.partialFailure(errorString);
+ await ruleStatusService.warning(errorString);
return true;
}
return false;
@@ -119,7 +119,7 @@ export const hasTimestampFields = async (
inputIndices
)}`;
logger.error(buildRuleMessage(errorString));
- await ruleStatusService.error(errorString);
+ await ruleStatusService.warning(errorString);
return true;
} else if (
!wroteStatus &&
@@ -128,7 +128,7 @@ export const hasTimestampFields = async (
timestampFieldCapsResponse.body.fields[timestampField]?.unmapped?.indices != null)
) {
// if there is a timestamp override and the unmapped array for the timestamp override key is not empty,
- // partial failure
+ // warning
const errorString = `The following indices are missing the ${
timestampField === '@timestamp'
? 'timestamp field "@timestamp"'
@@ -139,7 +139,7 @@ export const hasTimestampFields = async (
: timestampFieldCapsResponse.body.fields[timestampField].unmapped.indices
)}`;
logger.error(buildRuleMessage(errorString));
- await ruleStatusService.partialFailure(errorString);
+ await ruleStatusService.warning(errorString);
return true;
}
return wroteStatus;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts
index 4b59fcddcb51ff..a8721d82285f25 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts
@@ -47,6 +47,7 @@ import {
ThreatLanguageOrUndefined,
ConcurrentSearchesOrUndefined,
ItemsPerSearchOrUndefined,
+ ThreatIndicatorPathOrUndefined,
} from '../../../common/detection_engine/schemas/types/threat_mapping';
import { LegacyCallAPIOptions } from '../../../../../../src/core/server';
@@ -88,6 +89,7 @@ export interface RuleTypeParams extends AlertTypeParams {
threshold: ThresholdOrUndefined;
threatFilters: PartialFilter[] | undefined;
threatIndex: ThreatIndexOrUndefined;
+ threatIndicatorPath: ThreatIndicatorPathOrUndefined;
threatQuery: ThreatQueryOrUndefined;
threatMapping: ThreatMappingOrUndefined;
threatLanguage: ThreatLanguageOrUndefined;
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_delete_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_delete_provider.tsx
index 0638d3349206d6..792538a730ebe8 100644
--- a/x-pack/plugins/snapshot_restore/public/application/components/policy_delete_provider.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_delete_provider.tsx
@@ -7,7 +7,7 @@
import React, { Fragment, useRef, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
+import { EuiConfirmModal } from '@elastic/eui';
import { useServices, useToastNotifications } from '../app_context';
import { deletePolicies } from '../services/http';
@@ -96,58 +96,56 @@ export const PolicyDeleteProvider: React.FunctionComponent = ({ children
const isSingle = policyNames.length === 1;
return (
-
-
- ) : (
-
- )
- }
- onCancel={closeModal}
- onConfirm={deletePolicy}
- cancelButtonText={
+
- }
- confirmButtonText={
+ ) : (
- }
- buttonColor="danger"
- data-test-subj="srdeletePolicyConfirmationModal"
- >
- {!isSingle ? (
-
-
-
-
-
- {policyNames.map((name) => (
- - {name}
- ))}
-
-
- ) : null}
-
-
+ )
+ }
+ onCancel={closeModal}
+ onConfirm={deletePolicy}
+ cancelButtonText={
+
+ }
+ confirmButtonText={
+
+ }
+ buttonColor="danger"
+ data-test-subj="srdeletePolicyConfirmationModal"
+ >
+ {!isSingle ? (
+
+
+
+
+
+ {policyNames.map((name) => (
+ - {name}
+ ))}
+
+
+ ) : null}
+
);
};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_execute_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_execute_provider.tsx
index 3fcf5a35b34551..5636ca651b6285 100644
--- a/x-pack/plugins/snapshot_restore/public/application/components/policy_execute_provider.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_execute_provider.tsx
@@ -7,7 +7,7 @@
import React, { Fragment, useRef, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
+import { EuiConfirmModal } from '@elastic/eui';
import { useServices, useToastNotifications } from '../app_context';
import { executePolicy as executePolicyRequest } from '../services/http';
@@ -81,32 +81,30 @@ export const PolicyExecuteProvider: React.FunctionComponent = ({ children
}
return (
-
-
- }
- onCancel={closeModal}
- onConfirm={executePolicy}
- cancelButtonText={
-
- }
- confirmButtonText={
-
- }
- data-test-subj="srExecutePolicyConfirmationModal"
- />
-
+
+ }
+ onCancel={closeModal}
+ onConfirm={executePolicy}
+ cancelButtonText={
+
+ }
+ confirmButtonText={
+
+ }
+ data-test-subj="srExecutePolicyConfirmationModal"
+ />
);
};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/repository_delete_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/components/repository_delete_provider.tsx
index 3009413541111c..f02f160958a203 100644
--- a/x-pack/plugins/snapshot_restore/public/application/components/repository_delete_provider.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/components/repository_delete_provider.tsx
@@ -7,7 +7,7 @@
import React, { Fragment, useRef, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
+import { EuiConfirmModal } from '@elastic/eui';
import { Repository } from '../../../common/types';
import { useServices, useToastNotifications } from '../app_context';
@@ -109,79 +109,77 @@ export const RepositoryDeleteProvider: React.FunctionComponent = ({ child
const isSingle = repositoryNames.length === 1;
return (
-
-
- ) : (
-
- )
- }
- onCancel={closeModal}
- onConfirm={deleteRepository}
- cancelButtonText={
+
- }
- confirmButtonText={
- isSingle ? (
-
- ) : (
+ ) : (
+
+ )
+ }
+ onCancel={closeModal}
+ onConfirm={deleteRepository}
+ cancelButtonText={
+
+ }
+ confirmButtonText={
+ isSingle ? (
+
+ ) : (
+
+ )
+ }
+ buttonColor="danger"
+ data-test-subj="deleteRepositoryConfirmation"
+ >
+ {isSingle ? (
+
+
+
+ ) : (
+
+
- )
- }
- buttonColor="danger"
- data-test-subj="deleteRepositoryConfirmation"
- >
- {isSingle ? (
+
+
+ {repositoryNames.map((name) => (
+ - {name}
+ ))}
+
- ) : (
-
-
-
-
-
- {repositoryNames.map((name) => (
- - {name}
- ))}
-
-
-
-
-
- )}
-
-
+
+ )}
+
);
};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/retention_execute_modal_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/components/retention_execute_modal_provider.tsx
index 9366815a0256e2..4ce1d93955952f 100644
--- a/x-pack/plugins/snapshot_restore/public/application/components/retention_execute_modal_provider.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/components/retention_execute_modal_provider.tsx
@@ -7,7 +7,7 @@
import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
+import { EuiConfirmModal } from '@elastic/eui';
import { useServices, useToastNotifications } from '../app_context';
import { executeRetention as executeRetentionRequest } from '../services/http';
@@ -58,31 +58,29 @@ export const RetentionExecuteModalProvider: React.FunctionComponent = ({
}
return (
-
-
- }
- onCancel={closeModal}
- onConfirm={executeRetention}
- cancelButtonText={
-
- }
- confirmButtonText={
-
- }
- data-test-subj="executeRetentionModal"
- />
-
+
+ }
+ onCancel={closeModal}
+ onConfirm={executeRetention}
+ cancelButtonText={
+
+ }
+ confirmButtonText={
+
+ }
+ data-test-subj="executeRetentionModal"
+ />
);
};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx
index d8916ce9858f80..73e19eee8bf7a7 100644
--- a/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/components/retention_update_modal_provider.tsx
@@ -8,7 +8,6 @@
import React, { Fragment, useRef, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
- EuiOverlayMask,
EuiModal,
EuiModalHeader,
EuiModalHeaderTitle,
@@ -129,165 +128,161 @@ export const RetentionSettingsUpdateModalProvider: React.FunctionComponent
-
-
-
- {isEditing ? (
-
- ) : (
-
- )}
-
-
-
-
- {saveError && (
-
-
- }
- color="danger"
- iconType="alert"
- >
- {saveError.data && saveError.data.message ? (
- {saveError.data.message}
- ) : null}
-
-
-
+
+
+
+ {isEditing ? (
+
+ ) : (
+
)}
- {isAdvancedCronVisible ? (
-
-
- }
- isInvalid={isInvalid}
- error={i18n.translate(
- 'xpack.snapshotRestore.policyForm.stepRetention.policyUpdateRetentionScheduleFieldErrorMessage',
- {
- defaultMessage: 'Retention schedule is required.',
- }
- )}
- helpText={
-
-
-
- ),
- }}
- />
+
+
+
+
+ {saveError && (
+
+
+ }
+ color="danger"
+ iconType="alert"
+ >
+ {saveError.data && saveError.data.message ? {saveError.data.message}
: null}
+
+
+
+ )}
+ {isAdvancedCronVisible ? (
+
+
+ }
+ isInvalid={isInvalid}
+ error={i18n.translate(
+ 'xpack.snapshotRestore.policyForm.stepRetention.policyUpdateRetentionScheduleFieldErrorMessage',
+ {
+ defaultMessage: 'Retention schedule is required.',
}
- fullWidth
- >
- setRetentionSchedule(e.target.value)}
+ )}
+ helpText={
+
+
+
+ ),
+ }}
/>
-
+ }
+ fullWidth
+ >
+ setRetentionSchedule(e.target.value)}
+ />
+
-
+
-
- {
- setIsAdvancedCronVisible(false);
- setRetentionSchedule(simpleCron.expression);
- }}
- data-test-subj="showBasicCronLink"
- >
-
-
-
-
- ) : (
-
- {
- setSimpleCron({
- expression,
- frequency,
- });
- setFieldToPreferredValueMap(newFieldToPreferredValueMap);
- setRetentionSchedule(expression);
+
+ {
+ setIsAdvancedCronVisible(false);
+ setRetentionSchedule(simpleCron.expression);
}}
- />
-
-
+ data-test-subj="showBasicCronLink"
+ >
+
+
+
+
+ ) : (
+
+ {
+ setSimpleCron({
+ expression,
+ frequency,
+ });
+ setFieldToPreferredValueMap(newFieldToPreferredValueMap);
+ setRetentionSchedule(expression);
+ }}
+ />
-
- {
- setIsAdvancedCronVisible(true);
- }}
- data-test-subj="showAdvancedCronLink"
- >
-
-
-
-
- )}
-
+
-
-
+
+ {
+ setIsAdvancedCronVisible(true);
+ }}
+ data-test-subj="showAdvancedCronLink"
+ >
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ {isEditing ? (
-
-
-
- {isEditing ? (
-
- ) : (
-
- )}
-
-
-
-
+ ) : (
+
+ )}
+
+
+
);
};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/snapshot_delete_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/components/snapshot_delete_provider.tsx
index 40af1b07a50bc1..74614efb314aae 100644
--- a/x-pack/plugins/snapshot_restore/public/application/components/snapshot_delete_provider.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/components/snapshot_delete_provider.tsx
@@ -9,7 +9,6 @@ import React, { Fragment, useRef, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiConfirmModal,
- EuiOverlayMask,
EuiCallOut,
EuiLoadingSpinner,
EuiFlexGroup,
@@ -118,95 +117,93 @@ export const SnapshotDeleteProvider: React.FunctionComponent = ({ childre
const isSingle = snapshotIds.length === 1;
return (
-
-
- ) : (
-
- )
- }
- onCancel={closeModal}
- onConfirm={deleteSnapshot}
- cancelButtonText={
+
- }
- confirmButtonText={
+ ) : (
- }
- confirmButtonDisabled={isDeleting}
- buttonColor="danger"
- data-test-subj="srdeleteSnapshotConfirmationModal"
- >
- {!isSingle ? (
-
+ )
+ }
+ onCancel={closeModal}
+ onConfirm={deleteSnapshot}
+ cancelButtonText={
+
+ }
+ confirmButtonText={
+
+ }
+ confirmButtonDisabled={isDeleting}
+ buttonColor="danger"
+ data-test-subj="srdeleteSnapshotConfirmationModal"
+ >
+ {!isSingle ? (
+
+
+
+
+
+ {snapshotIds.map(({ snapshot, repository }) => (
+ - {snapshot}
+ ))}
+
+
+ ) : null}
+
+
+
+ {!isSingle && isDeleting ? (
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
-
- {snapshotIds.map(({ snapshot, repository }) => (
- - {snapshot}
- ))}
-
-
- ) : null}
-
-
-
- {!isSingle && isDeleting ? (
-
-
-
-
-
-
-
-
-
-
-
- }
- >
-
-
-
-
-
- ) : null}
-
-
+
+
+ ) : null}
+
);
};
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx
index e7bdde2984d6f4..823ce3a122ef12 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx
@@ -281,7 +281,7 @@ export const RepositoryDetails: React.FunctionComponent = ({
{verification ? (
-
+
{JSON.stringify(
verification.valid ? verification.response : verification.error,
null,
@@ -350,7 +350,7 @@ export const RepositoryDetails: React.FunctionComponent = ({
/>
-
+
{JSON.stringify(cleanup.response, null, 2)}
diff --git a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap
index b0d0933614d125..5bf93a1021c054 100644
--- a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap
+++ b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap
@@ -1,95 +1,93 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConfirmDeleteModal renders as expected 1`] = `
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ ,
}
}
/>
-
-
-
-
-
-
-
- ,
- }
- }
- />
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
`;
diff --git a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx
index c57bc1cef8fbee..94a5c082834ad7 100644
--- a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx
+++ b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx
@@ -20,7 +20,6 @@ import {
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalProps,
- EuiOverlayMask,
EuiSpacer,
EuiText,
} from '@elastic/eui';
@@ -97,88 +96,86 @@ class ConfirmDeleteModalUI extends Component
{
};
return (
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ ),
}}
/>
-
-
-
-
-
-
-
-
- ),
- }}
- />
-
-
-
-
-
-
- {warning}
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
+
+
+ {warning}
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap b/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap
index 750afcfc44e7e3..3eb92de017927b 100644
--- a/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap
+++ b/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap
@@ -1,28 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConfirmAlterActiveSpaceModal renders as expected 1`] = `
-
-
- }
- >
-
-
-
-
-
+
+ }
+>
+
+
+
+
`;
diff --git a/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx b/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx
index 1839fbdfdda7da..c95bb7250a23e1 100644
--- a/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx
+++ b/x-pack/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
+import { EuiConfirmModal } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
@@ -16,34 +16,32 @@ interface Props {
}
const ConfirmAlterActiveSpaceModalUI: React.FC = (props) => (
-
-
- }
- defaultFocusedButton={'confirm'}
- cancelButtonText={props.intl.formatMessage({
- id: 'xpack.spaces.management.confirmAlterActiveSpaceModal.cancelButton',
- defaultMessage: 'Cancel',
- })}
- confirmButtonText={props.intl.formatMessage({
- id: 'xpack.spaces.management.confirmAlterActiveSpaceModal.updateSpaceButton',
- defaultMessage: 'Update space',
- })}
- >
-
-
-
-
-
+
+ }
+ defaultFocusedButton={'confirm'}
+ cancelButtonText={props.intl.formatMessage({
+ id: 'xpack.spaces.management.confirmAlterActiveSpaceModal.cancelButton',
+ defaultMessage: 'Cancel',
+ })}
+ confirmButtonText={props.intl.formatMessage({
+ id: 'xpack.spaces.management.confirmAlterActiveSpaceModal.updateSpaceButton',
+ defaultMessage: 'Update space',
+ })}
+ >
+
+
+
+
);
export const ConfirmAlterActiveSpaceModal = injectI18n(ConfirmAlterActiveSpaceModalUI);
diff --git a/x-pack/plugins/transform/common/api_schemas/transforms.ts b/x-pack/plugins/transform/common/api_schemas/transforms.ts
index face319f141db5..3d8d7ef4d8ae3a 100644
--- a/x-pack/plugins/transform/common/api_schemas/transforms.ts
+++ b/x-pack/plugins/transform/common/api_schemas/transforms.ts
@@ -51,13 +51,43 @@ export type PivotConfig = TypeOf;
export type LatestFunctionConfig = TypeOf;
+export const retentionPolicySchema = schema.object({
+ time: schema.object({
+ field: schema.string(),
+ max_age: schema.string(),
+ }),
+});
+
export const settingsSchema = schema.object({
max_page_search_size: schema.maybe(schema.number()),
// The default value is null, which disables throttling.
docs_per_second: schema.maybe(schema.nullable(schema.number())),
});
+export const runtimeMappingsSchema = schema.maybe(
+ schema.recordOf(
+ schema.string(),
+ schema.object({
+ type: schema.oneOf([
+ schema.literal('keyword'),
+ schema.literal('long'),
+ schema.literal('double'),
+ schema.literal('date'),
+ schema.literal('ip'),
+ schema.literal('boolean'),
+ ]),
+ script: schema.oneOf([
+ schema.string(),
+ schema.object({
+ source: schema.string(),
+ }),
+ ]),
+ })
+ )
+);
+
export const sourceSchema = schema.object({
+ runtime_mappings: runtimeMappingsSchema,
index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]),
query: schema.maybe(schema.recordOf(schema.string(), schema.any())),
});
@@ -94,6 +124,7 @@ export const putTransformsRequestSchema = schema.object(
* Latest and pivot are mutually exclusive, i.e. exactly one must be specified in the transform configuration
*/
latest: schema.maybe(latestFunctionSchema),
+ retention_policy: schema.maybe(retentionPolicySchema),
settings: schema.maybe(settingsSchema),
source: sourceSchema,
sync: schema.maybe(syncSchema),
diff --git a/x-pack/plugins/transform/common/api_schemas/update_transforms.ts b/x-pack/plugins/transform/common/api_schemas/update_transforms.ts
index 4ff9780be1f5d9..9bd4df51080490 100644
--- a/x-pack/plugins/transform/common/api_schemas/update_transforms.ts
+++ b/x-pack/plugins/transform/common/api_schemas/update_transforms.ts
@@ -9,7 +9,7 @@ import { schema, TypeOf } from '@kbn/config-schema';
import { TransformPivotConfig } from '../types/transform';
-import { settingsSchema, sourceSchema, syncSchema } from './transforms';
+import { retentionPolicySchema, settingsSchema, sourceSchema, syncSchema } from './transforms';
// POST _transform/{transform_id}/_update
export const postTransformsUpdateRequestSchema = schema.object({
@@ -22,6 +22,7 @@ export const postTransformsUpdateRequestSchema = schema.object({
})
),
frequency: schema.maybe(schema.string()),
+ retention_policy: schema.maybe(retentionPolicySchema),
settings: schema.maybe(settingsSchema),
source: schema.maybe(sourceSchema),
sync: schema.maybe(syncSchema),
diff --git a/x-pack/plugins/transform/common/types/transform_stats.ts b/x-pack/plugins/transform/common/types/transform_stats.ts
index d280f4ce3505c7..f3b7000a424dbb 100644
--- a/x-pack/plugins/transform/common/types/transform_stats.ts
+++ b/x-pack/plugins/transform/common/types/transform_stats.ts
@@ -33,6 +33,8 @@ export interface TransformStats {
attributes: Record;
};
stats: {
+ delete_time_in_ms: number;
+ documents_deleted: number;
documents_indexed: number;
documents_processed: number;
index_failures: number;
diff --git a/x-pack/plugins/transform/public/app/common/pivot_group_by.ts b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts
index 18b0c7dde819f2..4f23b8aa4d86fd 100644
--- a/x-pack/plugins/transform/public/app/common/pivot_group_by.ts
+++ b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts
@@ -10,6 +10,7 @@ import { Dictionary } from '../../../common/types/common';
import { EsFieldName } from '../../../common/types/fields';
import { GenericAgg } from '../../../common/types/pivot_group_by';
import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common';
+import { PivotAggsConfigWithUiSupport } from './pivot_aggs';
export enum PIVOT_SUPPORTED_GROUP_BY_AGGS {
DATE_HISTOGRAM = 'date_histogram',
@@ -117,3 +118,7 @@ export function getEsAggFromGroupByConfig(groupByConfig: GroupByConfigBase): Gen
[agg]: esAgg,
};
}
+
+export function isPivotAggConfigWithUiSupport(arg: any): arg is PivotAggsConfigWithUiSupport {
+ return arg.hasOwnProperty('agg') && arg.hasOwnProperty('field');
+}
diff --git a/x-pack/plugins/transform/public/app/common/request.test.ts b/x-pack/plugins/transform/public/app/common/request.test.ts
index 778b2c24325f62..13e7c0a9feb7a8 100644
--- a/x-pack/plugins/transform/public/app/common/request.test.ts
+++ b/x-pack/plugins/transform/public/app/common/request.test.ts
@@ -10,7 +10,7 @@ import { PIVOT_SUPPORTED_AGGS } from '../../../common/types/pivot_aggs';
import { PivotGroupByConfig } from '../common';
import { StepDefineExposedState } from '../sections/create_transform/components/step_define';
-import { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form';
+import { StepDetailsExposedState } from '../sections/create_transform/components/step_details';
import { PIVOT_SUPPORTED_GROUP_BY_AGGS } from './pivot_group_by';
import { PivotAggsConfig } from './pivot_aggs';
@@ -27,6 +27,7 @@ import {
PivotQuery,
} from './request';
import { LatestFunctionConfigUI } from '../../../common/types/transform';
+import { RuntimeField } from '../../../../../../src/plugins/data/common/index_patterns';
const simpleQuery: PivotQuery = { query_string: { query: 'airline:AAL' } };
@@ -168,12 +169,18 @@ describe('Transform: Common', () => {
validationStatus: {
isValid: true,
},
+ runtimeMappings: undefined,
+ runtimeMappingsUpdated: false,
+ isRuntimeMappingsEditorEnabled: false,
};
const transformDetailsState: StepDetailsExposedState = {
continuousModeDateField: 'the-continuous-mode-date-field',
continuousModeDelay: 'the-continuous-mode-delay',
createIndexPattern: false,
isContinuousModeEnabled: false,
+ isRetentionPolicyEnabled: false,
+ retentionPolicyDateField: '',
+ retentionPolicyMaxAge: '',
transformId: 'the-transform-id',
transformDescription: 'the-transform-description',
transformFrequency: '1m',
@@ -209,6 +216,85 @@ describe('Transform: Common', () => {
});
});
+ test('getCreateTransformRequestBody() with runtime mappings', () => {
+ const runtimeMappings = {
+ rt_bytes_bigger: {
+ type: 'double',
+ script: {
+ source: "emit(doc['bytes'].value * 2.0)",
+ },
+ } as RuntimeField,
+ };
+
+ const pivotState: StepDefineExposedState = {
+ aggList: { 'the-agg-name': aggsAvg },
+ groupByList: { 'the-group-by-name': groupByTerms },
+ isAdvancedPivotEditorEnabled: false,
+ isAdvancedSourceEditorEnabled: false,
+ sourceConfigUpdated: false,
+ searchLanguage: 'kuery',
+ searchString: 'the-query',
+ searchQuery: 'the-search-query',
+ valid: true,
+ transformFunction: 'pivot',
+ latestConfig: {} as LatestFunctionConfigUI,
+ previewRequest: {
+ pivot: {
+ aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } },
+ group_by: { 'the-group-by-agg-name': { terms: { field: 'the-group-by-field' } } },
+ },
+ },
+ validationStatus: {
+ isValid: true,
+ },
+ runtimeMappings,
+ runtimeMappingsUpdated: false,
+ isRuntimeMappingsEditorEnabled: false,
+ };
+ const transformDetailsState: StepDetailsExposedState = {
+ continuousModeDateField: 'the-continuous-mode-date-field',
+ continuousModeDelay: 'the-continuous-mode-delay',
+ createIndexPattern: false,
+ isContinuousModeEnabled: false,
+ isRetentionPolicyEnabled: false,
+ retentionPolicyDateField: '',
+ retentionPolicyMaxAge: '',
+ transformId: 'the-transform-id',
+ transformDescription: 'the-transform-description',
+ transformFrequency: '1m',
+ transformSettingsMaxPageSearchSize: 100,
+ transformSettingsDocsPerSecond: 400,
+ destinationIndex: 'the-destination-index',
+ touched: true,
+ valid: true,
+ };
+
+ const request = getCreateTransformRequestBody(
+ 'the-index-pattern-title',
+ pivotState,
+ transformDetailsState
+ );
+
+ expect(request).toEqual({
+ description: 'the-transform-description',
+ dest: { index: 'the-destination-index' },
+ frequency: '1m',
+ pivot: {
+ aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } },
+ group_by: { 'the-group-by-agg-name': { terms: { field: 'the-group-by-field' } } },
+ },
+ settings: {
+ max_page_search_size: 100,
+ docs_per_second: 400,
+ },
+ source: {
+ index: ['the-index-pattern-title'],
+ query: { query_string: { default_operator: 'AND', query: 'the-search-query' } },
+ runtime_mappings: runtimeMappings,
+ },
+ });
+ });
+
test('getCreateTransformSettingsRequestBody() with multiple settings', () => {
const transformDetailsState: Partial = {
transformSettingsDocsPerSecond: 400,
diff --git a/x-pack/plugins/transform/public/app/common/request.ts b/x-pack/plugins/transform/public/app/common/request.ts
index 8e535e653a380e..e4cfd0a874f0f5 100644
--- a/x-pack/plugins/transform/public/app/common/request.ts
+++ b/x-pack/plugins/transform/public/app/common/request.ts
@@ -19,7 +19,8 @@ import type {
import type { SavedSearchQuery } from '../hooks/use_search_items';
import type { StepDefineExposedState } from '../sections/create_transform/components/step_define';
-import type { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form';
+import type { StepDetailsExposedState } from '../sections/create_transform/components/step_details';
+import { isPopulatedObject } from './utils/object_utils';
export interface SimpleQuery {
query_string: {
@@ -57,10 +58,34 @@ export function isDefaultQuery(query: PivotQuery): boolean {
return isSimpleQuery(query) && query.query_string.query === '*';
}
+export function getCombinedRuntimeMappings(
+ indexPattern: IndexPattern | undefined,
+ runtimeMappings?: StepDefineExposedState['runtimeMappings']
+): StepDefineExposedState['runtimeMappings'] | undefined {
+ let combinedRuntimeMappings = {};
+
+ // Use runtime field mappings defined inline from API
+ if (isPopulatedObject(runtimeMappings)) {
+ combinedRuntimeMappings = { ...combinedRuntimeMappings, ...runtimeMappings };
+ }
+
+ // And runtime field mappings defined by index pattern
+ if (indexPattern !== undefined) {
+ const ipRuntimeMappings = indexPattern.getComputedFields().runtimeFields;
+ combinedRuntimeMappings = { ...combinedRuntimeMappings, ...ipRuntimeMappings };
+ }
+
+ if (isPopulatedObject(combinedRuntimeMappings)) {
+ return combinedRuntimeMappings;
+ }
+ return undefined;
+}
+
export function getPreviewTransformRequestBody(
indexPatternTitle: IndexPattern['title'],
query: PivotQuery,
- partialRequest?: StepDefineExposedState['previewRequest'] | undefined
+ partialRequest?: StepDefineExposedState['previewRequest'] | undefined,
+ runtimeMappings?: StepDefineExposedState['runtimeMappings']
): PostTransformsPreviewRequestSchema {
const index = indexPatternTitle.split(',').map((name: string) => name.trim());
@@ -68,6 +93,7 @@ export function getPreviewTransformRequestBody(
source: {
index,
...(!isDefaultQuery(query) && !isMatchAllQuery(query) ? { query } : {}),
+ ...(isPopulatedObject(runtimeMappings) ? { runtime_mappings: runtimeMappings } : {}),
},
...(partialRequest ?? {}),
};
@@ -95,7 +121,8 @@ export const getCreateTransformRequestBody = (
...getPreviewTransformRequestBody(
indexPatternTitle,
getPivotQuery(pivotState.searchQuery),
- pivotState.previewRequest
+ pivotState.previewRequest,
+ pivotState.runtimeMappings
),
// conditionally add optional description
...(transformDetailsState.transformDescription !== ''
@@ -119,6 +146,17 @@ export const getCreateTransformRequestBody = (
},
}
: {}),
+ // conditionally add retention policy settings
+ ...(transformDetailsState.isRetentionPolicyEnabled
+ ? {
+ retention_policy: {
+ time: {
+ field: transformDetailsState.retentionPolicyDateField,
+ max_age: transformDetailsState.retentionPolicyMaxAge,
+ },
+ },
+ }
+ : {}),
// conditionally add additional settings
...getCreateTransformSettingsRequestBody(transformDetailsState),
});
diff --git a/x-pack/plugins/transform/public/app/common/utils/object_utils.ts b/x-pack/plugins/transform/public/app/common/utils/object_utils.ts
new file mode 100644
index 00000000000000..4bbd0c1c2810fe
--- /dev/null
+++ b/x-pack/plugins/transform/public/app/common/utils/object_utils.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const isPopulatedObject = >(arg: any): arg is T => {
+ return typeof arg === 'object' && arg !== null && Object.keys(arg).length > 0;
+};
diff --git a/x-pack/plugins/transform/public/app/common/validators.test.ts b/x-pack/plugins/transform/public/app/common/validators.test.ts
index 44126b8f3fa26f..f48039052d2034 100644
--- a/x-pack/plugins/transform/public/app/common/validators.test.ts
+++ b/x-pack/plugins/transform/public/app/common/validators.test.ts
@@ -5,7 +5,12 @@
* 2.0.
*/
-import { continuousModeDelayValidator, transformFrequencyValidator } from './validators';
+import {
+ continuousModeDelayValidator,
+ parseDuration,
+ retentionPolicyMaxAgeValidator,
+ transformFrequencyValidator,
+} from './validators';
describe('continuousModeDelayValidator', () => {
it('should allow 0 input without unit', () => {
@@ -29,6 +34,73 @@ describe('continuousModeDelayValidator', () => {
});
});
+describe('parseDuration', () => {
+ it('should return undefined when the input is not an integer and valid time unit.', () => {
+ expect(parseDuration('0')).toBe(undefined);
+ expect(parseDuration('0.1s')).toBe(undefined);
+ expect(parseDuration('1.1m')).toBe(undefined);
+ expect(parseDuration('10.1asdf')).toBe(undefined);
+ });
+
+ it('should return parsed data for valid time units nanos|micros|ms|s|m|h|d.', () => {
+ expect(parseDuration('1a')).toEqual(undefined);
+ expect(parseDuration('1nanos')).toEqual({
+ number: 1,
+ timeUnit: 'nanos',
+ });
+ expect(parseDuration('1micros')).toEqual({
+ number: 1,
+ timeUnit: 'micros',
+ });
+ expect(parseDuration('1ms')).toEqual({ number: 1, timeUnit: 'ms' });
+ expect(parseDuration('1s')).toEqual({ number: 1, timeUnit: 's' });
+ expect(parseDuration('1m')).toEqual({ number: 1, timeUnit: 'm' });
+ expect(parseDuration('1h')).toEqual({ number: 1, timeUnit: 'h' });
+ expect(parseDuration('1d')).toEqual({ number: 1, timeUnit: 'd' });
+ });
+});
+
+describe('retentionPolicyMaxAgeValidator', () => {
+ it('should fail when the input is not an integer and valid time unit.', () => {
+ expect(retentionPolicyMaxAgeValidator('0')).toBe(false);
+ expect(retentionPolicyMaxAgeValidator('0.1s')).toBe(false);
+ expect(retentionPolicyMaxAgeValidator('1.1m')).toBe(false);
+ expect(retentionPolicyMaxAgeValidator('10.1asdf')).toBe(false);
+ });
+
+ it('should only allow values equal or above 60s.', () => {
+ expect(retentionPolicyMaxAgeValidator('0nanos')).toBe(false);
+ expect(retentionPolicyMaxAgeValidator('59999999999nanos')).toBe(false);
+ expect(retentionPolicyMaxAgeValidator('60000000000nanos')).toBe(true);
+ expect(retentionPolicyMaxAgeValidator('60000000001nanos')).toBe(true);
+
+ expect(retentionPolicyMaxAgeValidator('0micros')).toBe(false);
+ expect(retentionPolicyMaxAgeValidator('59999999micros')).toBe(false);
+ expect(retentionPolicyMaxAgeValidator('60000000micros')).toBe(true);
+ expect(retentionPolicyMaxAgeValidator('60000001micros')).toBe(true);
+
+ expect(retentionPolicyMaxAgeValidator('0ms')).toBe(false);
+ expect(retentionPolicyMaxAgeValidator('59999ms')).toBe(false);
+ expect(retentionPolicyMaxAgeValidator('60000ms')).toBe(true);
+ expect(retentionPolicyMaxAgeValidator('60001ms')).toBe(true);
+
+ expect(retentionPolicyMaxAgeValidator('0s')).toBe(false);
+ expect(retentionPolicyMaxAgeValidator('1s')).toBe(false);
+ expect(retentionPolicyMaxAgeValidator('59s')).toBe(false);
+ expect(retentionPolicyMaxAgeValidator('60s')).toBe(true);
+ expect(retentionPolicyMaxAgeValidator('61s')).toBe(true);
+ expect(retentionPolicyMaxAgeValidator('10000s')).toBe(true);
+
+ expect(retentionPolicyMaxAgeValidator('0m')).toBe(false);
+ expect(retentionPolicyMaxAgeValidator('1m')).toBe(true);
+ expect(retentionPolicyMaxAgeValidator('100m')).toBe(true);
+
+ expect(retentionPolicyMaxAgeValidator('0h')).toBe(false);
+ expect(retentionPolicyMaxAgeValidator('1h')).toBe(true);
+ expect(retentionPolicyMaxAgeValidator('2h')).toBe(true);
+ });
+});
+
describe('transformFrequencyValidator', () => {
it('should fail when the input is not an integer and valid time unit.', () => {
expect(transformFrequencyValidator('0')).toBe(false);
diff --git a/x-pack/plugins/transform/public/app/common/validators.ts b/x-pack/plugins/transform/public/app/common/validators.ts
index 125a7cd714aa52..065a6b4d1c0ca1 100644
--- a/x-pack/plugins/transform/public/app/common/validators.ts
+++ b/x-pack/plugins/transform/public/app/common/validators.ts
@@ -5,6 +5,9 @@
* 2.0.
*/
+const RETENTION_POLICY_MIN_AGE_SECONDS = 60;
+const TIME_UNITS = ['nanos', 'micros', 'ms', 's', 'm', 'h', 'd'];
+
/**
* Validates continuous mode time delay input.
* Doesn't allow floating intervals.
@@ -14,6 +17,78 @@ export function continuousModeDelayValidator(value: string): boolean {
return value.match(/^(0|\d*(nanos|micros|ms|s|m|h|d))$/) !== null;
}
+/**
+ * Parses a duration uses a string format like `60s`.
+ * @param value User input value.
+ */
+export interface ParsedDuration {
+ number: number;
+ timeUnit: string;
+}
+export function parseDuration(value: string): ParsedDuration | undefined {
+ if (typeof value !== 'string' || value === null) {
+ return;
+ }
+
+ // split string by groups of numbers and letters
+ const regexStr = value.match(/[a-z]+|[^a-z]+/gi);
+
+ // only valid if one group of numbers and one group of letters
+ if (regexStr === null || (Array.isArray(regexStr) && regexStr.length !== 2)) {
+ return;
+ }
+
+ const number = +regexStr[0];
+ const timeUnit = regexStr[1];
+
+ // only valid if number is an integer
+ if (isNaN(number) || !Number.isInteger(number)) {
+ return;
+ }
+
+ if (!TIME_UNITS.includes(timeUnit)) {
+ return;
+ }
+
+ return { number, timeUnit };
+}
+
+export function isValidRetentionPolicyMaxAge({ number, timeUnit }: ParsedDuration): boolean {
+ // only valid if value is equal or more than 60s
+ // supported time units: https://www.elastic.co/guide/en/elasticsearch/reference/master/common-options.html#time-units
+ return (
+ (timeUnit === 'nanos' && number >= RETENTION_POLICY_MIN_AGE_SECONDS * 1000000000) ||
+ (timeUnit === 'micros' && number >= RETENTION_POLICY_MIN_AGE_SECONDS * 1000000) ||
+ (timeUnit === 'ms' && number >= RETENTION_POLICY_MIN_AGE_SECONDS * 1000) ||
+ (timeUnit === 's' && number >= RETENTION_POLICY_MIN_AGE_SECONDS) ||
+ ((timeUnit === 'm' || timeUnit === 'h' || timeUnit === 'd') && number >= 1)
+ );
+}
+
+/**
+ * Validates retention policy max age input.
+ * Doesn't allow floating intervals.
+ * @param value User input value. Minimum of 60s.
+ */
+export function retentionPolicyMaxAgeValidator(value: string): boolean {
+ const parsedValue = parseDuration(value);
+
+ if (parsedValue === undefined) {
+ return false;
+ }
+
+ return isValidRetentionPolicyMaxAge(parsedValue);
+}
+
+// only valid if value is up to 1 hour
+export function isValidFrequency({ number, timeUnit }: ParsedDuration): boolean {
+ return (
+ (timeUnit === 's' && number <= 3600) ||
+ (timeUnit === 'm' && number <= 60) ||
+ (timeUnit === 'h' && number === 1)
+ );
+}
+
/**
* Validates transform frequency input.
* Allows time units of s/m/h only.
@@ -33,20 +108,15 @@ export const transformFrequencyValidator = (value: string): boolean => {
return false;
}
- const valueNumber = +regexStr[0];
- const valueTimeUnit = regexStr[1];
+ const number = +regexStr[0];
+ const timeUnit = regexStr[1];
// only valid if number is an integer above 0
- if (isNaN(valueNumber) || !Number.isInteger(valueNumber) || valueNumber === 0) {
+ if (isNaN(number) || !Number.isInteger(number) || number === 0) {
return false;
}
- // only valid if value is up to 1 hour
- return (
- (valueTimeUnit === 's' && valueNumber <= 3600) ||
- (valueTimeUnit === 'm' && valueNumber <= 60) ||
- (valueTimeUnit === 'h' && valueNumber === 1)
- );
+ return isValidFrequency({ number, timeUnit });
};
/**
diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx
index d7a760503a00cb..bd361afac2d8d7 100644
--- a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx
+++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx
@@ -25,6 +25,7 @@ jest.mock('./use_api');
import { useAppDependencies } from '../__mocks__/app_dependencies';
import { MlSharedContext } from '../__mocks__/shared_context';
+import { RuntimeField } from '../../../../../../src/plugins/data/common/index_patterns';
const query: SimpleQuery = {
query_string: {
@@ -33,13 +34,21 @@ const query: SimpleQuery = {
},
};
+const runtimeMappings = {
+ rt_bytes_bigger: {
+ type: 'double',
+ script: {
+ source: "emit(doc['bytes'].value * 2.0)",
+ },
+ } as RuntimeField,
+};
+
describe('Transform: useIndexData()', () => {
test('indexPattern set triggers loading', async () => {
const mlShared = await getMlSharedImports();
const wrapper: FC = ({ children }) => (
{children}
);
-
const { result, waitForNextUpdate } = renderHook(
() =>
useIndexData(
@@ -48,7 +57,8 @@ describe('Transform: useIndexData()', () => {
title: 'the-title',
fields: [],
} as unknown) as SearchItems['indexPattern'],
- query
+ query,
+ runtimeMappings
),
{ wrapper }
);
@@ -77,7 +87,7 @@ describe('Transform: with useIndexData()', () => {
ml: { DataGrid },
} = useAppDependencies();
const props = {
- ...useIndexData(indexPattern, { match_all: {} }),
+ ...useIndexData(indexPattern, { match_all: {} }, runtimeMappings),
copyToClipboard: 'the-copy-to-clipboard-code',
copyToClipboardDescription: 'the-copy-to-clipboard-description',
dataTestSubj: 'the-data-test-subj',
diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts
index ff2d5d2a8d71c5..abc63d886dbcc3 100644
--- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts
+++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { useEffect } from 'react';
+import { useEffect, useMemo } from 'react';
import { EuiDataGridColumn } from '@elastic/eui';
@@ -21,10 +21,12 @@ import { SearchItems } from './use_search_items';
import { useApi } from './use_api';
import { useAppDependencies, useToastNotifications } from '../app_dependencies';
+import type { StepDefineExposedState } from '../sections/create_transform/components/step_define/common';
export const useIndexData = (
indexPattern: SearchItems['indexPattern'],
- query: PivotQuery
+ query: PivotQuery,
+ combinedRuntimeMappings?: StepDefineExposedState['runtimeMappings']
): UseIndexDataReturnType => {
const api = useApi();
const toastNotifications = useToastNotifications();
@@ -32,6 +34,7 @@ export const useIndexData = (
ml: {
getFieldType,
getDataGridSchemaFromKibanaFieldType,
+ getDataGridSchemaFromESFieldType,
getFieldsFromKibanaIndexPattern,
showDataGridColumnChartErrorMessageToast,
useDataGrid,
@@ -43,14 +46,37 @@ export const useIndexData = (
const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern);
- // EuiDataGrid State
- const columns: EuiDataGridColumn[] = [
- ...indexPatternFields.map((id) => {
+ const columns: EuiDataGridColumn[] = useMemo(() => {
+ let result: Array<{ id: string; schema: string | undefined }> = [];
+
+ // Get the the runtime fields that are defined from API field and index patterns
+ if (combinedRuntimeMappings !== undefined) {
+ result = Object.keys(combinedRuntimeMappings).map((fieldName) => {
+ const field = combinedRuntimeMappings[fieldName];
+ const schema = getDataGridSchemaFromESFieldType(field.type);
+ return { id: fieldName, schema };
+ });
+ }
+
+ // Combine the runtime field that are defined from API field
+ indexPatternFields.forEach((id) => {
const field = indexPattern.fields.getByName(id);
- const schema = getDataGridSchemaFromKibanaFieldType(field);
- return { id, schema };
- }),
- ];
+ if (!field?.runtimeField) {
+ const schema = getDataGridSchemaFromKibanaFieldType(field);
+ result.push({ id, schema });
+ }
+ });
+
+ return result.sort((a, b) => a.id.localeCompare(b.id));
+ }, [
+ indexPatternFields,
+ indexPattern.fields,
+ combinedRuntimeMappings,
+ getDataGridSchemaFromESFieldType,
+ getDataGridSchemaFromKibanaFieldType,
+ ]);
+
+ // EuiDataGrid State
const dataGrid = useDataGrid(columns);
@@ -92,9 +118,12 @@ export const useIndexData = (
from: pagination.pageIndex * pagination.pageSize,
size: pagination.pageSize,
...(Object.keys(sort).length > 0 ? { sort } : {}),
+ ...(typeof combinedRuntimeMappings === 'object' &&
+ Object.keys(combinedRuntimeMappings).length > 0
+ ? { runtime_mappings: combinedRuntimeMappings }
+ : {}),
},
};
-
const resp = await api.esSearch(esSearchRequest);
if (!isEsSearchResponse(resp)) {
@@ -134,7 +163,17 @@ export const useIndexData = (
fetchDataGridData();
// custom comparison
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]);
+ }, [
+ indexPattern.title,
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ JSON.stringify([
+ query,
+ pagination,
+ sortingColumns,
+ indexPatternFields,
+ combinedRuntimeMappings,
+ ]),
+ ]);
useEffect(() => {
if (chartsVisible) {
diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts
index 673d8d38aa8fd1..62b3a077df5e61 100644
--- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts
+++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts
@@ -71,7 +71,8 @@ export const usePivotData = (
indexPatternTitle: SearchItems['indexPattern']['title'],
query: PivotQuery,
validationStatus: StepDefineExposedState['validationStatus'],
- requestPayload: StepDefineExposedState['previewRequest']
+ requestPayload: StepDefineExposedState['previewRequest'],
+ combinedRuntimeMappings?: StepDefineExposedState['runtimeMappings']
): UseIndexDataReturnType => {
const [
previewMappingsProperties,
@@ -79,7 +80,13 @@ export const usePivotData = (
] = useState({});
const api = useApi();
const {
- ml: { formatHumanReadableDateTimeSeconds, multiColumnSortFactory, useDataGrid, INDEX_STATUS },
+ ml: {
+ getDataGridSchemaFromESFieldType,
+ formatHumanReadableDateTimeSeconds,
+ multiColumnSortFactory,
+ useDataGrid,
+ INDEX_STATUS,
+ },
} = useAppDependencies();
// Filters mapping properties of type `object`, which get returned for nested field parents.
@@ -97,38 +104,7 @@ export const usePivotData = (
// EuiDataGrid State
const columns: EuiDataGridColumn[] = columnKeys.map((id) => {
const field = previewMappingsProperties[id];
-
- // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json']
- // To fall back to the default string schema it needs to be undefined.
- let schema;
-
- switch (field?.type) {
- case ES_FIELD_TYPES.GEO_POINT:
- case ES_FIELD_TYPES.GEO_SHAPE:
- schema = 'json';
- break;
- case ES_FIELD_TYPES.BOOLEAN:
- schema = 'boolean';
- break;
- case ES_FIELD_TYPES.DATE:
- case ES_FIELD_TYPES.DATE_NANOS:
- schema = 'datetime';
- break;
- case ES_FIELD_TYPES.BYTE:
- case ES_FIELD_TYPES.DOUBLE:
- case ES_FIELD_TYPES.FLOAT:
- case ES_FIELD_TYPES.HALF_FLOAT:
- case ES_FIELD_TYPES.INTEGER:
- case ES_FIELD_TYPES.LONG:
- case ES_FIELD_TYPES.SCALED_FLOAT:
- case ES_FIELD_TYPES.SHORT:
- schema = 'numeric';
- break;
- // keep schema undefined for text based columns
- case ES_FIELD_TYPES.KEYWORD:
- case ES_FIELD_TYPES.TEXT:
- break;
- }
+ const schema = getDataGridSchemaFromESFieldType(field?.type);
return { id, schema };
});
@@ -159,7 +135,12 @@ export const usePivotData = (
setNoDataMessage('');
setStatus(INDEX_STATUS.LOADING);
- const previewRequest = getPreviewTransformRequestBody(indexPatternTitle, query, requestPayload);
+ const previewRequest = getPreviewTransformRequestBody(
+ indexPatternTitle,
+ query,
+ requestPayload,
+ combinedRuntimeMappings
+ );
const resp = await api.getTransformsPreview(previewRequest);
if (!isPostTransformsPreviewResponseSchema(resp)) {
@@ -196,11 +177,7 @@ export const usePivotData = (
getPreviewData();
// custom comparison
/* eslint-disable react-hooks/exhaustive-deps */
- }, [
- indexPatternTitle,
- JSON.stringify([requestPayload, query]),
- /* eslint-enable react-hooks/exhaustive-deps */
- ]);
+ }, [indexPatternTitle, JSON.stringify([requestPayload, query, combinedRuntimeMappings])]);
if (sortingColumns.length > 0) {
tableItems.sort(multiColumnSortFactory(sortingColumns));
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx
new file mode 100644
index 00000000000000..087bae97e287ef
--- /dev/null
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx
@@ -0,0 +1,70 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { isEqual } from 'lodash';
+import React, { memo, FC } from 'react';
+
+import { EuiCodeEditor } from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
+
+import { StepDefineFormHook } from '../step_define';
+
+export const AdvancedRuntimeMappingsEditor: FC = memo(
+ ({
+ actions: {
+ convertToJson,
+ setAdvancedRuntimeMappingsConfig,
+ setRuntimeMappingsEditorApplyButtonEnabled,
+ },
+ state: { advancedEditorRuntimeMappingsLastApplied, advancedRuntimeMappingsConfig, xJsonMode },
+ }) => {
+ return (
+ {
+ setAdvancedRuntimeMappingsConfig(d);
+
+ // Disable the "Apply"-Button if the config hasn't changed.
+ if (advancedEditorRuntimeMappingsLastApplied === d) {
+ setRuntimeMappingsEditorApplyButtonEnabled(false);
+ return;
+ }
+
+ // Try to parse the string passed on from the editor.
+ // If parsing fails, the "Apply"-Button will be disabled
+ try {
+ JSON.parse(convertToJson(d));
+ setRuntimeMappingsEditorApplyButtonEnabled(true);
+ } catch (e) {
+ setRuntimeMappingsEditorApplyButtonEnabled(false);
+ }
+ }}
+ setOptions={{
+ fontSize: '12px',
+ }}
+ theme="textmate"
+ aria-label={i18n.translate('xpack.transform.stepDefineForm.advancedEditorAriaLabel', {
+ defaultMessage: 'Advanced pivot editor',
+ })}
+ />
+ );
+ },
+ (prevProps, nextProps) => isEqual(pickProps(prevProps), pickProps(nextProps))
+);
+
+function pickProps(props: StepDefineFormHook['runtimeMappingsEditor']) {
+ return [
+ props.state.advancedEditorRuntimeMappingsLastApplied,
+ props.state.advancedRuntimeMappingsConfig,
+ ];
+}
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/advanced_runtime_mappings_editor_switch.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/advanced_runtime_mappings_editor_switch.tsx
new file mode 100644
index 00000000000000..be297c10a8f88c
--- /dev/null
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/advanced_runtime_mappings_editor_switch.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC } from 'react';
+import { EuiSwitch } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { StepDefineFormHook } from '../step_define';
+
+export const AdvancedRuntimeMappingsEditorSwitch: FC<
+ StepDefineFormHook['runtimeMappingsEditor']
+> = (props) => {
+ const {
+ actions: { setRuntimeMappingsUpdated, toggleRuntimeMappingsEditor },
+ state: { isRuntimeMappingsEditorEnabled },
+ } = props;
+
+ // If switching to KQL after updating via editor - reset search
+ const toggleEditorHandler = (reset = false) => {
+ if (reset === true) {
+ setRuntimeMappingsUpdated(false);
+ }
+ toggleRuntimeMappingsEditor(reset);
+ };
+
+ return (
+ toggleEditorHandler()}
+ data-test-subj="transformAdvancedRuntimeMappingsEditorSwitch"
+ />
+ );
+};
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/index.ts
new file mode 100644
index 00000000000000..89a05690cab52d
--- /dev/null
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor_switch/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { AdvancedRuntimeMappingsEditorSwitch } from './advanced_runtime_mappings_editor_switch';
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx
new file mode 100644
index 00000000000000..f3c121a86cdc1b
--- /dev/null
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx
@@ -0,0 +1,175 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { FC } from 'react';
+import {
+ EuiButton,
+ EuiButtonIcon,
+ EuiCopy,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFormRow,
+ EuiSpacer,
+ EuiText,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { StepDefineFormHook } from '../step_define';
+import { AdvancedRuntimeMappingsEditor } from '../advanced_runtime_mappings_editor/advanced_runtime_mappings_editor';
+import { AdvancedRuntimeMappingsEditorSwitch } from '../advanced_runtime_mappings_editor_switch';
+import {
+ isPivotGroupByConfigWithUiSupport,
+ PivotAggsConfigWithUiSupport,
+} from '../../../../common';
+import { isPivotAggConfigWithUiSupport } from '../../../../common/pivot_group_by';
+
+const advancedEditorsSidebarWidth = '220px';
+const COPY_TO_CLIPBOARD_RUNTIME_MAPPINGS = i18n.translate(
+ 'xpack.transform.indexPreview.copyRuntimeMappingsClipboardTooltip',
+ {
+ defaultMessage: 'Copy Dev Console statement of the runtime mappings to the clipboard.',
+ }
+);
+
+export const AdvancedRuntimeMappingsSettings: FC = (props) => {
+ const {
+ actions: { applyRuntimeMappingsEditorChanges },
+ state: {
+ runtimeMappings,
+ advancedRuntimeMappingsConfig,
+ isRuntimeMappingsEditorApplyButtonEnabled,
+ isRuntimeMappingsEditorEnabled,
+ },
+ } = props.runtimeMappingsEditor;
+ const {
+ actions: { deleteAggregation, deleteGroupBy },
+ state: { groupByList, aggList },
+ } = props.pivotConfig;
+
+ const applyChanges = () => {
+ const nextConfig = JSON.parse(advancedRuntimeMappingsConfig);
+ const previousConfig = runtimeMappings;
+
+ applyRuntimeMappingsEditorChanges();
+
+ // If the user updates the name of the runtime mapping fields
+ // delete any groupBy or aggregation associated with the deleted field
+ Object.keys(groupByList).forEach((groupByKey) => {
+ const groupBy = groupByList[groupByKey];
+ if (
+ isPivotGroupByConfigWithUiSupport(groupBy) &&
+ previousConfig?.hasOwnProperty(groupBy.field) &&
+ !nextConfig.hasOwnProperty(groupBy.field)
+ ) {
+ deleteGroupBy(groupByKey);
+ }
+ });
+ Object.keys(aggList).forEach((aggName) => {
+ const agg = aggList[aggName] as PivotAggsConfigWithUiSupport;
+ if (
+ isPivotAggConfigWithUiSupport(agg) &&
+ agg.field !== undefined &&
+ previousConfig?.hasOwnProperty(agg.field) &&
+ !nextConfig.hasOwnProperty(agg.field)
+ ) {
+ deleteAggregation(aggName);
+ }
+ });
+ };
+ return (
+ <>
+
+
+
+
+
+ {runtimeMappings !== undefined && Object.keys(runtimeMappings).length > 0 ? (
+
+ ) : (
+
+ )}
+
+ {isRuntimeMappingsEditorEnabled && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {(copy: () => void) => (
+
+ )}
+
+
+
+
+
+ {isRuntimeMappingsEditorEnabled && (
+
+
+
+ {i18n.translate(
+ 'xpack.transform.stepDefineForm.advancedRuntimeMappingsEditorHelpText',
+ {
+ defaultMessage:
+ 'The advanced editor allows you to edit the runtime mappings of the transform configuration.',
+ }
+ )}
+
+
+
+ {i18n.translate(
+ 'xpack.transform.stepDefineForm.advancedSourceEditorApplyButtonText',
+ {
+ defaultMessage: 'Apply changes',
+ }
+ )}
+
+
+ )}
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/index.ts
new file mode 100644
index 00000000000000..69b3bc36a559e9
--- /dev/null
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { AdvancedRuntimeMappingsSettings } from './advanced_runtime_mappings_settings';
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx
index 807830d749892d..34832ec968e296 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx
@@ -46,6 +46,9 @@ import {
PutTransformsLatestRequestSchema,
PutTransformsPivotRequestSchema,
} from '../../../../../../common/api_schemas/transforms';
+import type { RuntimeField } from '../../../../../../../../../src/plugins/data/common/index_patterns';
+import { isPopulatedObject } from '../../../../common/utils/object_utils';
+import { isLatestTransform } from '../../../../../../common/types/transform';
export interface StepDetailsExposedState {
created: boolean;
@@ -189,12 +192,19 @@ export const StepCreateForm: FC = React.memo(
const createKibanaIndexPattern = async () => {
setLoading(true);
const indexPatternName = transformConfig.dest.index;
+ const runtimeMappings = transformConfig.source.runtime_mappings as Record<
+ string,
+ RuntimeField
+ >;
try {
const newIndexPattern = await indexPatterns.createAndSave(
{
title: indexPatternName,
timeFieldName,
+ ...(isPopulatedObject(runtimeMappings) && isLatestTransform(transformConfig)
+ ? { runtimeFieldMap: runtimeMappings }
+ : {}),
},
false,
true
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts
index 77b60b6f5966af..6298874a203666 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts
@@ -30,12 +30,19 @@ import { TRANSFORM_FUNCTION } from '../../../../../../../common/constants';
import { StepDefineFormProps } from '../step_define_form';
import { validateLatestConfig } from '../hooks/use_latest_function_config';
import { validatePivotConfig } from '../hooks/use_pivot_config';
+import { getCombinedRuntimeMappings } from '../../../../../common/request';
export function applyTransformConfigToDefineState(
state: StepDefineExposedState,
transformConfig?: TransformBaseConfig,
indexPattern?: StepDefineFormProps['searchItems']['indexPattern']
): StepDefineExposedState {
+ // apply runtime mappings from both the index pattern and inline configurations
+ state.runtimeMappings = getCombinedRuntimeMappings(
+ indexPattern,
+ transformConfig?.source?.runtime_mappings
+ );
+
if (transformConfig === undefined) {
return state;
}
@@ -107,6 +114,5 @@ export function applyTransformConfigToDefineState(
// applying a transform config to wizard state will always result in a valid configuration
state.valid = true;
-
return state;
}
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts
index deaaddc44ba7ab..fcdbac8c7ff39c 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts
@@ -8,6 +8,7 @@
import { getPivotDropdownOptions } from '../common';
import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public';
import { FilterAggForm } from './filter_agg/components';
+import type { RuntimeField } from '../../../../../../../../../../src/plugins/data/common/index_patterns';
describe('Transform: Define Pivot Common', () => {
test('getPivotDropdownOptions()', () => {
@@ -109,5 +110,169 @@ describe('Transform: Define Pivot Common', () => {
},
},
});
+
+ const runtimeMappings = {
+ rt_bytes_bigger: {
+ type: 'double',
+ script: {
+ source: "emit(doc['bytes'].value * 2.0)",
+ },
+ } as RuntimeField,
+ };
+ const optionsWithRuntimeFields = getPivotDropdownOptions(indexPattern, runtimeMappings);
+ expect(optionsWithRuntimeFields).toMatchObject({
+ aggOptions: [
+ {
+ label: ' the-f[i]e>ld ',
+ options: [
+ { label: 'avg( the-f[i]e>ld )' },
+ { label: 'cardinality( the-f[i]e>ld )' },
+ { label: 'max( the-f[i]e>ld )' },
+ { label: 'min( the-f[i]e>ld )' },
+ { label: 'percentiles( the-f[i]e>ld )' },
+ { label: 'sum( the-f[i]e>ld )' },
+ { label: 'value_count( the-f[i]e>ld )' },
+ { label: 'filter( the-f[i]e>ld )' },
+ ],
+ },
+ {
+ label: 'rt_bytes_bigger',
+ options: [
+ { label: 'avg(rt_bytes_bigger)' },
+ { label: 'cardinality(rt_bytes_bigger)' },
+ { label: 'max(rt_bytes_bigger)' },
+ { label: 'min(rt_bytes_bigger)' },
+ { label: 'percentiles(rt_bytes_bigger)' },
+ { label: 'sum(rt_bytes_bigger)' },
+ { label: 'value_count(rt_bytes_bigger)' },
+ { label: 'filter(rt_bytes_bigger)' },
+ ],
+ },
+ ],
+ aggOptionsData: {
+ 'avg( the-f[i]e>ld )': {
+ agg: 'avg',
+ aggName: 'the-field.avg',
+ dropDownName: 'avg( the-f[i]e>ld )',
+ field: ' the-f[i]e>ld ',
+ },
+ 'cardinality( the-f[i]e>ld )': {
+ agg: 'cardinality',
+ aggName: 'the-field.cardinality',
+ dropDownName: 'cardinality( the-f[i]e>ld )',
+ field: ' the-f[i]e>ld ',
+ },
+ 'max( the-f[i]e>ld )': {
+ agg: 'max',
+ aggName: 'the-field.max',
+ dropDownName: 'max( the-f[i]e>ld )',
+ field: ' the-f[i]e>ld ',
+ },
+ 'min( the-f[i]e>ld )': {
+ agg: 'min',
+ aggName: 'the-field.min',
+ dropDownName: 'min( the-f[i]e>ld )',
+ field: ' the-f[i]e>ld ',
+ },
+ 'percentiles( the-f[i]e>ld )': {
+ agg: 'percentiles',
+ aggName: 'the-field.percentiles',
+ dropDownName: 'percentiles( the-f[i]e>ld )',
+ field: ' the-f[i]e>ld ',
+ percents: [1, 5, 25, 50, 75, 95, 99],
+ },
+ 'sum( the-f[i]e>ld )': {
+ agg: 'sum',
+ aggName: 'the-field.sum',
+ dropDownName: 'sum( the-f[i]e>ld )',
+ field: ' the-f[i]e>ld ',
+ },
+ 'value_count( the-f[i]e>ld )': {
+ agg: 'value_count',
+ aggName: 'the-field.value_count',
+ dropDownName: 'value_count( the-f[i]e>ld )',
+ field: ' the-f[i]e>ld ',
+ },
+ 'filter( the-f[i]e>ld )': {
+ agg: 'filter',
+ aggName: 'the-field.filter',
+ dropDownName: 'filter( the-f[i]e>ld )',
+ field: ' the-f[i]e>ld ',
+ isSubAggsSupported: true,
+ AggFormComponent: FilterAggForm,
+ },
+ 'avg(rt_bytes_bigger)': {
+ agg: 'avg',
+ aggName: 'rt_bytes_bigger.avg',
+ dropDownName: 'avg(rt_bytes_bigger)',
+ field: 'rt_bytes_bigger',
+ },
+ 'cardinality(rt_bytes_bigger)': {
+ agg: 'cardinality',
+ aggName: 'rt_bytes_bigger.cardinality',
+ dropDownName: 'cardinality(rt_bytes_bigger)',
+ field: 'rt_bytes_bigger',
+ },
+ 'max(rt_bytes_bigger)': {
+ agg: 'max',
+ aggName: 'rt_bytes_bigger.max',
+ dropDownName: 'max(rt_bytes_bigger)',
+ field: 'rt_bytes_bigger',
+ },
+ 'min(rt_bytes_bigger)': {
+ agg: 'min',
+ aggName: 'rt_bytes_bigger.min',
+ dropDownName: 'min(rt_bytes_bigger)',
+ field: 'rt_bytes_bigger',
+ },
+ 'percentiles(rt_bytes_bigger)': {
+ agg: 'percentiles',
+ aggName: 'rt_bytes_bigger.percentiles',
+ dropDownName: 'percentiles(rt_bytes_bigger)',
+ field: 'rt_bytes_bigger',
+ percents: [1, 5, 25, 50, 75, 95, 99],
+ },
+ 'sum(rt_bytes_bigger)': {
+ agg: 'sum',
+ aggName: 'rt_bytes_bigger.sum',
+ dropDownName: 'sum(rt_bytes_bigger)',
+ field: 'rt_bytes_bigger',
+ },
+ 'value_count(rt_bytes_bigger)': {
+ agg: 'value_count',
+ aggName: 'rt_bytes_bigger.value_count',
+ dropDownName: 'value_count(rt_bytes_bigger)',
+ field: 'rt_bytes_bigger',
+ },
+ 'filter(rt_bytes_bigger)': {
+ agg: 'filter',
+ aggName: 'rt_bytes_bigger.filter',
+ dropDownName: 'filter(rt_bytes_bigger)',
+ field: 'rt_bytes_bigger',
+ isSubAggsSupported: true,
+ AggFormComponent: FilterAggForm,
+ },
+ },
+ groupByOptions: [
+ { label: 'histogram( the-f[i]e>ld )' },
+ { label: 'histogram(rt_bytes_bigger)' },
+ ],
+ groupByOptionsData: {
+ 'histogram( the-f[i]e>ld )': {
+ agg: 'histogram',
+ aggName: 'the-field',
+ dropDownName: 'histogram( the-f[i]e>ld )',
+ field: ' the-f[i]e>ld ',
+ interval: '10',
+ },
+ 'histogram(rt_bytes_bigger)': {
+ agg: 'histogram',
+ aggName: 'rt_bytes_bigger',
+ dropDownName: 'histogram(rt_bytes_bigger)',
+ field: 'rt_bytes_bigger',
+ interval: '10',
+ },
+ },
+ });
});
});
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.test.tsx
index dae8f61aaa4dff..7f9c4256f77557 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.test.tsx
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.test.tsx
@@ -10,11 +10,23 @@ import React from 'react';
import { I18nProvider } from '@kbn/i18n/react';
import { FilterAggForm } from './filter_agg_form';
import { CreateTransformWizardContext } from '../../../../wizard/wizard';
-import { KBN_FIELD_TYPES } from '../../../../../../../../../../../../src/plugins/data/common';
+import {
+ KBN_FIELD_TYPES,
+ RuntimeField,
+} from '../../../../../../../../../../../../src/plugins/data/common';
import { IndexPattern } from '../../../../../../../../../../../../src/plugins/data/public';
import { FilterTermForm } from './filter_term_form';
describe('FilterAggForm', () => {
+ const runtimeMappings = {
+ rt_bytes_bigger: {
+ type: 'double',
+ script: {
+ source: "emit(doc['bytes'].value * 2.0)",
+ },
+ } as RuntimeField,
+ };
+
const indexPattern = ({
fields: {
getByName: jest.fn((fieldName: string) => {
@@ -37,7 +49,7 @@ describe('FilterAggForm', () => {
const { getByLabelText, findByTestId, container } = render(
-
+
@@ -62,7 +74,7 @@ describe('FilterAggForm', () => {
const { findByTestId } = render(
-
+
@@ -90,7 +102,7 @@ describe('FilterAggForm', () => {
const { rerender, findByTestId } = render(
-
+
@@ -99,7 +111,7 @@ describe('FilterAggForm', () => {
// re-render the same component with different props
rerender(
-
+
@@ -127,7 +139,7 @@ describe('FilterAggForm', () => {
const { findByTestId, container } = render(
-
+
{
- const { indexPattern } = useContext(CreateTransformWizardContext);
+ const { indexPattern, runtimeMappings } = useContext(CreateTransformWizardContext);
- const filterAggsOptions = useMemo(() => getSupportedFilterAggs(selectedField, indexPattern!), [
- indexPattern,
- selectedField,
- ]);
+ const filterAggsOptions = useMemo(
+ () => getSupportedFilterAggs(selectedField, indexPattern!, runtimeMappings),
+ [indexPattern, selectedField, runtimeMappings]
+ );
useUpdateEffect(() => {
// reset filter agg on field change
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_range_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_range_form.tsx
index 2e9ad761d3b790..67c904946d302b 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_range_form.tsx
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_range_form.tsx
@@ -17,6 +17,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import { FilterAggConfigRange } from '../types';
+const BUTTON_SIZE = 40;
/**
* Form component for the range filter aggregation for number type fields.
*/
@@ -45,7 +46,7 @@ export const FilterRangeForm: FilterAggConfigRange['aggTypeConfig']['FilterAggFo
return (
<>
-
+
{
updateConfig({ includeFrom: e.target.checked });
}}
@@ -94,13 +96,14 @@ export const FilterRangeForm: FilterAggConfigRange['aggTypeConfig']['FilterAggFo
step="any"
append={
{
updateConfig({ includeTo: !includeTo });
}}
fill={includeTo}
>
- {includeTo ? '≤' : '<'}s
+ {includeTo ? '≤' : '<'}
}
/>
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx
index ad06cfb31a62f1..f2db6167c163c6 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx
@@ -26,7 +26,7 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm
selectedField,
}) => {
const api = useApi();
- const { indexPattern } = useContext(CreateTransformWizardContext);
+ const { indexPattern, runtimeMappings } = useContext(CreateTransformWizardContext);
const toastNotifications = useToastNotifications();
const [options, setOptions] = useState([]);
@@ -38,6 +38,7 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm
const esSearchRequest = {
index: indexPattern!.title,
body: {
+ ...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}),
query: {
wildcard: {
[selectedField!]: {
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts
index d3b1df41b3cfbd..c75da651f79d0d 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_step_define_state.ts
@@ -30,5 +30,8 @@ export function getDefaultStepDefineState(searchItems: SearchItems): StepDefineE
isValid: false,
},
previewRequest: undefined,
+ runtimeMappings: undefined,
+ runtimeMappingsUpdated: false,
+ isRuntimeMappingsEditorEnabled: false,
};
}
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts
index 6845d096a2e022..c88b604989680a 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts
@@ -7,6 +7,7 @@
import { EuiComboBoxOptionOption } from '@elastic/eui';
import {
+ ES_FIELD_TYPES,
IndexPattern,
KBN_FIELD_TYPES,
} from '../../../../../../../../../../src/plugins/data/public';
@@ -24,11 +25,40 @@ import {
import { getDefaultAggregationConfig } from './get_default_aggregation_config';
import { getDefaultGroupByConfig } from './get_default_group_by_config';
-import { Field } from './types';
+import type { Field, StepDefineExposedState } from './types';
+import { isPopulatedObject } from '../../../../../common/utils/object_utils';
const illegalEsAggNameChars = /[[\]>]/g;
-export function getPivotDropdownOptions(indexPattern: IndexPattern) {
+export function getKibanaFieldTypeFromEsType(type: string): KBN_FIELD_TYPES {
+ switch (type) {
+ case ES_FIELD_TYPES.FLOAT:
+ case ES_FIELD_TYPES.HALF_FLOAT:
+ case ES_FIELD_TYPES.SCALED_FLOAT:
+ case ES_FIELD_TYPES.DOUBLE:
+ case ES_FIELD_TYPES.INTEGER:
+ case ES_FIELD_TYPES.LONG:
+ case ES_FIELD_TYPES.SHORT:
+ case ES_FIELD_TYPES.UNSIGNED_LONG:
+ return KBN_FIELD_TYPES.NUMBER;
+
+ case ES_FIELD_TYPES.DATE:
+ case ES_FIELD_TYPES.DATE_NANOS:
+ return KBN_FIELD_TYPES.DATE;
+
+ case ES_FIELD_TYPES.KEYWORD:
+ case ES_FIELD_TYPES.STRING:
+ return KBN_FIELD_TYPES.STRING;
+
+ default:
+ return type as KBN_FIELD_TYPES;
+ }
+}
+
+export function getPivotDropdownOptions(
+ indexPattern: IndexPattern,
+ runtimeMappings?: StepDefineExposedState['runtimeMappings']
+) {
// The available group by options
const groupByOptions: EuiComboBoxOptionOption[] = [];
const groupByOptionsData: PivotGroupByConfigWithUiSupportDict = {};
@@ -38,11 +68,26 @@ export function getPivotDropdownOptions(indexPattern: IndexPattern) {
const aggOptionsData: PivotAggsConfigWithUiSupportDict = {};
const ignoreFieldNames = ['_id', '_index', '_type'];
- const fields = indexPattern.fields
- .filter((field) => field.aggregatable === true && !ignoreFieldNames.includes(field.name))
+ const indexPatternFields = indexPattern.fields
+ .filter(
+ (field) =>
+ field.aggregatable === true && !ignoreFieldNames.includes(field.name) && !field.runtimeField
+ )
.map((field): Field => ({ name: field.name, type: field.type as KBN_FIELD_TYPES }));
- fields.forEach((field) => {
+ // Support for runtime_mappings that are defined by queries
+ let runtimeFields: Field[] = [];
+ if (isPopulatedObject(runtimeMappings)) {
+ runtimeFields = Object.keys(runtimeMappings).map((fieldName) => {
+ const field = runtimeMappings[fieldName];
+ return { name: fieldName, type: getKibanaFieldTypeFromEsType(field.type) };
+ });
+ }
+
+ const sortByLabel = (a: Field, b: Field) => a.name.localeCompare(b.name);
+
+ const combinedFields = [...indexPatternFields, ...runtimeFields].sort(sortByLabel);
+ combinedFields.forEach((field) => {
// Group by
const availableGroupByAggs: [] = getNestedProperty(pivotGroupByFieldSupport, field.type);
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts
index d1325e4af5ce74..cdba7a3f5482c9 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts
@@ -9,7 +9,11 @@ import { KBN_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/
import { EsFieldName } from '../../../../../../../common/types/fields';
-import { PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../common';
+import {
+ PivotAggsConfigDict,
+ PivotGroupByConfigDict,
+ PivotGroupByConfigWithUiSupportDict,
+} from '../../../../../common';
import { SavedSearchQuery } from '../../../../../hooks/use_search_items';
import { QUERY_LANGUAGE } from './constants';
@@ -30,10 +34,24 @@ export interface Field {
type: KBN_FIELD_TYPES;
}
+// Replace this with import once #88995 is merged
+const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const;
+type RuntimeType = typeof RUNTIME_FIELD_TYPES[number];
+
+export interface RuntimeField {
+ type: RuntimeType;
+ script:
+ | string
+ | {
+ source: string;
+ };
+}
+
+export type RuntimeMappings = Record;
export interface StepDefineExposedState {
transformFunction: TransformFunction;
aggList: PivotAggsConfigDict;
- groupByList: PivotGroupByConfigDict;
+ groupByList: PivotGroupByConfigDict | PivotGroupByConfigWithUiSupportDict;
latestConfig: LatestFunctionConfigUI;
isAdvancedPivotEditorEnabled: boolean;
isAdvancedSourceEditorEnabled: boolean;
@@ -47,6 +65,9 @@ export interface StepDefineExposedState {
* Undefined when the form is incomplete or invalid
*/
previewRequest: { latest: LatestFunctionConfig } | { pivot: PivotConfigDefinition } | undefined;
+ runtimeMappings?: RuntimeMappings;
+ runtimeMappingsUpdated: boolean;
+ isRuntimeMappingsEditorEnabled: boolean;
}
export function isPivotPartialRequest(arg: any): arg is { pivot: PivotConfigDefinition } {
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_runtime_mappings_editor.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_runtime_mappings_editor.ts
new file mode 100644
index 00000000000000..9bb5f91ae03c79
--- /dev/null
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_runtime_mappings_editor.ts
@@ -0,0 +1,102 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useState } from 'react';
+import { XJsonMode } from '@kbn/ace';
+import { StepDefineExposedState } from '../common';
+import { XJson } from '../../../../../../../../../../src/plugins/es_ui_shared/public';
+
+const { useXJsonMode } = XJson;
+const xJsonMode = new XJsonMode();
+
+export const useAdvancedRuntimeMappingsEditor = (defaults: StepDefineExposedState) => {
+ const stringifiedRuntimeMappings = JSON.stringify(defaults.runtimeMappings, null, 2);
+
+ // Advanced editor for source config state
+ const [runtimeMappingsUpdated, setRuntimeMappingsUpdated] = useState(
+ defaults.runtimeMappingsUpdated
+ );
+ const [runtimeMappings, setRuntimeMappings] = useState(defaults.runtimeMappings);
+
+ const [
+ isRuntimeMappingsEditorSwitchModalVisible,
+ setRuntimeMappingsEditorSwitchModalVisible,
+ ] = useState(false);
+
+ const [isRuntimeMappingsEditorEnabled, setRuntimeMappingsEditorEnabled] = useState(
+ defaults.isRuntimeMappingsEditorEnabled
+ );
+
+ const [
+ isRuntimeMappingsEditorApplyButtonEnabled,
+ setRuntimeMappingsEditorApplyButtonEnabled,
+ ] = useState(false);
+
+ const [
+ advancedEditorRuntimeMappingsLastApplied,
+ setAdvancedEditorRuntimeMappingsLastApplied,
+ ] = useState(stringifiedRuntimeMappings);
+
+ const [advancedEditorRuntimeMappings, setAdvancedEditorRuntimeMappings] = useState(
+ stringifiedRuntimeMappings
+ );
+
+ const {
+ convertToJson,
+ setXJson: setAdvancedRuntimeMappingsConfig,
+ xJson: advancedRuntimeMappingsConfig,
+ } = useXJsonMode(stringifiedRuntimeMappings ?? '');
+
+ const applyRuntimeMappingsEditorChanges = () => {
+ const parsedRuntimeMappings = JSON.parse(advancedRuntimeMappingsConfig);
+ const prettySourceConfig = JSON.stringify(parsedRuntimeMappings, null, 2);
+ setRuntimeMappingsUpdated(true);
+ setRuntimeMappings(parsedRuntimeMappings);
+ setAdvancedEditorRuntimeMappings(prettySourceConfig);
+ setAdvancedEditorRuntimeMappingsLastApplied(prettySourceConfig);
+ setRuntimeMappingsEditorApplyButtonEnabled(false);
+ };
+
+ // If switching to KQL after updating via editor - reset search
+ const toggleRuntimeMappingsEditor = (reset = false) => {
+ if (reset === true) {
+ setRuntimeMappingsUpdated(false);
+ }
+ if (isRuntimeMappingsEditorEnabled === false) {
+ setAdvancedEditorRuntimeMappingsLastApplied(advancedEditorRuntimeMappings);
+ }
+
+ setRuntimeMappingsEditorEnabled(!isRuntimeMappingsEditorEnabled);
+ setRuntimeMappingsEditorApplyButtonEnabled(false);
+ };
+
+ return {
+ actions: {
+ applyRuntimeMappingsEditorChanges,
+ setRuntimeMappingsEditorApplyButtonEnabled,
+ setRuntimeMappingsEditorEnabled,
+ setAdvancedEditorRuntimeMappings,
+ setAdvancedEditorRuntimeMappingsLastApplied,
+ setRuntimeMappingsEditorSwitchModalVisible,
+ setRuntimeMappingsUpdated,
+ toggleRuntimeMappingsEditor,
+ convertToJson,
+ setAdvancedRuntimeMappingsConfig,
+ },
+ state: {
+ advancedEditorRuntimeMappings,
+ advancedEditorRuntimeMappingsLastApplied,
+ isRuntimeMappingsEditorApplyButtonEnabled,
+ isRuntimeMappingsEditorEnabled,
+ isRuntimeMappingsEditorSwitchModalVisible,
+ runtimeMappingsUpdated,
+ advancedRuntimeMappingsConfig,
+ xJsonMode,
+ runtimeMappings,
+ },
+ };
+};
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_latest_function_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_latest_function_config.ts
index ecc8bf673d93a8..d52bd3f5bf7060 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_latest_function_config.ts
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_latest_function_config.ts
@@ -32,19 +32,28 @@ export const latestConfigMapper = {
* Provides available options for unique_key and sort fields
* @param indexPattern
* @param aggConfigs
+ * @param runtimeMappings
*/
function getOptions(
indexPattern: StepDefineFormProps['searchItems']['indexPattern'],
- aggConfigs: AggConfigs
+ aggConfigs: AggConfigs,
+ runtimeMappings?: StepDefineExposedState['runtimeMappings']
) {
const aggConfig = aggConfigs.aggs[0];
const param = aggConfig.type.params.find((p) => p.type === 'field');
const filteredIndexPatternFields = param
- ? ((param as unknown) as FieldParamType).getAvailableFields(aggConfig)
+ ? ((param as unknown) as FieldParamType)
+ .getAvailableFields(aggConfig)
+ // runtimeMappings may already include runtime fields defined by the index pattern
+ .filter((ip) => ip.runtimeField === undefined)
: [];
const ignoreFieldNames = new Set(['_source', '_type', '_index', '_id', '_version', '_score']);
+ const runtimeFieldsOptions = runtimeMappings
+ ? Object.keys(runtimeMappings).map((k) => ({ label: k, value: k }))
+ : [];
+
const uniqueKeyOptions: Array> = filteredIndexPatternFields
.filter((v) => !ignoreFieldNames.has(v.name))
.map((v) => ({
@@ -52,7 +61,16 @@ function getOptions(
value: v.name,
}));
- const sortFieldOptions: Array> = indexPattern.fields
+ const runtimeFieldsSortOptions: Array> = runtimeMappings
+ ? Object.entries(runtimeMappings)
+ .filter(([fieldName, fieldMapping]) => fieldMapping.type === 'date')
+ .map(([fieldName, fieldMapping]) => ({
+ label: fieldName,
+ value: fieldName,
+ }))
+ : [];
+
+ const indexPatternFieldsSortOptions: Array> = indexPattern.fields
// The backend API for `latest` allows all field types for sort but the UI will be limited to `date`.
.filter((v) => !ignoreFieldNames.has(v.name) && v.sortable && v.type === 'date')
.map((v) => ({
@@ -60,7 +78,15 @@ function getOptions(
value: v.name,
}));
- return { uniqueKeyOptions, sortFieldOptions };
+ const sortByLabel = (a: EuiComboBoxOptionOption, b: EuiComboBoxOptionOption) =>
+ a.label.localeCompare(b.label);
+
+ return {
+ uniqueKeyOptions: [...uniqueKeyOptions, ...runtimeFieldsOptions].sort(sortByLabel),
+ sortFieldOptions: [...indexPatternFieldsSortOptions, ...runtimeFieldsSortOptions].sort(
+ sortByLabel
+ ),
+ };
}
/**
@@ -86,7 +112,8 @@ export function validateLatestConfig(config?: LatestFunctionConfig) {
export function useLatestFunctionConfig(
defaults: StepDefineExposedState['latestConfig'],
- indexPattern: StepDefineFormProps['searchItems']['indexPattern']
+ indexPattern: StepDefineFormProps['searchItems']['indexPattern'],
+ runtimeMappings: StepDefineExposedState['runtimeMappings']
): {
config: LatestFunctionConfigUI;
uniqueKeyOptions: Array>;
@@ -104,8 +131,8 @@ export function useLatestFunctionConfig(
const { uniqueKeyOptions, sortFieldOptions } = useMemo(() => {
const aggConfigs = data.search.aggs.createAggConfigs(indexPattern, [{ type: 'terms' }]);
- return getOptions(indexPattern, aggConfigs);
- }, [indexPattern, data.search.aggs]);
+ return getOptions(indexPattern, aggConfigs, runtimeMappings);
+ }, [indexPattern, data.search.aggs, runtimeMappings]);
const updateLatestFunctionConfig = useCallback(
(update) =>
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts
index 1748f6f8fd4873..a02d3bafac9848 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts
@@ -115,8 +115,8 @@ export const usePivotConfig = (
const toastNotifications = useToastNotifications();
const { aggOptions, aggOptionsData, groupByOptions, groupByOptionsData } = useMemo(
- () => getPivotDropdownOptions(indexPattern),
- [indexPattern]
+ () => getPivotDropdownOptions(indexPattern, defaults.runtimeMappings),
+ [defaults.runtimeMappings, indexPattern]
);
// The list of selected aggregations
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts
index c2f01db05ff3e7..0ceea070df1b66 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts
@@ -19,6 +19,7 @@ import { usePivotConfig } from './use_pivot_config';
import { useSearchBar } from './use_search_bar';
import { useLatestFunctionConfig } from './use_latest_function_config';
import { TRANSFORM_FUNCTION } from '../../../../../../../common/constants';
+import { useAdvancedRuntimeMappingsEditor } from './use_advanced_runtime_mappings_editor';
export type StepDefineFormHook = ReturnType;
@@ -30,12 +31,18 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi
const searchBar = useSearchBar(defaults, indexPattern);
const pivotConfig = usePivotConfig(defaults, indexPattern);
- const latestFunctionConfig = useLatestFunctionConfig(defaults.latestConfig, indexPattern);
+
+ const latestFunctionConfig = useLatestFunctionConfig(
+ defaults.latestConfig,
+ indexPattern,
+ defaults?.runtimeMappings
+ );
const previewRequest = getPreviewTransformRequestBody(
indexPattern.title,
searchBar.state.pivotQuery,
- pivotConfig.state.requestPayload
+ pivotConfig.state.requestPayload,
+ defaults?.runtimeMappings
);
// pivot config hook
@@ -44,12 +51,17 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi
// source config hook
const advancedSourceEditor = useAdvancedSourceEditor(defaults, previewRequest);
+ // runtime mappings config hook
+ const runtimeMappingsEditor = useAdvancedRuntimeMappingsEditor(defaults);
+
useEffect(() => {
+ const runtimeMappings = runtimeMappingsEditor.state.runtimeMappings;
if (!advancedSourceEditor.state.isAdvancedSourceEditorEnabled) {
const previewRequestUpdate = getPreviewTransformRequestBody(
indexPattern.title,
searchBar.state.pivotQuery,
- pivotConfig.state.requestPayload
+ pivotConfig.state.requestPayload,
+ runtimeMappings
);
const stringifiedSourceConfigUpdate = JSON.stringify(
@@ -60,7 +72,6 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi
advancedSourceEditor.actions.setAdvancedEditorSourceConfig(stringifiedSourceConfigUpdate);
}
-
onChange({
transformFunction,
latestConfig: latestFunctionConfig.config,
@@ -84,6 +95,9 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi
transformFunction === TRANSFORM_FUNCTION.PIVOT
? pivotConfig.state.requestPayload
: latestFunctionConfig.requestPayload,
+ runtimeMappings,
+ runtimeMappingsUpdated: runtimeMappingsEditor.state.runtimeMappingsUpdated,
+ isRuntimeMappingsEditorEnabled: runtimeMappingsEditor.state.isRuntimeMappingsEditorEnabled,
});
// custom comparison
/* eslint-disable react-hooks/exhaustive-deps */
@@ -92,9 +106,13 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi
JSON.stringify(advancedSourceEditor.state),
pivotConfig.state,
JSON.stringify(searchBar.state),
+ JSON.stringify([
+ runtimeMappingsEditor.state.runtimeMappings,
+ runtimeMappingsEditor.state.runtimeMappingsUpdated,
+ runtimeMappingsEditor.state.isRuntimeMappingsEditorEnabled,
+ ]),
latestFunctionConfig.config,
transformFunction,
- /* eslint-enable react-hooks/exhaustive-deps */
]);
return {
@@ -102,6 +120,7 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi
setTransformFunction,
advancedPivotEditor,
advancedSourceEditor,
+ runtimeMappingsEditor,
pivotConfig,
latestFunctionConfig,
searchBar,
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx
index a5d9310e586e66..1ddb9aa61045ba 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx
@@ -57,6 +57,7 @@ import { getAggConfigFromEsAgg } from '../../../../common/pivot_aggs';
import { TransformFunctionSelector } from './transform_function_selector';
import { TRANSFORM_FUNCTION } from '../../../../../../common/constants';
import { LatestFunctionForm } from './latest_function_form';
+import { AdvancedRuntimeMappingsSettings } from '../advanced_runtime_mappings_settings';
export interface StepDefineFormProps {
overrides?: StepDefineExposedState;
@@ -67,7 +68,6 @@ export interface StepDefineFormProps {
export const StepDefineForm: FC = React.memo((props) => {
const { searchItems } = props;
const { indexPattern } = searchItems;
-
const {
ml: { DataGrid },
} = useAppDependencies();
@@ -87,11 +87,14 @@ export const StepDefineForm: FC = React.memo((props) => {
const pivotQuery = stepDefineForm.searchBar.state.pivotQuery;
const indexPreviewProps = {
- ...useIndexData(indexPattern, stepDefineForm.searchBar.state.pivotQuery),
+ ...useIndexData(
+ indexPattern,
+ stepDefineForm.searchBar.state.pivotQuery,
+ stepDefineForm.runtimeMappingsEditor.state.runtimeMappings
+ ),
dataTestSubj: 'transformIndexPreview',
toastNotifications,
};
-
const { requestPayload, validationStatus } =
stepDefineForm.transformFunction === TRANSFORM_FUNCTION.PIVOT
? stepDefineForm.pivotConfig.state
@@ -102,7 +105,8 @@ export const StepDefineForm: FC = React.memo((props) => {
pivotQuery,
stepDefineForm.transformFunction === TRANSFORM_FUNCTION.PIVOT
? stepDefineForm.pivotConfig.state.requestPayload
- : stepDefineForm.latestFunctionConfig.requestPayload
+ : stepDefineForm.latestFunctionConfig.requestPayload,
+ stepDefineForm.runtimeMappingsEditor.state.runtimeMappings
);
const copyToClipboardSource = getIndexDevConsoleStatement(pivotQuery, indexPattern.title);
@@ -122,7 +126,13 @@ export const StepDefineForm: FC = React.memo((props) => {
);
const pivotPreviewProps = {
- ...usePivotData(indexPattern.title, pivotQuery, validationStatus, requestPayload),
+ ...usePivotData(
+ indexPattern.title,
+ pivotQuery,
+ validationStatus,
+ requestPayload,
+ stepDefineForm.runtimeMappingsEditor.state.runtimeMappings
+ ),
dataTestSubj: 'transformPivotPreview',
title: i18n.translate('xpack.transform.pivotPreview.transformPreviewTitle', {
defaultMessage: 'Transform preview',
@@ -273,7 +283,7 @@ export const StepDefineForm: FC = React.memo((props) => {
defaultMessage:
'The advanced editor allows you to edit the source query clause of the transform configuration.',
}
- )}{' '}
+ )}
{i18n.translate(
'xpack.transform.stepDefineForm.advancedEditorHelpTextLink',
@@ -304,6 +314,9 @@ export const StepDefineForm: FC = React.memo((props) => {
+
+
+
>
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx
index 614965c8a3efe3..27e25596c980fa 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx
@@ -37,6 +37,7 @@ interface Props {
export const StepDefineSummary: FC = ({
formState: {
+ runtimeMappings,
searchString,
searchQuery,
groupByList,
@@ -57,14 +58,16 @@ export const StepDefineSummary: FC = ({
const previewRequest = getPreviewTransformRequestBody(
searchItems.indexPattern.title,
pivotQuery,
- partialPreviewRequest
+ partialPreviewRequest,
+ runtimeMappings
);
const pivotPreviewProps = usePivotData(
searchItems.indexPattern.title,
pivotQuery,
validationStatus,
- partialPreviewRequest
+ partialPreviewRequest,
+ runtimeMappings
);
const isModifiedQuery =
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts
index 3b8df3b977fff6..fbe32e9bea12ff 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts
@@ -5,5 +5,95 @@
* 2.0.
*/
+import type { TransformId, TransformPivotConfig } from '../../../../../../common/types/transform';
+
export type EsIndexName = string;
export type IndexPatternTitle = string;
+
+export interface StepDetailsExposedState {
+ continuousModeDateField: string;
+ continuousModeDelay: string;
+ createIndexPattern: boolean;
+ destinationIndex: EsIndexName;
+ isContinuousModeEnabled: boolean;
+ isRetentionPolicyEnabled: boolean;
+ retentionPolicyDateField: string;
+ retentionPolicyMaxAge: string;
+ touched: boolean;
+ transformId: TransformId;
+ transformDescription: string;
+ transformFrequency: string;
+ transformSettingsMaxPageSearchSize: number;
+ transformSettingsDocsPerSecond?: number;
+ valid: boolean;
+ indexPatternTimeField?: string | undefined;
+}
+
+const defaultContinuousModeDelay = '60s';
+const defaultTransformFrequency = '1m';
+const defaultTransformSettingsMaxPageSearchSize = 500;
+
+export function getDefaultStepDetailsState(): StepDetailsExposedState {
+ return {
+ continuousModeDateField: '',
+ continuousModeDelay: defaultContinuousModeDelay,
+ createIndexPattern: true,
+ isContinuousModeEnabled: false,
+ isRetentionPolicyEnabled: false,
+ retentionPolicyDateField: '',
+ retentionPolicyMaxAge: '',
+ transformId: '',
+ transformDescription: '',
+ transformFrequency: defaultTransformFrequency,
+ transformSettingsMaxPageSearchSize: defaultTransformSettingsMaxPageSearchSize,
+ destinationIndex: '',
+ touched: false,
+ valid: false,
+ indexPatternTimeField: undefined,
+ };
+}
+
+export function applyTransformConfigToDetailsState(
+ state: StepDetailsExposedState,
+ transformConfig?: TransformPivotConfig
+): StepDetailsExposedState {
+ // apply the transform configuration to wizard DETAILS state
+ if (transformConfig !== undefined) {
+ // Continuous mode
+ const continuousModeTime = transformConfig.sync?.time;
+ if (continuousModeTime !== undefined) {
+ state.continuousModeDateField = continuousModeTime.field;
+ state.continuousModeDelay = continuousModeTime?.delay ?? defaultContinuousModeDelay;
+ state.isContinuousModeEnabled = true;
+ }
+
+ // Description
+ if (transformConfig.description !== undefined) {
+ state.transformDescription = transformConfig.description;
+ }
+
+ // Frequency
+ if (transformConfig.frequency !== undefined) {
+ state.transformFrequency = transformConfig.frequency;
+ }
+
+ // Retention policy
+ const retentionPolicyTime = transformConfig.retention_policy?.time;
+ if (retentionPolicyTime !== undefined) {
+ state.retentionPolicyDateField = retentionPolicyTime.field;
+ state.retentionPolicyMaxAge = retentionPolicyTime.max_age;
+ state.isRetentionPolicyEnabled = true;
+ }
+
+ // Settings
+ if (transformConfig.settings) {
+ if (typeof transformConfig.settings?.max_page_search_size === 'number') {
+ state.transformSettingsMaxPageSearchSize = transformConfig.settings.max_page_search_size;
+ }
+ if (typeof transformConfig.settings?.docs_per_second === 'number') {
+ state.transformSettingsDocsPerSecond = transformConfig.settings.docs_per_second;
+ }
+ }
+ }
+ return state;
+}
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts
index 4b01e0c3746ec7..bbc4b42e1b236e 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/index.ts
@@ -8,6 +8,7 @@
export {
applyTransformConfigToDetailsState,
getDefaultStepDetailsState,
- StepDetailsForm,
-} from './step_details_form';
+ StepDetailsExposedState,
+} from './common';
+export { StepDetailsForm } from './step_details_form';
export { StepDetailsSummary } from './step_details_summary';
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx
index 100c37d911fa01..0d39ec77d059fb 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { Fragment, FC, useEffect, useState } from 'react';
+import React, { FC, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -49,87 +49,23 @@ import {
import { EsIndexName, IndexPatternTitle } from './common';
import {
continuousModeDelayValidator,
+ retentionPolicyMaxAgeValidator,
transformFrequencyValidator,
transformSettingsMaxPageSearchSizeValidator,
} from '../../../../common/validators';
import { StepDefineExposedState } from '../step_define/common';
import { TRANSFORM_FUNCTION } from '../../../../../../common/constants';
-export interface StepDetailsExposedState {
- continuousModeDateField: string;
- continuousModeDelay: string;
- createIndexPattern: boolean;
- destinationIndex: EsIndexName;
- isContinuousModeEnabled: boolean;
- touched: boolean;
- transformId: TransformId;
- transformDescription: string;
- transformFrequency: string;
- transformSettingsMaxPageSearchSize: number;
- transformSettingsDocsPerSecond?: number;
- valid: boolean;
- indexPatternTimeField?: string | undefined;
-}
-
-const defaultContinuousModeDelay = '60s';
-const defaultTransformFrequency = '1m';
-const defaultTransformSettingsMaxPageSearchSize = 500;
-
-export function getDefaultStepDetailsState(): StepDetailsExposedState {
- return {
- continuousModeDateField: '',
- continuousModeDelay: defaultContinuousModeDelay,
- createIndexPattern: true,
- isContinuousModeEnabled: false,
- transformId: '',
- transformDescription: '',
- transformFrequency: defaultTransformFrequency,
- transformSettingsMaxPageSearchSize: defaultTransformSettingsMaxPageSearchSize,
- destinationIndex: '',
- touched: false,
- valid: false,
- indexPatternTimeField: undefined,
- };
-}
+import { getDefaultStepDetailsState, StepDetailsExposedState } from './common';
-export function applyTransformConfigToDetailsState(
- state: StepDetailsExposedState,
- transformConfig?: TransformPivotConfig
-): StepDetailsExposedState {
- // apply the transform configuration to wizard DETAILS state
- if (transformConfig !== undefined) {
- const time = transformConfig.sync?.time;
- if (time !== undefined) {
- state.continuousModeDateField = time.field;
- state.continuousModeDelay = time?.delay ?? defaultContinuousModeDelay;
- state.isContinuousModeEnabled = true;
- }
- if (transformConfig.description !== undefined) {
- state.transformDescription = transformConfig.description;
- }
- if (transformConfig.frequency !== undefined) {
- state.transformFrequency = transformConfig.frequency;
- }
- if (transformConfig.settings) {
- if (typeof transformConfig.settings?.max_page_search_size === 'number') {
- state.transformSettingsMaxPageSearchSize = transformConfig.settings.max_page_search_size;
- }
- if (typeof transformConfig.settings?.docs_per_second === 'number') {
- state.transformSettingsDocsPerSecond = transformConfig.settings.docs_per_second;
- }
- }
- }
- return state;
-}
-
-interface Props {
+interface StepDetailsFormProps {
overrides?: StepDetailsExposedState;
onChange(s: StepDetailsExposedState): void;
searchItems: SearchItems;
stepDefineState: StepDefineExposedState;
}
-export const StepDetailsForm: FC = React.memo(
+export const StepDetailsForm: FC = React.memo(
({ overrides = {}, onChange, searchItems, stepDefineState }) => {
const deps = useAppDependencies();
const toastNotifications = useToastNotifications();
@@ -171,11 +107,6 @@ export const StepDetailsForm: FC = React.memo(
[setIndexPatternTimeField, indexPatternAvailableTimeFields]
);
- // Continuous mode state
- const [isContinuousModeEnabled, setContinuousModeEnabled] = useState(
- defaults.isContinuousModeEnabled
- );
-
const api = useApi();
// fetch existing transform IDs and indices once for form validation
@@ -187,7 +118,8 @@ export const StepDetailsForm: FC = React.memo(
const previewRequest = getPreviewTransformRequestBody(
searchItems.indexPattern.title,
pivotQuery,
- partialPreviewRequest
+ partialPreviewRequest,
+ stepDefineState.runtimeMappings
);
const transformPreview = await api.getTransformsPreview(previewRequest);
@@ -268,13 +200,41 @@ export const StepDetailsForm: FC = React.memo(
.filter((f) => f.type === KBN_FIELD_TYPES.DATE)
.map((f) => f.name)
.sort();
+
+ // Continuous Mode
const isContinuousModeAvailable = dateFieldNames.length > 0;
+ const [isContinuousModeEnabled, setContinuousModeEnabled] = useState(
+ defaults.isContinuousModeEnabled
+ );
const [continuousModeDateField, setContinuousModeDateField] = useState(
isContinuousModeAvailable ? dateFieldNames[0] : ''
);
const [continuousModeDelay, setContinuousModeDelay] = useState(defaults.continuousModeDelay);
const isContinuousModeDelayValid = continuousModeDelayValidator(continuousModeDelay);
+ // Retention Policy
+ const isRetentionPolicyAvailable = dateFieldNames.length > 0;
+ const [isRetentionPolicyEnabled, setRetentionPolicyEnabled] = useState(
+ defaults.isRetentionPolicyEnabled
+ );
+ const [retentionPolicyDateField, setRetentionPolicyDateField] = useState(
+ isRetentionPolicyAvailable ? dateFieldNames[0] : ''
+ );
+ const [retentionPolicyMaxAge, setRetentionPolicyMaxAge] = useState(
+ defaults.retentionPolicyMaxAge
+ );
+ const retentionPolicyMaxAgeEmpty = retentionPolicyMaxAge === '';
+ const isRetentionPolicyMaxAgeValid = retentionPolicyMaxAgeValidator(retentionPolicyMaxAge);
+
+ // Reset retention policy settings when the user disables the whole option
+ useEffect(() => {
+ if (!isRetentionPolicyEnabled) {
+ setRetentionPolicyDateField(isRetentionPolicyAvailable ? dateFieldNames[0] : '');
+ setRetentionPolicyMaxAge('');
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isRetentionPolicyEnabled]);
+
const transformIdExists = transformIds.some((id) => transformId === id);
const transformIdEmpty = transformId === '';
const transformIdValid = isTransformIdValid(transformId);
@@ -305,7 +265,13 @@ export const StepDetailsForm: FC = React.memo(
!indexNameEmpty &&
indexNameValid &&
(!indexPatternTitleExists || !createIndexPattern) &&
- (!isContinuousModeAvailable || (isContinuousModeAvailable && isContinuousModeDelayValid));
+ (!isContinuousModeAvailable || (isContinuousModeAvailable && isContinuousModeDelayValid)) &&
+ (!isRetentionPolicyAvailable ||
+ !isRetentionPolicyEnabled ||
+ (isRetentionPolicyAvailable &&
+ isRetentionPolicyEnabled &&
+ !retentionPolicyMaxAgeEmpty &&
+ isRetentionPolicyMaxAgeValid));
// expose state to wizard
useEffect(() => {
@@ -314,6 +280,9 @@ export const StepDetailsForm: FC = React.memo(
continuousModeDelay,
createIndexPattern,
isContinuousModeEnabled,
+ isRetentionPolicyEnabled,
+ retentionPolicyDateField,
+ retentionPolicyMaxAge,
transformId,
transformDescription,
transformFrequency,
@@ -331,6 +300,9 @@ export const StepDetailsForm: FC = React.memo(
continuousModeDelay,
createIndexPattern,
isContinuousModeEnabled,
+ isRetentionPolicyEnabled,
+ retentionPolicyDateField,
+ retentionPolicyMaxAge,
transformId,
transformDescription,
transformFrequency,
@@ -417,7 +389,7 @@ export const StepDetailsForm: FC = React.memo(
error={
!indexNameEmpty &&
!indexNameValid && [
-
+ <>
{i18n.translate('xpack.transform.stepDetailsForm.destinationIndexInvalidError', {
defaultMessage: 'Invalid destination index name.',
})}
@@ -430,7 +402,7 @@ export const StepDetailsForm: FC = React.memo(
}
)}
- ,
+ >,
]
}
>
@@ -502,6 +474,8 @@ export const StepDetailsForm: FC = React.memo(
onTimeFieldChanged={onTimeFieldChanged}
/>
)}
+
+ {/* Continuous mode */}
= React.memo(
/>
{isContinuousModeEnabled && (
-
+ <>
= React.memo(
)}
>
setContinuousModeDelay(e.target.value)}
aria-label={i18n.translate(
@@ -580,7 +560,100 @@ export const StepDetailsForm: FC = React.memo(
data-test-subj="transformContinuousDelayInput"
/>
-
+ >
+ )}
+
+ {/* Retention policy */}
+
+ setRetentionPolicyEnabled(!isRetentionPolicyEnabled)}
+ disabled={isRetentionPolicyAvailable === false}
+ data-test-subj="transformRetentionPolicySwitch"
+ />
+
+ {isRetentionPolicyEnabled && (
+ <>
+
+ ({ text }))}
+ value={retentionPolicyDateField}
+ onChange={(e) => setRetentionPolicyDateField(e.target.value)}
+ data-test-subj="transformRetentionPolicyDateFieldSelect"
+ />
+
+
+ setRetentionPolicyMaxAge(e.target.value)}
+ aria-label={i18n.translate(
+ 'xpack.transform.stepDetailsForm.retentionPolicyMaxAgeAriaLabel',
+ {
+ defaultMessage: 'Choose a max age.',
+ }
+ )}
+ isInvalid={!retentionPolicyMaxAgeEmpty && !isRetentionPolicyMaxAgeValid}
+ data-test-subj="transformRetentionPolicyMaxAgeInput"
+ />
+
+ >
)}
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx
index 7fb9f8ba06c055..f39132da819859 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx
@@ -11,13 +11,16 @@ import { i18n } from '@kbn/i18n';
import { EuiAccordion, EuiFormRow, EuiSpacer } from '@elastic/eui';
-import { StepDetailsExposedState } from './step_details_form';
+import { StepDetailsExposedState } from './common';
export const StepDetailsSummary: FC = React.memo((props) => {
const {
continuousModeDateField,
createIndexPattern,
isContinuousModeEnabled,
+ isRetentionPolicyEnabled,
+ retentionPolicyDateField,
+ retentionPolicyMaxAge,
transformId,
transformDescription,
transformFrequency,
@@ -85,6 +88,28 @@ export const StepDetailsSummary: FC = React.memo((props
)}
+ {isRetentionPolicyEnabled && (
+ <>
+
+ {retentionPolicyDateField}
+
+
+ {retentionPolicyMaxAge}
+
+ >
+ )}
+
void;
@@ -59,19 +59,17 @@ const cancelButtonText = i18n.translate(
);
export const SwitchModal: FC = ({ onCancel, onConfirm, type }) => (
-
-
- {type === 'pivot' ? pivotModalMessage : sourceModalMessage}
-
-
+
+ {type === 'pivot' ? pivotModalMessage : sourceModalMessage}
+
);
diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx
index 9837ace2720725..5ae464affa0164 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx
@@ -32,6 +32,7 @@ import {
} from '../step_details';
import { WizardNav } from '../wizard_nav';
import { IndexPattern } from '../../../../../../../../../src/plugins/data/public';
+import type { RuntimeMappings } from '../step_define/common/types';
enum KBN_MANAGEMENT_PAGE_CLASSNAME {
DEFAULT_BODY = 'mgtPage__body',
@@ -89,8 +90,12 @@ interface WizardProps {
searchItems: SearchItems;
}
-export const CreateTransformWizardContext = createContext<{ indexPattern: IndexPattern | null }>({
+export const CreateTransformWizardContext = createContext<{
+ indexPattern: IndexPattern | null;
+ runtimeMappings: RuntimeMappings | undefined;
+}>({
indexPattern: null,
+ runtimeMappings: undefined,
});
export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) => {
@@ -239,7 +244,9 @@ export const Wizard: FC = React.memo(({ cloneConfig, searchItems })
const stepsConfig = [stepDefine, stepDetails, stepCreate];
return (
-
+
);
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx
index 148e6c1a3bac0c..d82f0769c8b74c 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx
@@ -12,7 +12,6 @@ import {
EuiConfirmModal,
EuiFlexGroup,
EuiFlexItem,
- EuiOverlayMask,
EuiSpacer,
EuiSwitch,
} from '@elastic/eui';
@@ -123,22 +122,20 @@ export const DeleteActionModal: FC = ({
);
return (
-
-
- {isBulkAction ? bulkDeleteModalContent : deleteModalContent}
-
-
+
+ {isBulkAction ? bulkDeleteModalContent : deleteModalContent}
+
);
};
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_modal.tsx
index c3967dd687a632..bb01fe355a33ec 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_modal.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_modal.tsx
@@ -7,7 +7,7 @@
import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiConfirmModal, EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui';
+import { EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui';
import { StartAction } from './use_start_action';
@@ -24,27 +24,25 @@ export const StartActionModal: FC = ({ closeModal, items, startAndC
});
return (
-
-
+
+ {i18n.translate('xpack.transform.transformList.startModalBody', {
+ defaultMessage:
+ 'A transform increases search and indexing load in your cluster. If excessive load is experienced, stop the transform.',
})}
- confirmButtonText={i18n.translate('xpack.transform.transformList.startModalStartButton', {
- defaultMessage: 'Start',
- })}
- defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
- buttonColor="primary"
- >
-
- {i18n.translate('xpack.transform.transformList.startModalBody', {
- defaultMessage:
- 'A transform increases search and indexing load in your cluster. If excessive load is experienced, stop the transform.',
- })}
-
-
-
+
+
);
};
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx
index 250cf6133dbcf0..62c63d90a17b93 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx
@@ -98,6 +98,46 @@ export const EditTransformFlyoutForm: FC = ({
+
+ dispatch({ field: 'retentionPolicyField', value })}
+ value={formFields.retentionPolicyField.value}
+ />
+
+ dispatch({ field: 'retentionPolicyMaxAge', value })}
+ value={formFields.retentionPolicyMaxAge.value}
+ />
+
+
+
+
{
});
});
-describe('Transfom: stringValidator()', () => {
+describe('Transform: stringValidator()', () => {
it('should allow an empty string for optional fields', () => {
expect(stringValidator('')).toHaveLength(0);
});
@@ -270,6 +271,43 @@ describe('Transform: frequencyValidator()', () => {
});
});
+describe('Transform: retentionPolicyMaxAgeValidator()', () => {
+ const transformRetentionPolicyMaxAgeValidator = (arg: string) =>
+ retentionPolicyMaxAgeValidator(arg).length === 0;
+
+ it('should only allow values equal or above 60s.', () => {
+ expect(transformRetentionPolicyMaxAgeValidator('0nanos')).toBe(false);
+ expect(transformRetentionPolicyMaxAgeValidator('59999999999nanos')).toBe(false);
+ expect(transformRetentionPolicyMaxAgeValidator('60000000000nanos')).toBe(true);
+ expect(transformRetentionPolicyMaxAgeValidator('60000000001nanos')).toBe(true);
+
+ expect(transformRetentionPolicyMaxAgeValidator('0micros')).toBe(false);
+ expect(transformRetentionPolicyMaxAgeValidator('59999999micros')).toBe(false);
+ expect(transformRetentionPolicyMaxAgeValidator('60000000micros')).toBe(true);
+ expect(transformRetentionPolicyMaxAgeValidator('60000001micros')).toBe(true);
+
+ expect(transformRetentionPolicyMaxAgeValidator('0ms')).toBe(false);
+ expect(transformRetentionPolicyMaxAgeValidator('59999ms')).toBe(false);
+ expect(transformRetentionPolicyMaxAgeValidator('60000ms')).toBe(true);
+ expect(transformRetentionPolicyMaxAgeValidator('60001ms')).toBe(true);
+
+ expect(transformRetentionPolicyMaxAgeValidator('0s')).toBe(false);
+ expect(transformRetentionPolicyMaxAgeValidator('1s')).toBe(false);
+ expect(transformRetentionPolicyMaxAgeValidator('59s')).toBe(false);
+ expect(transformRetentionPolicyMaxAgeValidator('60s')).toBe(true);
+ expect(transformRetentionPolicyMaxAgeValidator('61s')).toBe(true);
+ expect(transformRetentionPolicyMaxAgeValidator('10000s')).toBe(true);
+
+ expect(transformRetentionPolicyMaxAgeValidator('0m')).toBe(false);
+ expect(transformRetentionPolicyMaxAgeValidator('1m')).toBe(true);
+ expect(transformRetentionPolicyMaxAgeValidator('100m')).toBe(true);
+
+ expect(transformRetentionPolicyMaxAgeValidator('0h')).toBe(false);
+ expect(transformRetentionPolicyMaxAgeValidator('1h')).toBe(true);
+ expect(transformRetentionPolicyMaxAgeValidator('2h')).toBe(true);
+ });
+});
+
describe('Transform: integerAboveZeroValidator()', () => {
it('should only allow integers above zero', () => {
// integerAboveZeroValidator() returns an array of error messages so
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts
index a86a9cd8012629..6680495bdab912 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts
@@ -16,6 +16,12 @@ import { PostTransformsUpdateRequestSchema } from '../../../../../../common/api_
import { TransformConfigUnion } from '../../../../../../common/types/transform';
import { getNestedProperty, setNestedProperty } from '../../../../../../common/utils/object_utils';
+import {
+ isValidFrequency,
+ isValidRetentionPolicyMaxAge,
+ ParsedDuration,
+} from '../../../../common/validators';
+
// This custom hook uses nested reducers to provide a generic framework to manage form state
// and apply it to a final possibly nested configuration object suitable for passing on
// directly to an API call. For now this is only used for the transform edit form.
@@ -25,21 +31,23 @@ import { getNestedProperty, setNestedProperty } from '../../../../../../common/u
// The outer most level reducer defines a flat structure of names for form fields.
// This is a flat structure regardless of whether the final request object will be nested.
// For example, `destinationIndex` and `destinationPipeline` will later be nested under `dest`.
-interface EditTransformFlyoutFieldsState {
- [key: string]: FormField;
- description: FormField;
- destinationIndex: FormField;
- destinationPipeline: FormField;
- frequency: FormField;
- docsPerSecond: FormField;
-}
+type EditTransformFormFields =
+ | 'description'
+ | 'destinationIndex'
+ | 'destinationPipeline'
+ | 'frequency'
+ | 'docsPerSecond'
+ | 'maxPageSearchSize'
+ | 'retentionPolicyField'
+ | 'retentionPolicyMaxAge';
+type EditTransformFlyoutFieldsState = Record;
// The inner reducers apply validation based on supplied attributes of each field.
export interface FormField {
formFieldName: string;
configFieldName: string;
defaultValue: string;
- dependsOn: string[];
+ dependsOn: EditTransformFormFields[];
errorMessages: string[];
isNullable: boolean;
isOptional: boolean;
@@ -122,14 +130,7 @@ export const stringValidator: Validator = (value, isOptional = true) => {
return [];
};
-// Only allow frequencies in the form of 1s/1h etc.
-const frequencyNotValidErrorMessage = i18n.translate(
- 'xpack.transform.transformList.editFlyoutFormFrequencyNotValidErrorMessage',
- {
- defaultMessage: 'The frequency value is not valid.',
- }
-);
-export const frequencyValidator: Validator = (arg) => {
+function parseDurationAboveZero(arg: any, errorMessage: string): ParsedDuration | string[] {
if (typeof arg !== 'string' || arg === null) {
return [stringNotValidErrorMessage];
}
@@ -142,20 +143,49 @@ export const frequencyValidator: Validator = (arg) => {
return [frequencyNotValidErrorMessage];
}
- const valueNumber = +regexStr[0];
- const valueTimeUnit = regexStr[1];
+ const number = +regexStr[0];
+ const timeUnit = regexStr[1];
// only valid if number is an integer above 0
- if (isNaN(valueNumber) || !Number.isInteger(valueNumber) || valueNumber === 0) {
+ if (isNaN(number) || !Number.isInteger(number) || number === 0) {
return [frequencyNotValidErrorMessage];
}
- // only valid if value is up to 1 hour
- return (valueTimeUnit === 's' && valueNumber <= 3600) ||
- (valueTimeUnit === 'm' && valueNumber <= 60) ||
- (valueTimeUnit === 'h' && valueNumber === 1)
- ? []
- : [frequencyNotValidErrorMessage];
+ return { number, timeUnit };
+}
+
+// Only allow frequencies in the form of 1s/1h etc.
+const frequencyNotValidErrorMessage = i18n.translate(
+ 'xpack.transform.transformList.editFlyoutFormFrequencyNotValidErrorMessage',
+ {
+ defaultMessage: 'The frequency value is not valid.',
+ }
+);
+export const frequencyValidator: Validator = (arg) => {
+ const parsedArg = parseDurationAboveZero(arg, frequencyNotValidErrorMessage);
+
+ if (Array.isArray(parsedArg)) {
+ return parsedArg;
+ }
+
+ return isValidFrequency(parsedArg) ? [] : [frequencyNotValidErrorMessage];
+};
+
+// Retention policy max age validator
+const retentionPolicyMaxAgeNotValidErrorMessage = i18n.translate(
+ 'xpack.transform.transformList.editFlyoutFormRetentionPolicyMaxAgeNotValidErrorMessage',
+ {
+ defaultMessage: 'Invalid max age format. Minimum of 60s required.',
+ }
+);
+export const retentionPolicyMaxAgeValidator: Validator = (arg) => {
+ const parsedArg = parseDurationAboveZero(arg, retentionPolicyMaxAgeNotValidErrorMessage);
+
+ if (Array.isArray(parsedArg)) {
+ return parsedArg;
+ }
+
+ return isValidRetentionPolicyMaxAge(parsedArg) ? [] : [retentionPolicyMaxAgeNotValidErrorMessage];
};
const validate = {
@@ -163,10 +193,11 @@ const validate = {
frequency: frequencyValidator,
integerAboveZero: integerAboveZeroValidator,
integerRange10To10000: integerRange10To10000Validator,
+ retentionPolicyMaxAge: retentionPolicyMaxAgeValidator,
} as const;
export const initializeField = (
- formFieldName: string,
+ formFieldName: EditTransformFormFields,
configFieldName: string,
config: TransformConfigUnion,
overloads?: Partial
@@ -199,7 +230,7 @@ export interface EditTransformFlyoutState {
// This is not a redux type action,
// since for now we only have one action type.
interface Action {
- field: keyof EditTransformFlyoutFieldsState;
+ field: EditTransformFormFields;
value: string;
}
@@ -207,7 +238,7 @@ interface Action {
// of the expected final configuration request object.
// Considers options like if a value is nullable or optional.
const getUpdateValue = (
- attribute: keyof EditTransformFlyoutFieldsState,
+ attribute: EditTransformFormFields,
config: TransformConfigUnion,
formState: EditTransformFlyoutFieldsState,
enforceFormValue = false
@@ -251,7 +282,7 @@ export const applyFormFieldsToTransformConfig = (
): PostTransformsUpdateRequestSchema =>
// Iterates over all form fields and only if necessary applies them to
// the request object used for updating the transform.
- Object.keys(formState).reduce(
+ (Object.keys(formState) as EditTransformFormFields[]).reduce(
(updateConfig, field) => merge({ ...updateConfig }, getUpdateValue(field, config, formState)),
{}
);
@@ -292,6 +323,25 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo
valueParser: (v) => +v,
}
),
+
+ // retention_policy.*
+ retentionPolicyField: initializeField(
+ 'retentionPolicyField',
+ 'retention_policy.time.field',
+ config,
+ { dependsOn: ['retentionPolicyMaxAge'], isNullable: false, isOptional: true }
+ ),
+ retentionPolicyMaxAge: initializeField(
+ 'retentionPolicyMaxAge',
+ 'retention_policy.time.max_age',
+ config,
+ {
+ dependsOn: ['retentionPolicyField'],
+ isNullable: false,
+ isOptional: true,
+ validator: 'retentionPolicyMaxAge',
+ }
+ ),
},
isFormTouched: false,
isFormValid: true,
@@ -300,7 +350,10 @@ export const getDefaultState = (config: TransformConfigUnion): EditTransformFlyo
// Checks each form field for error messages to return
// if the overall form is valid or not.
const isFormValid = (fieldsState: EditTransformFlyoutFieldsState) =>
- Object.keys(fieldsState).reduce((p, c) => p && fieldsState[c].errorMessages.length === 0, true);
+ (Object.keys(fieldsState) as EditTransformFormFields[]).reduce(
+ (p, c) => p && fieldsState[c].errorMessages.length === 0,
+ true
+ );
// Updates a form field with its new value,
// runs validation and populates
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx
index 2ee558d449c9ac..87ae90afdf9c97 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx
@@ -29,7 +29,7 @@ export const ExpandedRowPreviewPane: FC = ({ transf
} = useAppDependencies();
const toastNotifications = useToastNotifications();
- const { searchQuery, validationStatus, previewRequest } = useMemo(
+ const { searchQuery, validationStatus, previewRequest, runtimeMappings } = useMemo(
() =>
applyTransformConfigToDefineState(
getDefaultStepDefineState({} as SearchItems),
@@ -48,7 +48,8 @@ export const ExpandedRowPreviewPane: FC = ({ transf
indexPatternTitle,
pivotQuery,
validationStatus,
- previewRequest
+ previewRequest,
+ runtimeMappings
);
return (
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx
index b84d7fc433dfc1..bcb07c8069ab2f 100644
--- a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx
+++ b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx
@@ -14,7 +14,6 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiModal,
- EuiOverlayMask,
EuiPageContent,
EuiPageContentBody,
EuiSpacer,
@@ -124,15 +123,13 @@ export const TransformManagement: FC = () => {
{isSearchSelectionVisible && (
-
-
-
-
-
+
+
+
)}
);
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index a408d2d8d76d8e..d60729aeb055f8 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -646,7 +646,6 @@
"dashboard.savedDashboard.newDashboardTitle": "新規ダッシュボード",
"dashboard.stateManager.timeNotSavedWithDashboardErrorMessage": "このダッシュボードに時刻が保存されていないため、同期できません。",
"dashboard.strings.dashboardEditTitle": "{title}を編集中",
- "dashboard.strings.dashboardUnsavedEditTitle": "{title}を編集中(未保存)",
"dashboard.topNav.cloneModal.cancelButtonLabel": "キャンセル",
"dashboard.topNav.cloneModal.cloneDashboardModalHeaderTitle": "ダッシュボードのクローンを作成",
"dashboard.topNav.cloneModal.confirmButtonLabel": "クローンの確認",
@@ -892,7 +891,6 @@
"data.noDataPopover.subtitle": "ヒント",
"data.noDataPopover.title": "空のデータセット",
"data.painlessError.buttonTxt": "スクリプトを編集",
- "data.painlessError.painlessScriptedFieldErrorMessage": "Painlessスクリプトの実行エラー:「{script}」。",
"data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "無効なカレンダー間隔:{interval}、1よりも大きな値が必要です",
"data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "無効な間隔形式:{interval}",
"data.query.queryBar.comboboxAriaLabel": "{pageType} ページの検索とフィルタリング",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 2ce673ef9c85b8..4643e64eb6b10b 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -646,7 +646,6 @@
"dashboard.savedDashboard.newDashboardTitle": "新建仪表板",
"dashboard.stateManager.timeNotSavedWithDashboardErrorMessage": "时间未随此仪表板保存,因此无法同步。",
"dashboard.strings.dashboardEditTitle": "正在编辑 {title}",
- "dashboard.strings.dashboardUnsavedEditTitle": "正在编辑 {title}(未保存)",
"dashboard.topNav.cloneModal.cancelButtonLabel": "取消",
"dashboard.topNav.cloneModal.cloneDashboardModalHeaderTitle": "克隆仪表板",
"dashboard.topNav.cloneModal.confirmButtonLabel": "确认克隆",
@@ -892,7 +891,6 @@
"data.noDataPopover.subtitle": "提示",
"data.noDataPopover.title": "空数据集",
"data.painlessError.buttonTxt": "编辑脚本",
- "data.painlessError.painlessScriptedFieldErrorMessage": "执行 Painless 脚本时出错:“{script}”。",
"data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "无效的日历时间间隔:{interval},值必须为 1",
"data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "时间间隔格式无效:{interval}",
"data.query.queryBar.comboboxAriaLabel": "搜索并筛选 {pageType} 页面",
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/delete_modal_confirmation.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/delete_modal_confirmation.tsx
index 952ea07ba05c33..b98db1178f4623 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/delete_modal_confirmation.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/delete_modal_confirmation.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
+import { EuiConfirmModal } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useEffect, useState } from 'react';
import { HttpSetup } from 'kibana/public';
@@ -73,56 +73,54 @@ export const DeleteModalConfirmation = ({
}
);
return (
-
- {
- setDeleteModalVisibility(false);
- onCancel();
- }}
- onConfirm={async () => {
- setDeleteModalVisibility(false);
- setIsLoadingState(true);
- const { successes, errors } = await apiDeleteCall({ ids: idsToDelete, http });
- setIsLoadingState(false);
+ {
+ setDeleteModalVisibility(false);
+ onCancel();
+ }}
+ onConfirm={async () => {
+ setDeleteModalVisibility(false);
+ setIsLoadingState(true);
+ const { successes, errors } = await apiDeleteCall({ ids: idsToDelete, http });
+ setIsLoadingState(false);
- const numSuccesses = successes.length;
- const numErrors = errors.length;
- if (numSuccesses > 0) {
- toasts.addSuccess(
- i18n.translate(
- 'xpack.triggersActionsUI.components.deleteSelectedIdsSuccessNotification.descriptionText',
- {
- defaultMessage:
- 'Deleted {numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}',
- values: { numSuccesses, singleTitle, multipleTitle },
- }
- )
- );
- }
+ const numSuccesses = successes.length;
+ const numErrors = errors.length;
+ if (numSuccesses > 0) {
+ toasts.addSuccess(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.deleteSelectedIdsSuccessNotification.descriptionText',
+ {
+ defaultMessage:
+ 'Deleted {numSuccesses, number} {numSuccesses, plural, one {{singleTitle}} other {{multipleTitle}}}',
+ values: { numSuccesses, singleTitle, multipleTitle },
+ }
+ )
+ );
+ }
- if (numErrors > 0) {
- toasts.addDanger(
- i18n.translate(
- 'xpack.triggersActionsUI.components.deleteSelectedIdsErrorNotification.descriptionText',
- {
- defaultMessage:
- 'Failed to delete {numErrors, number} {numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}',
- values: { numErrors, singleTitle, multipleTitle },
- }
- )
- );
- await onErrors();
- }
- await onDeleted(successes);
- }}
- cancelButtonText={cancelButtonText}
- confirmButtonText={confirmButtonText}
- >
- {confirmModalText}
-
-
+ if (numErrors > 0) {
+ toasts.addDanger(
+ i18n.translate(
+ 'xpack.triggersActionsUI.components.deleteSelectedIdsErrorNotification.descriptionText',
+ {
+ defaultMessage:
+ 'Failed to delete {numErrors, number} {numErrors, plural, one {{singleTitle}} other {{multipleTitle}}}',
+ values: { numErrors, singleTitle, multipleTitle },
+ }
+ )
+ );
+ await onErrors();
+ }
+ await onDeleted(successes);
+ }}
+ cancelButtonText={cancelButtonText}
+ confirmButtonText={confirmButtonText}
+ >
+ {confirmModalText}
+
);
};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts
index 0e848d8cc07882..0dca49ea5353d4 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.test.ts
@@ -44,10 +44,18 @@ describe('transformActionVariables', () => {
"description": "The alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroup",
},
+ Object {
+ "description": "The alert action subgroup that was used to scheduled actions for the alert.",
+ "name": "alertActionSubgroup",
+ },
Object {
"description": "The human readable name of the alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroupName",
},
+ Object {
+ "description": "The configured server.publicBaseUrl value or empty string if not configured.",
+ "name": "kibanaBaseUrl",
+ },
]
`);
});
@@ -91,10 +99,18 @@ describe('transformActionVariables', () => {
"description": "The alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroup",
},
+ Object {
+ "description": "The alert action subgroup that was used to scheduled actions for the alert.",
+ "name": "alertActionSubgroup",
+ },
Object {
"description": "The human readable name of the alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroupName",
},
+ Object {
+ "description": "The configured server.publicBaseUrl value or empty string if not configured.",
+ "name": "kibanaBaseUrl",
+ },
Object {
"description": "foo-description",
"name": "context.foo",
@@ -146,10 +162,18 @@ describe('transformActionVariables', () => {
"description": "The alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroup",
},
+ Object {
+ "description": "The alert action subgroup that was used to scheduled actions for the alert.",
+ "name": "alertActionSubgroup",
+ },
Object {
"description": "The human readable name of the alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroupName",
},
+ Object {
+ "description": "The configured server.publicBaseUrl value or empty string if not configured.",
+ "name": "kibanaBaseUrl",
+ },
Object {
"description": "foo-description",
"name": "state.foo",
@@ -204,10 +228,18 @@ describe('transformActionVariables', () => {
"description": "The alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroup",
},
+ Object {
+ "description": "The alert action subgroup that was used to scheduled actions for the alert.",
+ "name": "alertActionSubgroup",
+ },
Object {
"description": "The human readable name of the alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroupName",
},
+ Object {
+ "description": "The configured server.publicBaseUrl value or empty string if not configured.",
+ "name": "kibanaBaseUrl",
+ },
Object {
"description": "fooC-description",
"name": "context.fooC",
@@ -280,10 +312,18 @@ describe('transformActionVariables', () => {
"description": "The alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroup",
},
+ Object {
+ "description": "The alert action subgroup that was used to scheduled actions for the alert.",
+ "name": "alertActionSubgroup",
+ },
Object {
"description": "The human readable name of the alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroupName",
},
+ Object {
+ "description": "The configured server.publicBaseUrl value or empty string if not configured.",
+ "name": "kibanaBaseUrl",
+ },
Object {
"description": "fooC-description",
"name": "context.fooC",
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts
index 65c1145b6d1d97..92be6a8685c7c8 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_variables.ts
@@ -88,6 +88,17 @@ function getAlwaysProvidedActionVariables(): ActionVariable[] {
}),
});
+ result.push({
+ name: 'alertActionSubgroup',
+ description: i18n.translate(
+ 'xpack.triggersActionsUI.actionVariables.alertActionSubgroupLabel',
+ {
+ defaultMessage:
+ 'The alert action subgroup that was used to scheduled actions for the alert.',
+ }
+ ),
+ });
+
result.push({
name: 'alertActionGroupName',
description: i18n.translate(
@@ -99,5 +110,13 @@ function getAlwaysProvidedActionVariables(): ActionVariable[] {
),
});
+ result.push({
+ name: 'kibanaBaseUrl',
+ description: i18n.translate('xpack.triggersActionsUI.actionVariables.kibanaBaseUrlLabel', {
+ defaultMessage:
+ 'The configured server.publicBaseUrl value or empty string if not configured.',
+ }),
+ });
+
return result;
}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx
index b7450d742bc45b..8732727b9a77a8 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx
@@ -7,17 +7,19 @@
import React, { useCallback, useMemo, useReducer, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiTitle, EuiFlexItem, EuiIcon, EuiFlexGroup } from '@elastic/eui';
import {
EuiModal,
EuiButton,
+ EuiButtonEmpty,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalBody,
EuiModalFooter,
+ EuiTitle,
+ EuiFlexItem,
+ EuiIcon,
+ EuiFlexGroup,
} from '@elastic/eui';
-import { EuiButtonEmpty } from '@elastic/eui';
-import { EuiOverlayMask } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ActionConnectorForm, getConnectorErrors } from './action_connector_form';
import { createConnectorReducer, InitialConnector, ConnectorReducer } from './connector_reducer';
@@ -127,92 +129,90 @@ export const ConnectorAddModal = ({
});
return (
-
-
-
-
-
- {actionTypeModel && actionTypeModel.iconClass ? (
-
-
-
- ) : null}
-
-
-
-
-
-
+
+
+
+
+ {actionTypeModel && actionTypeModel.iconClass ? (
+
+
-
-
-
+ ) : null}
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
- {i18n.translate(
- 'xpack.triggersActionsUI.sections.addModalConnectorForm.cancelButtonLabel',
- {
- defaultMessage: 'Cancel',
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.triggersActionsUI.sections.addModalConnectorForm.cancelButtonLabel',
+ {
+ defaultMessage: 'Cancel',
+ }
+ )}
+
+ {canSave ? (
+ {
+ if (hasErrors) {
+ setConnector(
+ getConnectorWithInvalidatedFields(
+ connector,
+ configErrors,
+ secretsErrors,
+ connectorBaseErrors
+ )
+ );
+ return;
}
- )}
-
- {canSave ? (
- {
- if (hasErrors) {
- setConnector(
- getConnectorWithInvalidatedFields(
- connector,
- configErrors,
- secretsErrors,
- connectorBaseErrors
- )
- );
- return;
- }
- setIsSaving(true);
- const savedAction = await onActionConnectorSave();
- setIsSaving(false);
- if (savedAction) {
- if (postSaveEventHandler) {
- postSaveEventHandler(savedAction);
- }
- closeModal();
+ setIsSaving(true);
+ const savedAction = await onActionConnectorSave();
+ setIsSaving(false);
+ if (savedAction) {
+ if (postSaveEventHandler) {
+ postSaveEventHandler(savedAction);
}
- }}
- >
-
-
- ) : null}
-
-
-
+ closeModal();
+ }
+ }}
+ >
+
+
+ ) : null}
+
+
);
};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_close.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_close.tsx
index 9ef7e414d505e9..6d71fe858f1c12 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_close.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_close.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui';
+import { EuiConfirmModal } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@@ -17,38 +17,36 @@ interface Props {
export const ConfirmAlertClose: React.FC = ({ onConfirm, onCancel }) => {
return (
-
-
-
-
-
-
-
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_save.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_save.tsx
index 48d4229bb9b303..c406ec7c802837 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_save.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_save.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui';
+import { EuiConfirmModal } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@@ -17,38 +17,36 @@ interface Props {
export const ConfirmAlertSave: React.FC = ({ onConfirm, onCancel }) => {
return (
-
-
-
-
-
-
-
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/manage_license_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/manage_license_modal.tsx
index f13e5fd96d2ad8..4a5739c8b44309 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/manage_license_modal.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/manage_license_modal.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui';
+import { EuiConfirmModal } from '@elastic/eui';
import { capitalize } from 'lodash';
interface Props {
@@ -26,37 +26,35 @@ export const ManageLicenseModal: React.FC = ({
}) => {
const licenseRequired = capitalize(licenseType);
return (
-
-
-
-
-
-
-
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap
index 238ce6c3f9ceec..b689ca7ff56f0e 100644
--- a/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap
+++ b/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap
@@ -50,6 +50,17 @@ exports[`DonutChart component passes correct props without errors for valid prop
"strokeWidth": 1,
"visible": true,
},
+ "axisPanelTitle": Object {
+ "fill": "#333",
+ "fontFamily": "sans-serif",
+ "fontSize": 10,
+ "fontStyle": "bold",
+ "padding": Object {
+ "inner": 8,
+ "outer": 0,
+ },
+ "visible": true,
+ },
"axisTitle": Object {
"fill": "#333",
"fontFamily": "sans-serif",
@@ -331,6 +342,14 @@ exports[`DonutChart component passes correct props without errors for valid prop
"band": Object {
"fill": "rgba(245, 247, 250, 1)",
},
+ "crossLine": Object {
+ "dash": Array [
+ 4,
+ 4,
+ ],
+ "stroke": "rgba(105, 112, 125, 1)",
+ "strokeWidth": 1,
+ },
"line": Object {
"dash": Array [
4,
diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/confirm_delete.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/confirm_delete.test.tsx.snap
index d83e45fea1aece..9d670158bc53a7 100644
--- a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/confirm_delete.test.tsx.snap
+++ b/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/confirm_delete.test.tsx.snap
@@ -1,58 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ML Confirm Job Delete shallow renders without errors 1`] = `
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
`;
exports[`ML Confirm Job Delete shallow renders without errors while loading 1`] = `
-
-
-
-
- )
-
-
+
+
-
-
+ )
+
+
+
`;
diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_flyout.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_flyout.test.tsx.snap
index fd59b14520ce17..23feec1e5181c7 100644
--- a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_flyout.test.tsx.snap
+++ b/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_flyout.test.tsx.snap
@@ -84,7 +84,7 @@ exports[`ML Flyout component shows license info if no ml available 1`] = `
data-eui="EuiFocusTrap"
>
= ({ onConfirm, onCancel }) => {
return (
-
-
-
-
-
-
-
+
+
+
+
+
);
};
diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx
index ef95f6a14ed9e5..96c67227cabafb 100644
--- a/x-pack/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx
+++ b/x-pack/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import { EuiOverlayMask, EuiConfirmModal, EuiLoadingSpinner } from '@elastic/eui';
+import { EuiConfirmModal, EuiLoadingSpinner } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import * as labels from './translations';
@@ -18,45 +18,43 @@ interface Props {
export const ConfirmJobDeletion: React.FC
= ({ loading, onConfirm, onCancel }) => {
return (
-
-
- {!loading ? (
-
-
-
- ) : (
-
-
- )
-
- )}
- {!loading ? (
-
-
+ {!loading ? (
+
+
+
+ ) : (
+
+
+ )
+
+ )}
+ {!loading ? (
+
+
-
- ) : (
-
- )}
-
-
+ />
+
+ ) : (
+
+ )}
+
);
};
diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.tsx
index d1401c64a89251..444198bfa9412b 100644
--- a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.tsx
+++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/enable_alert.tsx
@@ -42,7 +42,7 @@ export const EnableMonitorAlert = ({ monitorId, monitorName }: Props) => {
const { data: deletedAlertId } = useSelector(isAlertDeletedSelector);
- const { data: newAlert } = useSelector(newAlertSelector);
+ const { data: newAlert, error: newAlertError } = useSelector(newAlertSelector);
const isNewAlert = newAlert?.params.search.includes(monitorId);
@@ -85,7 +85,7 @@ export const EnableMonitorAlert = ({ monitorId, monitorName }: Props) => {
useEffect(() => {
setIsLoading(false);
- }, [hasAlert, deletedAlertId]);
+ }, [hasAlert, deletedAlertId, newAlertError]);
const hasDefaultConnectors = (settings?.defaultConnectors ?? []).length > 0;
diff --git a/x-pack/plugins/watcher/public/application/components/confirm_watches_modal.tsx b/x-pack/plugins/watcher/public/application/components/confirm_watches_modal.tsx
index 88cb04a0362980..04114c87e61d6c 100644
--- a/x-pack/plugins/watcher/public/application/components/confirm_watches_modal.tsx
+++ b/x-pack/plugins/watcher/public/application/components/confirm_watches_modal.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
+import { EuiConfirmModal } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
@@ -26,29 +26,27 @@ export const ConfirmWatchesModal = ({
}
const { title, message, buttonType, buttonLabel } = modalOptions;
return (
-
- callback()}
- onConfirm={() => {
- callback(true);
- }}
- cancelButtonText={i18n.translate(
- 'xpack.watcher.sections.watchEdit.json.saveConfirmModal.cancelButtonLabel',
- { defaultMessage: 'Cancel' }
- )}
- confirmButtonText={
- buttonLabel
- ? buttonLabel
- : i18n.translate(
- 'xpack.watcher.sections.watchEdit.json.saveConfirmModal.saveButtonLabel',
- { defaultMessage: 'Save watch' }
- )
- }
- >
- {message}
-
-
+ callback()}
+ onConfirm={() => {
+ callback(true);
+ }}
+ cancelButtonText={i18n.translate(
+ 'xpack.watcher.sections.watchEdit.json.saveConfirmModal.cancelButtonLabel',
+ { defaultMessage: 'Cancel' }
+ )}
+ confirmButtonText={
+ buttonLabel
+ ? buttonLabel
+ : i18n.translate(
+ 'xpack.watcher.sections.watchEdit.json.saveConfirmModal.saveButtonLabel',
+ { defaultMessage: 'Save watch' }
+ )
+ }
+ >
+ {message}
+
);
};
diff --git a/x-pack/plugins/watcher/public/application/components/delete_watches_modal.tsx b/x-pack/plugins/watcher/public/application/components/delete_watches_modal.tsx
index c9d8d3fb545c52..844e210de26f1a 100644
--- a/x-pack/plugins/watcher/public/application/components/delete_watches_modal.tsx
+++ b/x-pack/plugins/watcher/public/application/components/delete_watches_modal.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
+import { EuiConfirmModal } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { deleteWatches } from '../lib/api';
@@ -45,48 +45,46 @@ export const DeleteWatchesModal = ({
}
);
return (
-
- callback()}
- onConfirm={async () => {
- const { successes, errors } = await deleteWatches(watchesToDelete);
- const numSuccesses = successes.length;
- const numErrors = errors.length;
- callback(successes);
- if (numSuccesses > 0) {
- toasts.addSuccess(
- i18n.translate(
- 'xpack.watcher.sections.watchList.deleteSelectedWatchesSuccessNotification.descriptionText',
- {
- defaultMessage:
- 'Deleted {numSuccesses, number} {numSuccesses, plural, one {watch} other {watches}}',
- values: { numSuccesses },
- }
- )
- );
- }
+ callback()}
+ onConfirm={async () => {
+ const { successes, errors } = await deleteWatches(watchesToDelete);
+ const numSuccesses = successes.length;
+ const numErrors = errors.length;
+ callback(successes);
+ if (numSuccesses > 0) {
+ toasts.addSuccess(
+ i18n.translate(
+ 'xpack.watcher.sections.watchList.deleteSelectedWatchesSuccessNotification.descriptionText',
+ {
+ defaultMessage:
+ 'Deleted {numSuccesses, number} {numSuccesses, plural, one {watch} other {watches}}',
+ values: { numSuccesses },
+ }
+ )
+ );
+ }
- if (numErrors > 0) {
- toasts.addDanger(
- i18n.translate(
- 'xpack.watcher.sections.watchList.deleteSelectedWatchesErrorNotification.descriptionText',
- {
- defaultMessage:
- 'Failed to delete {numErrors, number} {numErrors, plural, one {watch} other {watches}}',
- values: { numErrors },
- }
- )
- );
- }
- }}
- cancelButtonText={cancelButtonText}
- confirmButtonText={confirmButtonText}
- >
- {confirmModalText}
-
-
+ if (numErrors > 0) {
+ toasts.addDanger(
+ i18n.translate(
+ 'xpack.watcher.sections.watchList.deleteSelectedWatchesErrorNotification.descriptionText',
+ {
+ defaultMessage:
+ 'Failed to delete {numErrors, number} {numErrors, plural, one {watch} other {watches}}',
+ values: { numErrors },
+ }
+ )
+ );
+ }
+ }}
+ cancelButtonText={cancelButtonText}
+ confirmButtonText={confirmButtonText}
+ >
+ {confirmModalText}
+
);
};
diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts
index 40a5902a32f68e..385edbc66a6d83 100644
--- a/x-pack/test/alerting_api_integration/common/config.ts
+++ b/x-pack/test/alerting_api_integration/common/config.ts
@@ -19,6 +19,7 @@ interface CreateTestConfigOptions {
ssl?: boolean;
enableActionsProxy: boolean;
rejectUnauthorized?: boolean;
+ publicBaseUrl?: boolean;
}
// test.not-enabled is specifically not enabled
@@ -97,7 +98,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
...xPackApiIntegrationTestsConfig.get('kbnTestServer'),
serverArgs: [
...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'),
- '--server.publicBaseUrl=https://localhost:5601',
+ ...(options.publicBaseUrl ? ['--server.publicBaseUrl=https://localhost:5601'] : []),
`--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`,
'--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"',
'--xpack.alerts.invalidateApiKeysTask.interval="15s"',
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/config.ts b/x-pack/test/alerting_api_integration/security_and_spaces/config.ts
index 2a6ce1c9fe51d1..314f65c167048a 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/config.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/config.ts
@@ -13,4 +13,5 @@ export default createTestConfig('security_and_spaces', {
license: 'trial',
ssl: true,
enableActionsProxy: true,
+ publicBaseUrl: true,
});
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts
index 9a66db05ec7164..c1f65fab3669e8 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts
@@ -51,6 +51,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
loadTestFile(require.resolve('./update_api_key'));
loadTestFile(require.resolve('./alerts'));
loadTestFile(require.resolve('./event_log'));
+ loadTestFile(require.resolve('./mustache_templates'));
});
});
}
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mustache_templates.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mustache_templates.ts
new file mode 100644
index 00000000000000..6ac50d84857281
--- /dev/null
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/mustache_templates.ts
@@ -0,0 +1,125 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+/*
+ * These tests ensure that the per-action mustache template escaping works
+ * for actions we have simulators for. It arranges to have an alert that
+ * schedules an action that will contain "escapable" characters in it, and
+ * then validates that the simulator receives the escaped versions.
+ */
+
+import http from 'http';
+import getPort from 'get-port';
+import axios from 'axios';
+import httpProxy from 'http-proxy';
+
+import expect from '@kbn/expect';
+import { Spaces } from '../../scenarios';
+import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib';
+import { FtrProviderContext } from '../../../common/ftr_provider_context';
+import { getSlackServer } from '../../../common/fixtures/plugins/actions_simulators/server/plugin';
+import { getHttpProxyServer } from '../../../common/lib/get_proxy_server';
+
+// eslint-disable-next-line import/no-default-export
+export default function executionStatusAlertTests({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const retry = getService('retry');
+ const configService = getService('config');
+
+ describe('mustacheTemplates', () => {
+ const objectRemover = new ObjectRemover(supertest);
+ let slackSimulatorURL: string = '';
+ let slackServer: http.Server;
+ let proxyServer: httpProxy | undefined;
+
+ before(async () => {
+ slackServer = await getSlackServer();
+ const availablePort = await getPort({ port: getPort.makeRange(9000, 9100) });
+ if (!slackServer.listening) {
+ slackServer.listen(availablePort);
+ }
+ slackSimulatorURL = `http://localhost:${availablePort}`;
+
+ proxyServer = await getHttpProxyServer(
+ slackSimulatorURL,
+ configService.get('kbnTestServer.serverArgs'),
+ () => {}
+ );
+ });
+
+ after(async () => {
+ await objectRemover.removeAll();
+ slackServer.close();
+
+ if (proxyServer) {
+ proxyServer.close();
+ }
+ });
+
+ it('should render kibanaBaseUrl as non-empty string since configured', async () => {
+ const actionResponse = await supertest
+ .post(`${getUrlPrefix(Spaces[0].id)}/api/actions/action`)
+ .set('kbn-xsrf', 'test')
+ .send({
+ name: 'testing context variable expansion',
+ actionTypeId: '.slack',
+ secrets: {
+ webhookUrl: slackSimulatorURL,
+ },
+ });
+ expect(actionResponse.status).to.eql(200);
+ const createdAction = actionResponse.body;
+ objectRemover.add(Spaces[0].id, createdAction.id, 'action', 'actions');
+
+ const varsTemplate = 'kibanaBaseUrl: "{{kibanaBaseUrl}}"';
+
+ const alertResponse = await supertest
+ .post(`${getUrlPrefix(Spaces[0].id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ name: 'testing context variable kibanaBaseUrl',
+ alertTypeId: 'test.patternFiring',
+ params: {
+ pattern: { instance: [true, true] },
+ },
+ actions: [
+ {
+ id: createdAction.id,
+ group: 'default',
+ params: {
+ message: `message {{alertId}} - ${varsTemplate}`,
+ },
+ },
+ ],
+ })
+ );
+ expect(alertResponse.status).to.eql(200);
+ const createdAlert = alertResponse.body;
+ objectRemover.add(Spaces[0].id, createdAlert.id, 'alert', 'alerts');
+
+ const body = await retry.try(async () =>
+ waitForActionBody(slackSimulatorURL, createdAlert.id)
+ );
+ expect(body).to.be('kibanaBaseUrl: "https://localhost:5601"');
+ });
+ });
+
+ async function waitForActionBody(url: string, id: string): Promise {
+ const response = await axios.get(url);
+ expect(response.status).to.eql(200);
+
+ for (const datum of response.data) {
+ const match = datum.match(/^(.*) - (.*)$/);
+ if (match == null) continue;
+
+ if (match[1] === id) return match[2];
+ }
+
+ throw new Error(`no action body posted yet for id ${id}`);
+ }
+}
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mustache_templates.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mustache_templates.ts
index 7b3464b6de62cf..e85f9b03d269a0 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mustache_templates.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/mustache_templates.ts
@@ -217,6 +217,54 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
'{"objectA":{"stringB":"B","arrayC":[{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}],"objectF":{"stringG":"G","nullG":null}},"stringH":"H","arrayI":[44,45],"nullJ":null}'
);
});
+
+ it('should render kibanaBaseUrl as empty string since not configured', async () => {
+ const actionResponse = await supertest
+ .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`)
+ .set('kbn-xsrf', 'test')
+ .send({
+ name: 'testing context variable expansion',
+ actionTypeId: '.slack',
+ secrets: {
+ webhookUrl: slackSimulatorURL,
+ },
+ });
+ expect(actionResponse.status).to.eql(200);
+ const createdAction = actionResponse.body;
+ objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions');
+
+ const varsTemplate = 'kibanaBaseUrl: "{{kibanaBaseUrl}}"';
+
+ const alertResponse = await supertest
+ .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`)
+ .set('kbn-xsrf', 'foo')
+ .send(
+ getTestAlertData({
+ name: 'testing context variable kibanaBaseUrl',
+ alertTypeId: 'test.patternFiring',
+ params: {
+ pattern: { instance: [true, true] },
+ },
+ actions: [
+ {
+ id: createdAction.id,
+ group: 'default',
+ params: {
+ message: `message {{alertId}} - ${varsTemplate}`,
+ },
+ },
+ ],
+ })
+ );
+ expect(alertResponse.status).to.eql(200);
+ const createdAlert = alertResponse.body;
+ objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts');
+
+ const body = await retry.try(async () =>
+ waitForActionBody(slackSimulatorURL, createdAlert.id)
+ );
+ expect(body).to.be('kibanaBaseUrl: ""');
+ });
});
async function waitForActionBody(url: string, id: string): Promise {
diff --git a/x-pack/test/api_integration/apis/lens/field_stats.ts b/x-pack/test/api_integration/apis/lens/field_stats.ts
index 2cfce5ef31305a..ac4ebb4e5b02c2 100644
--- a/x-pack/test/api_integration/apis/lens/field_stats.ts
+++ b/x-pack/test/api_integration/apis/lens/field_stats.ts
@@ -23,10 +23,12 @@ export default ({ getService }: FtrProviderContext) => {
before(async () => {
await esArchiver.loadIfNeeded('logstash_functional');
await esArchiver.loadIfNeeded('visualize/default');
+ await esArchiver.loadIfNeeded('pre_calculated_histogram');
});
after(async () => {
await esArchiver.unload('logstash_functional');
await esArchiver.unload('visualize/default');
+ await esArchiver.unload('pre_calculated_histogram');
});
describe('field distribution', () => {
@@ -347,6 +349,101 @@ export default ({ getService }: FtrProviderContext) => {
});
});
+ it('should return an auto histogram for precalculated histograms', async () => {
+ const { body } = await supertest
+ .post('/api/lens/index_stats/histogram-test/field')
+ .set(COMMON_HEADERS)
+ .send({
+ dslQuery: { match_all: {} },
+ fromDate: TEST_START_TIME,
+ toDate: TEST_END_TIME,
+ field: {
+ name: 'histogram-content',
+ type: 'histogram',
+ },
+ })
+ .expect(200);
+
+ expect(body).to.eql({
+ histogram: {
+ buckets: [
+ {
+ count: 237,
+ key: 0,
+ },
+ {
+ count: 323,
+ key: 0.47000000000000003,
+ },
+ {
+ count: 454,
+ key: 0.9400000000000001,
+ },
+ {
+ count: 166,
+ key: 1.4100000000000001,
+ },
+ {
+ count: 168,
+ key: 1.8800000000000001,
+ },
+ {
+ count: 425,
+ key: 2.35,
+ },
+ {
+ count: 311,
+ key: 2.8200000000000003,
+ },
+ {
+ count: 391,
+ key: 3.29,
+ },
+ {
+ count: 406,
+ key: 3.7600000000000002,
+ },
+ {
+ count: 324,
+ key: 4.23,
+ },
+ {
+ count: 628,
+ key: 4.7,
+ },
+ ],
+ },
+ sampledDocuments: 7,
+ sampledValues: 3833,
+ totalDocuments: 7,
+ topValues: { buckets: [] },
+ });
+ });
+
+ it('should return a single-value histogram when filtering a precalculated histogram', async () => {
+ const { body } = await supertest
+ .post('/api/lens/index_stats/histogram-test/field')
+ .set(COMMON_HEADERS)
+ .send({
+ dslQuery: { match: { 'histogram-title': 'single value' } },
+ fromDate: TEST_START_TIME,
+ toDate: TEST_END_TIME,
+ field: {
+ name: 'histogram-content',
+ type: 'histogram',
+ },
+ })
+ .expect(200);
+
+ expect(body).to.eql({
+ histogram: { buckets: [{ count: 1, key: 1 }] },
+ sampledDocuments: 1,
+ sampledValues: 1,
+ totalDocuments: 1,
+ topValues: { buckets: [] },
+ });
+ });
+
it('should return histograms for scripted date fields', async () => {
const { body } = await supertest
.post('/api/lens/index_stats/logstash-2015.09.22/field')
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts
index a735eba6693fed..a9120bde274f24 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts
@@ -118,7 +118,7 @@ export default ({ getService }: FtrProviderContext) => {
expect(statusBody[body.id].current_status.status).to.eql('succeeded');
});
- it('should create a single rule with a rule_id and an index pattern that does not match anything available and fail the rule', async () => {
+ it('should create a single rule with a rule_id and an index pattern that does not match anything available and warning for the rule', async () => {
const simpleRule = getRuleForSignalTesting(['does-not-exist-*']);
const { body } = await supertest
.post(DETECTION_ENGINE_RULES_URL)
@@ -126,7 +126,7 @@ export default ({ getService }: FtrProviderContext) => {
.send(simpleRule)
.expect(200);
- await waitForRuleSuccessOrStatus(supertest, body.id, 'failed');
+ await waitForRuleSuccessOrStatus(supertest, body.id, 'warning');
const { body: statusBody } = await supertest
.post(DETECTION_ENGINE_RULES_STATUS_URL)
@@ -134,8 +134,8 @@ export default ({ getService }: FtrProviderContext) => {
.send({ ids: [body.id] })
.expect(200);
- expect(statusBody[body.id].current_status.status).to.eql('failed');
- expect(statusBody[body.id].current_status.last_failure_message).to.eql(
+ expect(statusBody[body.id].current_status.status).to.eql('warning');
+ expect(statusBody[body.id].current_status.last_success_message).to.eql(
'The following index patterns did not match any indices: ["does-not-exist-*"]'
);
});
@@ -287,10 +287,7 @@ export default ({ getService }: FtrProviderContext) => {
await deleteAllAlerts(supertest);
await esArchiver.unload('security_solution/timestamp_override');
});
- it('should create a single rule which has a timestamp override and generates two signals with a failing status', async () => {
- // should be a failing status because one of the indices in the index pattern is missing
- // the timestamp override field.
-
+ it('should create a single rule which has a timestamp override and generates two signals with a "warning" status', async () => {
// defaults to event.ingested timestamp override.
// event.ingested is one of the timestamp fields set on the es archive data
// inside of x-pack/test/functional/es_archives/security_solution/timestamp_override/data.json.gz
@@ -302,7 +299,7 @@ export default ({ getService }: FtrProviderContext) => {
.expect(200);
const bodyId = body.id;
- await waitForRuleSuccessOrStatus(supertest, bodyId, 'partial failure');
+ await waitForRuleSuccessOrStatus(supertest, bodyId, 'warning');
await waitForSignalsToBePresent(supertest, 2, [bodyId]);
const { body: statusBody } = await supertest
@@ -311,9 +308,7 @@ export default ({ getService }: FtrProviderContext) => {
.send({ ids: [bodyId] })
.expect(200);
- // set to "failed" for now. Will update this with a partial failure
- // once I figure out the logic
- expect(statusBody[bodyId].current_status.status).to.eql('partial failure');
+ expect(statusBody[bodyId].current_status.status).to.eql('warning');
});
});
});
diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts
index 158247ee244ddc..4875e837556fcc 100644
--- a/x-pack/test/detection_engine_api_integration/utils.ts
+++ b/x-pack/test/detection_engine_api_integration/utils.ts
@@ -972,7 +972,7 @@ export const getRule = async (
export const waitForRuleSuccessOrStatus = async (
supertest: SuperTest,
id: string,
- status: 'succeeded' | 'failed' | 'partial failure' = 'succeeded'
+ status: 'succeeded' | 'failed' | 'partial failure' | 'warning' = 'succeeded'
): Promise => {
await waitFor(async () => {
const { body } = await supertest
diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts
index b8bdc7de16e1e1..49728603c246c7 100644
--- a/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts
+++ b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts
@@ -14,8 +14,7 @@ export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const ml = getService('ml');
- // Failing: See https://github.com/elastic/kibana/issues/90526
- describe.skip('total feature importance panel and decision path popover', function () {
+ describe('total feature importance panel and decision path popover', function () {
const testDataList: Array<{
suiteTitle: string;
archive: string;
diff --git a/x-pack/test/functional/apps/visualize/precalculated_histogram.ts b/x-pack/test/functional/apps/visualize/precalculated_histogram.ts
index a0c8fa3b21ffd2..151c9e981250f9 100644
--- a/x-pack/test/functional/apps/visualize/precalculated_histogram.ts
+++ b/x-pack/test/functional/apps/visualize/precalculated_histogram.ts
@@ -64,7 +64,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('with average aggregation', async () => {
const data = await renderTableForAggregation('Average');
- expect(data).to.eql([['2.8510720308359434']]);
+ expect(data).to.eql([['2.8653795982259327']]);
});
it('with median aggregation', async () => {
@@ -79,7 +79,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('with sum aggregation', async () => {
const data = await renderTableForAggregation('Sum');
- expect(data).to.eql([['11834.800000000001']]);
+ expect(data).to.eql([['10983']]);
});
});
});
diff --git a/x-pack/test/functional/es_archives/pre_calculated_histogram/data.json b/x-pack/test/functional/es_archives/pre_calculated_histogram/data.json
index cab1dbdf844839..121a4036aaacdd 100644
--- a/x-pack/test/functional/es_archives/pre_calculated_histogram/data.json
+++ b/x-pack/test/functional/es_archives/pre_calculated_histogram/data.json
@@ -5,8 +5,7 @@
"index": ".kibana",
"source": {
"index-pattern": {
- "title": "histogram-test",
- "fields": "[{\"name\":\"_id\",\"type\":\"string\",\"esTypes\":[\"_id\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"esTypes\":[\"_index\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"esTypes\":[\"_source\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"esTypes\":[\"_type\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"histogram-content\",\"type\":\"histogram\",\"esTypes\":[\"histogram\"],\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"histogram-title\",\"type\":\"string\",\"esTypes\":[\"keyword\"],\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]"
+ "title": "histogram-test"
},
"type": "index-pattern"
}
@@ -195,3 +194,22 @@
}
}
}
+
+{
+ "type": "doc",
+ "value": {
+ "id": "5e694159d909d9d99b5e12d1",
+ "index": "histogram-test",
+ "source": {
+ "histogram-title": "single value",
+ "histogram-content": {
+ "values": [
+ 1
+ ],
+ "counts": [
+ 1
+ ]
+ }
+ }
+ }
+}
diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts
index 057b43913b939f..7274105413145b 100644
--- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts
@@ -9,8 +9,7 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
- // FLAKY: https://github.com/elastic/kibana/issues/88177
- describe.skip('uptime alerts', () => {
+ describe('uptime alerts', () => {
const pageObjects = getPageObjects(['common', 'uptime']);
const supertest = getService('supertest');
const retry = getService('retry');
@@ -90,7 +89,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
// the previous line resolves, the API may not be done creating the alert yet, so we
// put the fetch code in a retry block with a timeout.
let alert: any;
- await retry.tryForTime(15000, async () => {
+ await retry.tryForTime(60 * 1000, async () => {
const apiResponse = await supertest.get('/api/alerts/_find?search=uptime-test');
const alertsFromThisTest = apiResponse.body.data.filter(
({ name }: { name: string }) => name === 'uptime-test'
diff --git a/x-pack/test/security_api_integration/audit.config.ts b/x-pack/test/security_api_integration/audit.config.ts
index adf36bdc99b5a9..60b1c0bf1fa808 100644
--- a/x-pack/test/security_api_integration/audit.config.ts
+++ b/x-pack/test/security_api_integration/audit.config.ts
@@ -29,9 +29,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
...xPackAPITestsConfig.get('kbnTestServer.serverArgs'),
`--plugin-path=${auditLogPlugin}`,
'--xpack.security.audit.enabled=true',
- '--xpack.security.audit.appender.kind=file',
- `--xpack.security.audit.appender.path=${auditLogPath}`,
- '--xpack.security.audit.appender.layout.kind=json',
+ '--xpack.security.audit.appender.type=file',
+ `--xpack.security.audit.appender.fileName=${auditLogPath}`,
+ '--xpack.security.audit.appender.layout.type=json',
],
},
};
diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json
index 1c2e0aeecd2477..ff3fec1c5aaee7 100644
--- a/x-pack/test/tsconfig.json
+++ b/x-pack/test/tsconfig.json
@@ -69,6 +69,7 @@
{ "path": "../plugins/license_management/tsconfig.json" },
{ "path": "../plugins/licensing/tsconfig.json" },
{ "path": "../plugins/ml/tsconfig.json" },
+ { "path": "../plugins/monitoring/tsconfig.json" },
{ "path": "../plugins/observability/tsconfig.json" },
{ "path": "../plugins/osquery/tsconfig.json" },
{ "path": "../plugins/painless_lab/tsconfig.json" },
@@ -88,7 +89,7 @@
{ "path": "../plugins/rollup/tsconfig.json" },
{ "path": "../plugins/remote_clusters/tsconfig.json" },
{ "path": "../plugins/cross_cluster_replication/tsconfig.json" },
- { "path": "../plugins/index_lifecycle_management/tsconfig.json"},
+ { "path": "../plugins/index_lifecycle_management/tsconfig.json" },
{ "path": "../plugins/uptime/tsconfig.json" }
]
}
diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json
index 813811d4a9ce45..b3e47136977e21 100644
--- a/x-pack/tsconfig.json
+++ b/x-pack/tsconfig.json
@@ -7,7 +7,6 @@
"plugins/case/**/*",
"plugins/lists/**/*",
"plugins/logstash/**/*",
- "plugins/monitoring/**/*",
"plugins/security_solution/**/*",
"plugins/xpack_legacy/**/*",
"plugins/drilldowns/url_drilldown/**/*"
@@ -90,6 +89,7 @@
{ "path": "./plugins/maps_legacy_licensing/tsconfig.json" },
{ "path": "./plugins/maps/tsconfig.json" },
{ "path": "./plugins/ml/tsconfig.json" },
+ { "path": "./plugins/monitoring/tsconfig.json" },
{ "path": "./plugins/observability/tsconfig.json" },
{ "path": "./plugins/osquery/tsconfig.json" },
{ "path": "./plugins/painless_lab/tsconfig.json" },
@@ -111,8 +111,8 @@
{ "path": "./plugins/watcher/tsconfig.json" },
{ "path": "./plugins/rollup/tsconfig.json" },
{ "path": "./plugins/remote_clusters/tsconfig.json" },
- { "path": "./plugins/cross_cluster_replication/tsconfig.json"},
- { "path": "./plugins/index_lifecycle_management/tsconfig.json"},
+ { "path": "./plugins/cross_cluster_replication/tsconfig.json" },
+ { "path": "./plugins/index_lifecycle_management/tsconfig.json" },
{ "path": "./plugins/uptime/tsconfig.json" }
]
}
diff --git a/yarn.lock b/yarn.lock
index 8dc2cf35287d75..31fbf7d9037a20 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2146,10 +2146,10 @@
dependencies:
object-hash "^1.3.0"
-"@elastic/charts@24.5.1":
- version "24.5.1"
- resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-24.5.1.tgz#4757721b0323b15412c92d696dd76fdef9b963f8"
- integrity sha512-eHJna3xyHREaSfTRb+3/34EmyoINopH6yP9KReakXRb0jW8DD4n9IkbPFwpVN3uXQ6ND2x1ObA0ZzLPSLCPozg==
+"@elastic/charts@24.6.0":
+ version "24.6.0"
+ resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-24.6.0.tgz#2123c72e69e1e4557be41ae55c085a5a9f75d3b6"
+ integrity sha512-fL0301EcHxJEYRzdlD4JIA3VXY4qwRPSkRrk8hvJNryTlQWEdyXZF3HNczk0IrgST5cfCOGAWG8IVtO59HxUJw==
dependencies:
"@popperjs/core" "^2.4.0"
chroma-js "^2.1.0"
@@ -2203,10 +2203,10 @@
resolved "https://registry.yarnpkg.com/@elastic/eslint-plugin-eui/-/eslint-plugin-eui-0.0.2.tgz#56b9ef03984a05cc213772ae3713ea8ef47b0314"
integrity sha512-IoxURM5zraoQ7C8f+mJb9HYSENiZGgRVcG4tLQxE61yHNNRDXtGDWTZh8N1KIHcsqN1CEPETjuzBXkJYF/fDiQ==
-"@elastic/eui@31.4.0":
- version "31.4.0"
- resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-31.4.0.tgz#d2c8cc91fc538f7b1c5e5229663e186fa0c9207c"
- integrity sha512-ADdUeNxj2uiN13U7AkF0ishLAN0xcqFWHC+xjEmx8Wedyaj5DFrmmJEuH9aXv+XSQG5l8ppMgZQb3pMDjR2mKw==
+"@elastic/eui@31.7.0":
+ version "31.7.0"
+ resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-31.7.0.tgz#091cad212da3d7bad8e1717398d9bf404257f835"
+ integrity sha512-Be4K/dizAsmOrRMAKZy6oW4VaEvQNB1LvbKxNWIa4yFeX7Wn6gw5ihttgbQ4vqWWIkhLPoadbXMebKF9+7VuhA==
dependencies:
"@types/chroma-js" "^2.0.0"
"@types/lodash" "^4.14.160"