{
export type ServiceFromProvider = P extends CanvasServiceProvider ? T : never;
export const services = {
+ expressions: new CanvasServiceProvider(expressionsServiceFactory),
notify: new CanvasServiceProvider(notifyServiceFactory),
platform: new CanvasServiceProvider(platformServiceFactory),
navLink: new CanvasServiceProvider(navLinkServiceFactory),
};
export interface CanvasServices {
+ expressions: ServiceFromProvider;
notify: ServiceFromProvider;
platform: ServiceFromProvider;
navLink: ServiceFromProvider;
}
-export const startServices = (
+export const startServices = async (
coreSetup: CoreSetup,
coreStart: CoreStart,
canvasSetupPlugins: CanvasSetupDeps,
canvasStartPlugins: CanvasStartDeps,
appUpdater: BehaviorSubject
) => {
- Object.entries(services).forEach(([key, provider]) =>
+ const startPromises = Object.values(services).map((provider) =>
provider.start(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins, appUpdater)
);
+
+ await Promise.all(startPromises);
};
export const stopServices = () => {
@@ -90,4 +95,5 @@ export const {
notify: notifyService,
platform: platformService,
navLink: navLinkService,
+ expressions: expressionsService,
} = services;
diff --git a/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.test.ts b/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.test.ts
index 760c4ef01b31c..1d1396fd520d1 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.test.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.test.ts
@@ -9,7 +9,7 @@ import { mockAuthenticatedUser } from '../../../security/common/model/authentica
it('properly logs audit events', () => {
const mockInternalAuditLogger = { log: jest.fn() };
- const audit = new EncryptedSavedObjectsAuditLogger(() => mockInternalAuditLogger);
+ const audit = new EncryptedSavedObjectsAuditLogger(mockInternalAuditLogger);
audit.encryptAttributesSuccess(['one', 'two'], {
type: 'known-type',
diff --git a/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.ts b/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.ts
index 1a10dd343d43d..de14a79dd0ddb 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/audit/audit_logger.ts
@@ -4,22 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { AuditLogger, AuthenticatedUser } from '../../../security/server';
import { SavedObjectDescriptor, descriptorToArray } from '../crypto';
-import { LegacyAPI } from '../plugin';
-import { AuthenticatedUser } from '../../../security/common/model';
/**
* Represents all audit events the plugin can log.
*/
export class EncryptedSavedObjectsAuditLogger {
- constructor(private readonly getAuditLogger: () => LegacyAPI['auditLogger']) {}
+ constructor(private readonly logger: AuditLogger = { log() {} }) {}
public encryptAttributeFailure(
attributeName: string,
descriptor: SavedObjectDescriptor,
user?: AuthenticatedUser
) {
- this.getAuditLogger().log(
+ this.logger.log(
'encrypt_failure',
`Failed to encrypt attribute "${attributeName}" for saved object "[${descriptorToArray(
descriptor
@@ -33,7 +32,7 @@ export class EncryptedSavedObjectsAuditLogger {
descriptor: SavedObjectDescriptor,
user?: AuthenticatedUser
) {
- this.getAuditLogger().log(
+ this.logger.log(
'decrypt_failure',
`Failed to decrypt attribute "${attributeName}" for saved object "[${descriptorToArray(
descriptor
@@ -47,7 +46,7 @@ export class EncryptedSavedObjectsAuditLogger {
descriptor: SavedObjectDescriptor,
user?: AuthenticatedUser
) {
- this.getAuditLogger().log(
+ this.logger.log(
'encrypt_success',
`Successfully encrypted attributes "[${attributesNames}]" for saved object "[${descriptorToArray(
descriptor
@@ -61,7 +60,7 @@ export class EncryptedSavedObjectsAuditLogger {
descriptor: SavedObjectDescriptor,
user?: AuthenticatedUser
) {
- this.getAuditLogger().log(
+ this.logger.log(
'decrypt_success',
`Successfully decrypted attributes "[${attributesNames}]" for saved object "[${descriptorToArray(
descriptor
diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts
index e8568e9964c2f..4afd74488f9fe 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts
@@ -16,9 +16,6 @@ describe('EncryptedSavedObjects Plugin', () => {
await expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() }))
.resolves.toMatchInlineSnapshot(`
Object {
- "__legacyCompat": Object {
- "registerLegacyAPI": [Function],
- },
"registerType": [Function],
"usingEphemeralEncryptionKey": true,
}
diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts
index 83b412de5db7e..cdbdd18b9d696 100644
--- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts
+++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts
@@ -22,7 +22,6 @@ export interface PluginsSetup {
export interface EncryptedSavedObjectsPluginSetup {
registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => void;
- __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => void };
usingEphemeralEncryptionKey: boolean;
}
@@ -31,16 +30,6 @@ export interface EncryptedSavedObjectsPluginStart {
getClient: ClientInstanciator;
}
-/**
- * Describes a set of APIs that is available in the legacy platform only and required by this plugin
- * to function properly.
- */
-export interface LegacyAPI {
- auditLogger: {
- log: (eventType: string, message: string, data?: Record) => void;
- };
-}
-
/**
* Represents EncryptedSavedObjects Plugin instance that will be managed by the Kibana plugin system.
*/
@@ -48,14 +37,6 @@ export class Plugin {
private readonly logger: Logger;
private savedObjectsSetup!: ClientInstanciator;
- private legacyAPI?: LegacyAPI;
- private readonly getLegacyAPI = () => {
- if (!this.legacyAPI) {
- throw new Error('Legacy API is not registered!');
- }
- return this.legacyAPI;
- };
-
constructor(private readonly initializerContext: PluginInitializerContext) {
this.logger = this.initializerContext.logger.get();
}
@@ -72,7 +53,9 @@ export class Plugin {
new EncryptedSavedObjectsService(
config.encryptionKey,
this.logger,
- new EncryptedSavedObjectsAuditLogger(() => this.getLegacyAPI().auditLogger)
+ new EncryptedSavedObjectsAuditLogger(
+ deps.security?.audit.getLogger('encryptedSavedObjects')
+ )
)
);
@@ -86,7 +69,6 @@ export class Plugin {
return {
registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) =>
service.registerType(typeRegistration),
- __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => (this.legacyAPI = legacyAPI) },
usingEphemeralEncryptionKey,
};
}
diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx
index cc87167b10a96..d81d11e01d4a5 100644
--- a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx
+++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx
@@ -17,11 +17,7 @@ import {
import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertsContextValue } from '../../../../../../triggers_actions_ui/public/application/context/alerts_context';
-import {
- LogDocumentCountAlertParams,
- Comparator,
- TimeUnit,
-} from '../../../../../common/alerting/logs/types';
+import { LogDocumentCountAlertParams, Comparator } from '../../../../../common/alerting/logs/types';
import { DocumentCount } from './document_count';
import { Criteria } from './criteria';
import { useSourceId } from '../../../../containers/source_id';
@@ -123,8 +119,6 @@ export const SourceStatusWrapper: React.FC = (props) => {
export const Editor: React.FC = (props) => {
const { setAlertParams, alertParams, errors } = props;
- const [timeSize, setTimeSize] = useState(1);
- const [timeUnit, setTimeUnit] = useState('m');
const [hasSetDefaults, setHasSetDefaults] = useState(false);
const { sourceStatus } = useLogSourceContext();
@@ -165,15 +159,13 @@ export const Editor: React.FC = (props) => {
const updateTimeSize = useCallback(
(ts: number | undefined) => {
- setTimeSize(ts || undefined);
setAlertParams('timeSize', ts);
},
- [setTimeSize, setAlertParams]
+ [setAlertParams]
);
const updateTimeUnit = useCallback(
(tu: string) => {
- setTimeUnit(tu as TimeUnit);
setAlertParams('timeUnit', tu);
},
[setAlertParams]
@@ -217,8 +209,8 @@ export const Editor: React.FC = (props) => {
/>
;
@@ -97,8 +98,38 @@ export const itemIdOrUndefined = t.union([item_id, t.undefined]);
export type ItemIdOrUndefined = t.TypeOf;
export const per_page = t.number; // TODO: Change this out for PositiveNumber from siem
+export type PerPage = t.TypeOf;
+
+export const perPageOrUndefined = t.union([per_page, t.undefined]);
+export type PerPageOrUndefined = t.TypeOf;
+
export const total = t.number; // TODO: Change this out for PositiveNumber from siem
+export const totalUndefined = t.union([total, t.undefined]);
+export type TotalOrUndefined = t.TypeOf;
+
export const page = t.number; // TODO: Change this out for PositiveNumber from siem
+export type Page = t.TypeOf;
+
+export const pageOrUndefined = t.union([page, t.undefined]);
+export type PageOrUndefined = t.TypeOf;
+
export const sort_field = t.string;
+export const sortFieldOrUndefined = t.union([sort_field, t.undefined]);
+export type SortFieldOrUndefined = t.TypeOf;
+
export const sort_order = t.keyof({ asc: null, desc: null });
+export const sortOrderOrUndefined = t.union([sort_order, t.undefined]);
+export type SortOrderOrUndefined = t.TypeOf;
+
export const filter = t.string;
+export type Filter = t.TypeOf;
+export const filterOrUndefined = t.union([filter, t.undefined]);
+export type FilterOrUndefined = t.TypeOf;
+
+export const cursor = t.string;
+export type Cursor = t.TypeOf;
+export const cursorOrUndefined = t.union([cursor, t.undefined]);
+export type CursorOrUndefined = t.TypeOf;
+
+export const namespace_type = DefaultNamespace;
+export type NamespaceType = t.TypeOf;
diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts
index f899fd69110fa..c10d441d93aa5 100644
--- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts
+++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts
@@ -10,6 +10,7 @@ import * as t from 'io-ts';
import {
ItemId,
+ NamespaceType,
Tags,
_Tags,
_tags,
@@ -19,6 +20,7 @@ import {
list_id,
meta,
name,
+ namespace_type,
tags,
} from '../common/schemas';
import { Identity, RequiredKeepUndefined } from '../../types';
@@ -41,6 +43,7 @@ export const createExceptionListItemSchema = t.intersection([
entries: DefaultEntryArray, // defaults to empty array if not set during decode
item_id: DefaultUuid, // defaults to GUID (uuid v4) if not set during decode
meta, // defaults to undefined if not set during decode
+ namespace_type, // defaults to 'single' if not set during decode
tags, // defaults to empty array if not set during decode
})
),
@@ -53,13 +56,16 @@ export type CreateExceptionListItemSchema = RequiredKeepUndefined<
t.TypeOf
>;
-// This type is used after a decode since the arrays turn into defaults of empty arrays
-// and if a item_id is not specified it turns into a default GUID
+// This type is used after a decode since some things are defaults after a decode.
export type CreateExceptionListItemSchemaDecoded = Identity<
- Omit & {
+ Omit<
+ CreateExceptionListItemSchema,
+ '_tags' | 'tags' | 'item_id' | 'entries' | 'namespace_type'
+ > & {
_tags: _Tags;
tags: Tags;
item_id: ItemId;
entries: EntriesArray;
+ namespace_type: NamespaceType;
}
>;
diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts
index d38d3cc038525..f0b98cb96f743 100644
--- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts
+++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { DESCRIPTION, LIST_ID, META, NAME, TYPE } from '../../constants.mock';
+import { DESCRIPTION, LIST_ID, META, NAME, NAMESPACE_TYPE, TYPE } from '../../constants.mock';
import { CreateExceptionListSchema } from './create_exception_list_schema';
@@ -14,6 +14,7 @@ export const getCreateExceptionListSchemaMock = (): CreateExceptionListSchema =>
list_id: LIST_ID,
meta: META,
name: NAME,
+ namespace_type: NAMESPACE_TYPE,
tags: [],
type: TYPE,
});
diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts
index 5ba3bf4e8f43b..3da8bfca126ae 100644
--- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts
+++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.ts
@@ -10,6 +10,7 @@ import * as t from 'io-ts';
import {
ListId,
+ NamespaceType,
Tags,
_Tags,
_tags,
@@ -17,6 +18,7 @@ import {
exceptionListType,
meta,
name,
+ namespace_type,
tags,
} from '../common/schemas';
import { Identity, RequiredKeepUndefined } from '../../types';
@@ -35,6 +37,7 @@ export const createExceptionListSchema = t.intersection([
_tags, // defaults to empty array if not set during decode
list_id: DefaultUuid, // defaults to a GUID (UUID v4) string if not set during decode
meta, // defaults to undefined if not set during decode
+ namespace_type, // defaults to 'single' if not set during decode
tags, // defaults to empty array if not set during decode
})
),
@@ -45,11 +48,12 @@ export type CreateExceptionListSchema = RequiredKeepUndefined<
t.TypeOf
>;
-// This type is used after a decode since the arrays turn into defaults of empty arrays.
+// This type is used after a decode since some things are defaults after a decode.
export type CreateExceptionListSchemaDecoded = Identity<
- CreateExceptionListSchema & {
+ Omit & {
_tags: _Tags;
tags: Tags;
list_id: ListId;
+ namespace_type: NamespaceType;
}
>;
diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts
index 607e05ef8286f..4c5b70d9a4073 100644
--- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts
+++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_item_schema.ts
@@ -8,13 +8,22 @@
import * as t from 'io-ts';
-import { id, item_id } from '../common/schemas';
+import { NamespaceType, id, item_id, namespace_type } from '../common/schemas';
export const deleteExceptionListItemSchema = t.exact(
t.partial({
id,
item_id,
+ namespace_type, // defaults to 'single' if not set during decode
})
);
export type DeleteExceptionListItemSchema = t.TypeOf;
+
+// This type is used after a decode since some things are defaults after a decode.
+export type DeleteExceptionListItemSchemaDecoded = Omit<
+ DeleteExceptionListItemSchema,
+ 'namespace_type'
+> & {
+ namespace_type: NamespaceType;
+};
diff --git a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts
index 7a6086514f943..2577d867031f0 100644
--- a/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts
+++ b/x-pack/plugins/lists/common/schemas/request/delete_exception_list_schema.ts
@@ -8,13 +8,19 @@
import * as t from 'io-ts';
-import { id, list_id } from '../common/schemas';
+import { NamespaceType, id, list_id, namespace_type } from '../common/schemas';
export const deleteExceptionListSchema = t.exact(
t.partial({
id,
list_id,
+ namespace_type, // defaults to 'single' if not set during decode
})
);
export type DeleteExceptionListSchema = t.TypeOf;
+
+// This type is used after a decode since some things are defaults after a decode.
+export type DeleteExceptionListSchemaDecoded = Omit & {
+ namespace_type: NamespaceType;
+};
diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts
index 3fc51dd20b0b3..31eb4925eb6d6 100644
--- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts
+++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_item_schema.ts
@@ -8,8 +8,16 @@
import * as t from 'io-ts';
-import { filter, list_id, page, per_page, sort_field, sort_order } from '../common/schemas';
+import {
+ NamespaceType,
+ filter,
+ list_id,
+ namespace_type,
+ sort_field,
+ sort_order,
+} from '../common/schemas';
import { RequiredKeepUndefined } from '../../types';
+import { StringToPositiveNumber } from '../types/string_to_positive_number';
export const findExceptionListItemSchema = t.intersection([
t.exact(
@@ -20,8 +28,9 @@ export const findExceptionListItemSchema = t.intersection([
t.exact(
t.partial({
filter, // defaults to undefined if not set during decode
- page, // defaults to undefined if not set during decode
- per_page, // defaults to undefined if not set during decode
+ namespace_type, // defaults to 'single' if not set during decode
+ page: StringToPositiveNumber, // defaults to undefined if not set during decode
+ per_page: StringToPositiveNumber, // defaults to undefined if not set during decode
sort_field, // defaults to undefined if not set during decode
sort_order, // defaults to undefined if not set during decode
})
@@ -30,6 +39,19 @@ export const findExceptionListItemSchema = t.intersection([
export type FindExceptionListItemSchemaPartial = t.TypeOf;
+// This type is used after a decode since some things are defaults after a decode.
+export type FindExceptionListItemSchemaPartialDecoded = Omit<
+ FindExceptionListItemSchemaPartial,
+ 'namespace_type'
+> & {
+ namespace_type: NamespaceType;
+};
+
+// This type is used after a decode since some things are defaults after a decode.
+export type FindExceptionListItemSchemaDecoded = RequiredKeepUndefined<
+ FindExceptionListItemSchemaPartialDecoded
+>;
+
export type FindExceptionListItemSchema = RequiredKeepUndefined<
t.TypeOf
>;
diff --git a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts
index f795be9493fbf..fa00c5b0dafb1 100644
--- a/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts
+++ b/x-pack/plugins/lists/common/schemas/request/find_exception_list_schema.ts
@@ -8,14 +8,16 @@
import * as t from 'io-ts';
-import { filter, page, per_page, sort_field, sort_order } from '../common/schemas';
+import { NamespaceType, filter, namespace_type, sort_field, sort_order } from '../common/schemas';
import { RequiredKeepUndefined } from '../../types';
+import { StringToPositiveNumber } from '../types/string_to_positive_number';
export const findExceptionListSchema = t.exact(
t.partial({
filter, // defaults to undefined if not set during decode
- page, // defaults to undefined if not set during decode
- per_page, // defaults to undefined if not set during decode
+ namespace_type, // defaults to 'single' if not set during decode
+ page: StringToPositiveNumber, // defaults to undefined if not set during decode
+ per_page: StringToPositiveNumber, // defaults to undefined if not set during decode
sort_field, // defaults to undefined if not set during decode
sort_order, // defaults to undefined if not set during decode
})
@@ -23,6 +25,19 @@ export const findExceptionListSchema = t.exact(
export type FindExceptionListSchemaPartial = t.TypeOf;
+// This type is used after a decode since some things are defaults after a decode.
+export type FindExceptionListSchemaPartialDecoded = Omit<
+ FindExceptionListSchemaPartial,
+ 'namespace_type'
+> & {
+ namespace_type: NamespaceType;
+};
+
+// This type is used after a decode since some things are defaults after a decode.
+export type FindExceptionListSchemaDecoded = RequiredKeepUndefined<
+ FindExceptionListSchemaPartialDecoded
+>;
+
export type FindExceptionListSchema = RequiredKeepUndefined<
t.TypeOf
>;
diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts
new file mode 100644
index 0000000000000..c9ece4224c4ce
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/request/find_list_item_schema.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/* eslint-disable @typescript-eslint/camelcase */
+
+import * as t from 'io-ts';
+
+import { cursor, filter, list_id, sort_field, sort_order } from '../common/schemas';
+import { Identity, RequiredKeepUndefined } from '../../types';
+import { StringToPositiveNumber } from '../types/string_to_positive_number';
+
+export const findListItemSchema = t.intersection([
+ t.exact(t.type({ list_id })),
+ t.exact(
+ t.partial({
+ cursor, // defaults to undefined if not set during decode
+ filter, // defaults to undefined if not set during decode
+ page: StringToPositiveNumber, // defaults to undefined if not set during decode
+ per_page: StringToPositiveNumber, // defaults to undefined if not set during decode
+ sort_field, // defaults to undefined if not set during decode
+ sort_order, // defaults to undefined if not set during decode
+ })
+ ),
+]);
+
+export type FindListItemSchemaPartial = Identity>;
+
+export type FindListItemSchema = RequiredKeepUndefined>;
diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts
new file mode 100644
index 0000000000000..c29ab4f5360dd
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/* eslint-disable @typescript-eslint/camelcase */
+
+import * as t from 'io-ts';
+
+import { cursor, filter, sort_field, sort_order } from '../common/schemas';
+import { RequiredKeepUndefined } from '../../types';
+import { StringToPositiveNumber } from '../types/string_to_positive_number';
+
+export const findListSchema = t.exact(
+ t.partial({
+ cursor, // defaults to undefined if not set during decode
+ filter, // defaults to undefined if not set during decode
+ page: StringToPositiveNumber, // defaults to undefined if not set during decode
+ per_page: StringToPositiveNumber, // defaults to undefined if not set during decode
+ sort_field, // defaults to undefined if not set during decode
+ sort_order, // defaults to undefined if not set during decode
+ })
+);
+
+export type FindListSchemaPartial = t.TypeOf;
+
+export type FindListSchema = RequiredKeepUndefined>;
diff --git a/x-pack/plugins/lists/common/schemas/request/index.ts b/x-pack/plugins/lists/common/schemas/request/index.ts
index 0dbd9297b773e..7ab3d943f14da 100644
--- a/x-pack/plugins/lists/common/schemas/request/index.ts
+++ b/x-pack/plugins/lists/common/schemas/request/index.ts
@@ -15,6 +15,8 @@ export * from './delete_list_schema';
export * from './export_list_item_query_schema';
export * from './find_exception_list_item_schema';
export * from './find_exception_list_schema';
+export * from './find_list_item_schema';
+export * from './find_list_schema';
export * from './import_list_item_schema';
export * from './patch_list_item_schema';
export * from './patch_list_schema';
diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts
index 095fcd2f63b48..fded35dfd1cc9 100644
--- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts
+++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_item_schema.ts
@@ -8,13 +8,28 @@
import * as t from 'io-ts';
-import { id, item_id } from '../common/schemas';
+import { NamespaceType, id, item_id, namespace_type } from '../common/schemas';
import { RequiredKeepUndefined } from '../../types';
export const readExceptionListItemSchema = t.partial({
id,
item_id,
+ namespace_type, // defaults to 'single' if not set during decode
});
export type ReadExceptionListItemSchemaPartial = t.TypeOf;
+
+// This type is used after a decode since some things are defaults after a decode.
+export type ReadExceptionListItemSchemaPartialDecoded = Omit<
+ ReadExceptionListItemSchemaPartial,
+ 'namespace_type'
+> & {
+ namespace_type: NamespaceType;
+};
+
+// This type is used after a decode since some things are defaults after a decode.
+export type ReadExceptionListItemSchemaDecoded = RequiredKeepUndefined<
+ ReadExceptionListItemSchemaPartialDecoded
+>;
+
export type ReadExceptionListItemSchema = RequiredKeepUndefined;
diff --git a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts
index 5593e640f71ac..6b623ea8c0b9b 100644
--- a/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts
+++ b/x-pack/plugins/lists/common/schemas/request/read_exception_list_schema.ts
@@ -8,13 +8,28 @@
import * as t from 'io-ts';
-import { id, list_id } from '../common/schemas';
+import { NamespaceType, id, list_id, namespace_type } from '../common/schemas';
import { RequiredKeepUndefined } from '../../types';
export const readExceptionListSchema = t.partial({
id,
list_id,
+ namespace_type, // defaults to 'single' if not set during decode
});
export type ReadExceptionListSchemaPartial = t.TypeOf;
+
+// This type is used after a decode since some things are defaults after a decode.
+export type ReadExceptionListSchemaPartialDecoded = Omit<
+ ReadExceptionListSchemaPartial,
+ 'namespace_type'
+> & {
+ namespace_type: NamespaceType;
+};
+
+// This type is used after a decode since some things are defaults after a decode.
+export type ReadExceptionListSchemaDecoded = RequiredKeepUndefined<
+ ReadExceptionListSchemaPartialDecoded
+>;
+
export type ReadExceptionListSchema = RequiredKeepUndefined;
diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts
index 162406a6d6589..3d66dad959c25 100644
--- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts
+++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts
@@ -9,6 +9,7 @@
import * as t from 'io-ts';
import {
+ NamespaceType,
Tags,
_Tags,
_tags,
@@ -18,6 +19,7 @@ import {
id,
meta,
name,
+ namespace_type,
tags,
} from '../common/schemas';
import { Identity, RequiredKeepUndefined } from '../../types';
@@ -40,6 +42,7 @@ export const updateExceptionListItemSchema = t.intersection([
id, // defaults to undefined if not set during decode
item_id: t.union([t.string, t.undefined]),
meta, // defaults to undefined if not set during decode
+ namespace_type, // defaults to 'single' if not set during decode
tags, // defaults to empty array if not set during decode
})
),
@@ -52,12 +55,12 @@ export type UpdateExceptionListItemSchema = RequiredKeepUndefined<
t.TypeOf
>;
-// This type is used after a decode since the arrays turn into defaults of empty arrays
-// and if a item_id is not specified it turns into a default GUID
+// This type is used after a decode since some things are defaults after a decode.
export type UpdateExceptionListItemSchemaDecoded = Identity<
- Omit & {
+ Omit & {
_tags: _Tags;
tags: Tags;
entries: EntriesArray;
+ namespace_type: NamespaceType;
}
>;
diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts
index e8a0dcd4994a2..76160c3419449 100644
--- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts
+++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.ts
@@ -9,6 +9,7 @@
import * as t from 'io-ts';
import {
+ NamespaceType,
Tags,
_Tags,
_tags,
@@ -16,6 +17,7 @@ import {
exceptionListType,
meta,
name,
+ namespace_type,
tags,
} from '../common/schemas';
import { Identity, RequiredKeepUndefined } from '../../types';
@@ -34,6 +36,7 @@ export const updateExceptionListSchema = t.intersection([
id: t.union([t.string, t.undefined]), // defaults to undefined if not set during decode
list_id: t.union([t.string, t.undefined]), // defaults to undefined if not set during decode
meta, // defaults to undefined if not set during decode
+ namespace_type, // defaults to 'single' if not set during decode
tags, // defaults to empty array if not set during decode
})
),
@@ -46,8 +49,9 @@ export type UpdateExceptionListSchema = RequiredKeepUndefined<
// This type is used after a decode since the arrays turn into defaults of empty arrays.
export type UpdateExceptionListSchemaDecoded = Identity<
- Omit & {
+ Omit & {
_tags: _Tags;
tags: Tags;
+ namespace_type: NamespaceType;
}
>;
diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts
index 15e1c92c06d13..ab405c21d9c77 100644
--- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts
+++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.ts
@@ -20,6 +20,7 @@ import {
list_id,
metaOrUndefined,
name,
+ namespace_type,
tags,
tie_breaker_id,
updated_at,
@@ -41,6 +42,7 @@ export const exceptionListItemSchema = t.exact(
list_id,
meta: metaOrUndefined,
name,
+ namespace_type,
tags,
tie_breaker_id,
type: exceptionListItemType,
diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts
index 1940d94597dec..120ed31f87d0d 100644
--- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts
+++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.ts
@@ -18,6 +18,7 @@ import {
list_id,
metaOrUndefined,
name,
+ namespace_type,
tags,
tie_breaker_id,
updated_at,
@@ -35,6 +36,7 @@ export const exceptionListSchema = t.exact(
list_id,
meta: metaOrUndefined,
name,
+ namespace_type,
tags,
tie_breaker_id,
type: exceptionListType,
diff --git a/x-pack/plugins/lists/common/schemas/response/found_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/found_list_item_schema.ts
new file mode 100644
index 0000000000000..f792774cd0c12
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/response/found_list_item_schema.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/* eslint-disable @typescript-eslint/camelcase */
+
+import * as t from 'io-ts';
+
+import { cursor, page, per_page, total } from '../common/schemas';
+
+import { listItemSchema } from './list_item_schema';
+
+export const foundListItemSchema = t.exact(
+ t.type({
+ cursor,
+ data: t.array(listItemSchema),
+ page,
+ per_page,
+ total,
+ })
+);
+
+export type FoundListItemSchema = t.TypeOf;
diff --git a/x-pack/plugins/lists/common/schemas/response/found_list_schema.ts b/x-pack/plugins/lists/common/schemas/response/found_list_schema.ts
new file mode 100644
index 0000000000000..aaf4a721d050d
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/response/found_list_schema.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/* eslint-disable @typescript-eslint/camelcase */
+
+import * as t from 'io-ts';
+
+import { cursor, page, per_page, total } from '../common/schemas';
+
+import { listSchema } from './list_schema';
+
+export const foundListSchema = t.exact(
+ t.type({
+ cursor,
+ data: t.array(listSchema),
+ page,
+ per_page,
+ total,
+ })
+);
+
+export type FoundListSchema = t.TypeOf;
diff --git a/x-pack/plugins/lists/common/schemas/response/index.ts b/x-pack/plugins/lists/common/schemas/response/index.ts
index 213685d1183bd..fb6f17a896ddb 100644
--- a/x-pack/plugins/lists/common/schemas/response/index.ts
+++ b/x-pack/plugins/lists/common/schemas/response/index.ts
@@ -4,11 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export * from './list_item_schema';
-export * from './list_schema';
export * from './acknowledge_schema';
-export * from './list_item_index_exist_schema';
export * from './exception_list_schema';
+export * from './exception_list_item_schema';
export * from './found_exception_list_item_schema';
export * from './found_exception_list_schema';
-export * from './exception_list_item_schema';
+export * from './found_list_item_schema';
+export * from './found_list_schema';
+export * from './list_item_schema';
+export * from './list_schema';
+export * from './list_item_index_exist_schema';
diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.ts
index cad449766ceb4..4e664685db9c7 100644
--- a/x-pack/plugins/lists/common/schemas/response/list_schema.ts
+++ b/x-pack/plugins/lists/common/schemas/response/list_schema.ts
@@ -37,3 +37,6 @@ export const listSchema = t.exact(
);
export type ListSchema = t.TypeOf;
+
+export const listArraySchema = t.array(listSchema);
+export type ListArraySchema = t.TypeOf;
diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace.ts
new file mode 100644
index 0000000000000..ebe2cd60cf6c8
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/types/default_namespace.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import * as t from 'io-ts';
+import { Either } from 'fp-ts/lib/Either';
+
+const namespaceType = t.keyof({ agnostic: null, single: null });
+
+type NamespaceType = t.TypeOf;
+
+export type DefaultNamespaceC = t.Type;
+
+/**
+ * Types the DefaultNamespace as:
+ * - If null or undefined, then a default string/enumeration of "single" will be used.
+ */
+export const DefaultNamespace: DefaultNamespaceC = new t.Type<
+ NamespaceType,
+ NamespaceType,
+ unknown
+>(
+ 'DefaultNamespace',
+ namespaceType.is,
+ (input): Either =>
+ input == null ? t.success('single') : namespaceType.decode(input),
+ t.identity
+);
diff --git a/x-pack/plugins/lists/common/schemas/types/string_to_positive_number.ts b/x-pack/plugins/lists/common/schemas/types/string_to_positive_number.ts
new file mode 100644
index 0000000000000..4b62d6c11d801
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/types/string_to_positive_number.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import * as t from 'io-ts';
+import { Either, either } from 'fp-ts/lib/Either';
+
+export type StringToPositiveNumberC = t.Type;
+
+/**
+ * Types the StrongToPositiveNumber as:
+ * - If a string this converts the string into a number
+ * - Ensures it is a number (and not NaN)
+ * - Ensures it is positive number
+ */
+export const StringToPositiveNumber: StringToPositiveNumberC = new t.Type(
+ 'StringToPositiveNumber',
+ t.number.is,
+ (input, context): Either => {
+ return either.chain(
+ t.string.validate(input, context),
+ (numberAsString): Either => {
+ const stringAsNumber = +numberAsString;
+ if (numberAsString.trim().length === 0 || isNaN(stringAsNumber) || stringAsNumber <= 0) {
+ return t.failure(input, context);
+ } else {
+ return t.success(stringAsNumber);
+ }
+ }
+ );
+ },
+ String
+);
diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts
index cc172ee1e6109..3a61140e5621d 100644
--- a/x-pack/plugins/lists/public/exceptions/api.test.ts
+++ b/x-pack/plugins/lists/public/exceptions/api.test.ts
@@ -68,7 +68,7 @@ describe('Exceptions Lists API', () => {
});
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', {
body:
- '{"_tags":["endpoint","process","malware","os:linux"],"created_at":"2020-04-23T00:19:13.289Z","created_by":"user_name","description":"This is a sample endpoint type exception","id":"1","list_id":"endpoint_list","meta":{},"name":"Sample Endpoint Exception List","tags":["user added string for a tag","malware"],"tie_breaker_id":"77fd1909-6786-428a-a671-30229a719c1f","type":"endpoint","updated_at":"2020-04-23T00:19:13.289Z","updated_by":"user_name"}',
+ '{"_tags":["endpoint","process","malware","os:linux"],"created_at":"2020-04-23T00:19:13.289Z","created_by":"user_name","description":"This is a sample endpoint type exception","id":"1","list_id":"endpoint_list","meta":{},"name":"Sample Endpoint Exception List","namespace_type":"single","tags":["user added string for a tag","malware"],"tie_breaker_id":"77fd1909-6786-428a-a671-30229a719c1f","type":"endpoint","updated_at":"2020-04-23T00:19:13.289Z","updated_by":"user_name"}',
method: 'PUT',
signal: abortCtrl.signal,
});
@@ -112,7 +112,7 @@ describe('Exceptions Lists API', () => {
});
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', {
body:
- '{"_tags":["endpoint","process","malware","os:linux"],"comment":[],"created_at":"2020-04-23T00:19:13.289Z","created_by":"user_name","description":"This is a sample endpoint type exception","entries":[{"field":"actingProcess.file.signer","match":"Elastic, N.V.","operator":"included"},{"field":"event.category","match_any":["process","malware"],"operator":"included"}],"id":"1","item_id":"endpoint_list_item","list_id":"endpoint_list","meta":{},"name":"Sample Endpoint Exception List","tags":["user added string for a tag","malware"],"tie_breaker_id":"77fd1909-6786-428a-a671-30229a719c1f","type":"simple","updated_at":"2020-04-23T00:19:13.289Z","updated_by":"user_name"}',
+ '{"_tags":["endpoint","process","malware","os:linux"],"comment":[],"created_at":"2020-04-23T00:19:13.289Z","created_by":"user_name","description":"This is a sample endpoint type exception","entries":[{"field":"actingProcess.file.signer","match":"Elastic, N.V.","operator":"included"},{"field":"event.category","match_any":["process","malware"],"operator":"included"}],"id":"1","item_id":"endpoint_list_item","list_id":"endpoint_list","meta":{},"name":"Sample Endpoint Exception List","namespace_type":"single","tags":["user added string for a tag","malware"],"tie_breaker_id":"77fd1909-6786-428a-a671-30229a719c1f","type":"simple","updated_at":"2020-04-23T00:19:13.289Z","updated_by":"user_name"}',
method: 'PUT',
signal: abortCtrl.signal,
});
diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx
index a4390ac07a5a0..308d1cf4d1b17 100644
--- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx
+++ b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx
@@ -69,6 +69,7 @@ describe('useExceptionList', () => {
list_id: 'endpoint_list',
meta: {},
name: 'Sample Endpoint Exception List',
+ namespace_type: 'single',
tags: ['user added string for a tag', 'malware'],
tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f',
type: 'simple',
@@ -84,6 +85,7 @@ describe('useExceptionList', () => {
list_id: 'endpoint_list',
meta: {},
name: 'Sample Endpoint Exception List',
+ namespace_type: 'single',
tags: ['user added string for a tag', 'malware'],
tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f',
type: 'endpoint',
diff --git a/x-pack/plugins/lists/public/exceptions/mock.ts b/x-pack/plugins/lists/public/exceptions/mock.ts
index 6980051238973..38a0e65992982 100644
--- a/x-pack/plugins/lists/public/exceptions/mock.ts
+++ b/x-pack/plugins/lists/public/exceptions/mock.ts
@@ -19,6 +19,7 @@ export const mockExceptionList: ExceptionListSchema = {
list_id: 'endpoint_list',
meta: {},
name: 'Sample Endpoint Exception List',
+ namespace_type: 'single',
tags: ['user added string for a tag', 'malware'],
tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f',
type: 'endpoint',
@@ -84,6 +85,7 @@ export const mockExceptionItem: ExceptionListItemSchema = {
list_id: 'endpoint_list',
meta: {},
name: 'Sample Endpoint Exception List',
+ namespace_type: 'single',
tags: ['user added string for a tag', 'malware'],
tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f',
type: 'simple',
diff --git a/x-pack/plugins/lists/server/index.ts b/x-pack/plugins/lists/server/index.ts
index c1e577aa60195..33f58ba65d3c3 100644
--- a/x-pack/plugins/lists/server/index.ts
+++ b/x-pack/plugins/lists/server/index.ts
@@ -9,6 +9,10 @@ import { PluginInitializerContext } from '../../../../src/core/server';
import { ConfigSchema } from './config';
import { ListPlugin } from './plugin';
+// exporting these since its required at top level in siem plugin
+export { ListClient } from './services/lists/list_client';
+export { ListPluginSetup } from './types';
+
export const config = { schema: ConfigSchema };
export const plugin = (initializerContext: PluginInitializerContext): ListPlugin =>
new ListPlugin(initializerContext);
diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts
index ddcae137a961a..e914d816b5e91 100644
--- a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts
+++ b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts
@@ -39,6 +39,7 @@ export const createExceptionListItemRoute = (router: IRouter): void => {
const siemResponse = buildSiemResponse(response);
try {
const {
+ namespace_type: namespaceType,
name,
_tags,
tags,
@@ -54,8 +55,7 @@ export const createExceptionListItemRoute = (router: IRouter): void => {
const exceptionList = await exceptionLists.getExceptionList({
id: undefined,
listId,
- // TODO: Expose the name space type
- namespaceType: 'single',
+ namespaceType,
});
if (exceptionList == null) {
return siemResponse.error({
@@ -66,8 +66,7 @@ export const createExceptionListItemRoute = (router: IRouter): void => {
const exceptionListItem = await exceptionLists.getExceptionListItem({
id: undefined,
itemId,
- // TODO: Expose the name space type
- namespaceType: 'single',
+ namespaceType,
});
if (exceptionListItem != null) {
return siemResponse.error({
@@ -84,8 +83,7 @@ export const createExceptionListItemRoute = (router: IRouter): void => {
listId,
meta,
name,
- // TODO: Expose the name space type
- namespaceType: 'single',
+ namespaceType,
tags,
type,
});
diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts
index c8a1b080c16f6..9be6b72dcd255 100644
--- a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts
+++ b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts
@@ -38,13 +38,21 @@ export const createExceptionListRoute = (router: IRouter): void => {
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
try {
- const { name, _tags, tags, meta, description, list_id: listId, type } = request.body;
+ const {
+ name,
+ _tags,
+ tags,
+ meta,
+ namespace_type: namespaceType,
+ description,
+ list_id: listId,
+ type,
+ } = request.body;
const exceptionLists = getExceptionListClient(context);
const exceptionList = await exceptionLists.getExceptionList({
id: undefined,
listId,
- // TODO: Expose the name space type
- namespaceType: 'single',
+ namespaceType,
});
if (exceptionList != null) {
return siemResponse.error({
@@ -58,8 +66,7 @@ export const createExceptionListRoute = (router: IRouter): void => {
listId,
meta,
name,
- // TODO: Expose the name space type
- namespaceType: 'single',
+ namespaceType,
tags,
type,
});
diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts
index e10ffab5359b0..2c91fe3c28681 100644
--- a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts
+++ b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts
@@ -13,7 +13,11 @@ import {
transformError,
validate,
} from '../siem_server_deps';
-import { deleteExceptionListItemSchema, exceptionListItemSchema } from '../../common/schemas';
+import {
+ DeleteExceptionListItemSchemaDecoded,
+ deleteExceptionListItemSchema,
+ exceptionListItemSchema,
+} from '../../common/schemas';
import { getErrorMessageExceptionListItem, getExceptionListClient } from './utils';
@@ -25,14 +29,17 @@ export const deleteExceptionListItemRoute = (router: IRouter): void => {
},
path: EXCEPTION_LIST_ITEM_URL,
validate: {
- query: buildRouteValidation(deleteExceptionListItemSchema),
+ query: buildRouteValidation<
+ typeof deleteExceptionListItemSchema,
+ DeleteExceptionListItemSchemaDecoded
+ >(deleteExceptionListItemSchema),
},
},
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
try {
const exceptionLists = getExceptionListClient(context);
- const { item_id: itemId, id } = request.query;
+ const { item_id: itemId, id, namespace_type: namespaceType } = request.query;
if (itemId == null && id == null) {
return siemResponse.error({
body: 'Either "item_id" or "id" needs to be defined in the request',
@@ -42,7 +49,7 @@ export const deleteExceptionListItemRoute = (router: IRouter): void => {
const deleted = await exceptionLists.deleteExceptionListItem({
id,
itemId,
- namespaceType: 'single', // TODO: Bubble this up
+ namespaceType,
});
if (deleted == null) {
return siemResponse.error({
diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts
index ef30ab6ab64c5..b4c67c0ab1418 100644
--- a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts
+++ b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts
@@ -13,7 +13,11 @@ import {
transformError,
validate,
} from '../siem_server_deps';
-import { deleteExceptionListSchema, exceptionListSchema } from '../../common/schemas';
+import {
+ DeleteExceptionListSchemaDecoded,
+ deleteExceptionListSchema,
+ exceptionListSchema,
+} from '../../common/schemas';
import { getErrorMessageExceptionList, getExceptionListClient } from './utils';
@@ -25,25 +29,27 @@ export const deleteExceptionListRoute = (router: IRouter): void => {
},
path: EXCEPTION_LIST_URL,
validate: {
- query: buildRouteValidation(deleteExceptionListSchema),
+ query: buildRouteValidation<
+ typeof deleteExceptionListSchema,
+ DeleteExceptionListSchemaDecoded
+ >(deleteExceptionListSchema),
},
},
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
try {
const exceptionLists = getExceptionListClient(context);
- const { list_id: listId, id } = request.query;
+ const { list_id: listId, id, namespace_type: namespaceType } = request.query;
if (listId == null && id == null) {
return siemResponse.error({
body: 'Either "list_id" or "id" needs to be defined in the request',
statusCode: 400,
});
} else {
- // TODO: At the moment this will delete the list but we need to delete all the list items before deleting the list
const deleted = await exceptionLists.deleteExceptionList({
id,
listId,
- namespaceType: 'single',
+ namespaceType,
});
if (deleted == null) {
return siemResponse.error({
diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts
index 3b5503ffb9833..1820ffdeadb88 100644
--- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts
+++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts
@@ -13,7 +13,11 @@ import {
transformError,
validate,
} from '../siem_server_deps';
-import { findExceptionListItemSchema, foundExceptionListItemSchema } from '../../common/schemas';
+import {
+ FindExceptionListItemSchemaDecoded,
+ findExceptionListItemSchema,
+ foundExceptionListItemSchema,
+} from '../../common/schemas';
import { getExceptionListClient } from './utils';
@@ -25,7 +29,10 @@ export const findExceptionListItemRoute = (router: IRouter): void => {
},
path: `${EXCEPTION_LIST_ITEM_URL}/_find`,
validate: {
- query: buildRouteValidation(findExceptionListItemSchema),
+ query: buildRouteValidation<
+ typeof findExceptionListItemSchema,
+ FindExceptionListItemSchemaDecoded
+ >(findExceptionListItemSchema),
},
},
async (context, request, response) => {
@@ -35,6 +42,7 @@ export const findExceptionListItemRoute = (router: IRouter): void => {
const {
filter,
list_id: listId,
+ namespace_type: namespaceType,
page,
per_page: perPage,
sort_field: sortField,
@@ -43,7 +51,7 @@ export const findExceptionListItemRoute = (router: IRouter): void => {
const exceptionListItems = await exceptionLists.findExceptionListItem({
filter,
listId,
- namespaceType: 'single', // TODO: Bubble this up
+ namespaceType,
page,
perPage,
sortField,
diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts
index 41c0c0760e03b..3181deda8b91d 100644
--- a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts
+++ b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts
@@ -13,7 +13,11 @@ import {
transformError,
validate,
} from '../siem_server_deps';
-import { findExceptionListSchema, foundExceptionListSchema } from '../../common/schemas';
+import {
+ FindExceptionListSchemaDecoded,
+ findExceptionListSchema,
+ foundExceptionListSchema,
+} from '../../common/schemas';
import { getExceptionListClient } from './utils';
@@ -25,7 +29,9 @@ export const findExceptionListRoute = (router: IRouter): void => {
},
path: `${EXCEPTION_LIST_URL}/_find`,
validate: {
- query: buildRouteValidation(findExceptionListSchema),
+ query: buildRouteValidation(
+ findExceptionListSchema
+ ),
},
},
async (context, request, response) => {
@@ -35,13 +41,14 @@ export const findExceptionListRoute = (router: IRouter): void => {
const {
filter,
page,
+ namespace_type: namespaceType,
per_page: perPage,
sort_field: sortField,
sort_order: sortOrder,
} = request.query;
const exceptionListItems = await exceptionLists.findExceptionList({
filter,
- namespaceType: 'single', // TODO: Bubble this up
+ namespaceType,
page,
perPage,
sortField,
diff --git a/x-pack/plugins/lists/server/routes/find_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_list_item_route.ts
new file mode 100644
index 0000000000000..37b5fe44b919c
--- /dev/null
+++ b/x-pack/plugins/lists/server/routes/find_list_item_route.ts
@@ -0,0 +1,99 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { IRouter } from 'kibana/server';
+
+import { LIST_ITEM_URL } from '../../common/constants';
+import {
+ buildRouteValidation,
+ buildSiemResponse,
+ transformError,
+ validate,
+} from '../siem_server_deps';
+import { findListItemSchema, foundListItemSchema } from '../../common/schemas';
+import { decodeCursor } from '../services/utils';
+
+import { getListClient } from './utils';
+
+export const findListItemRoute = (router: IRouter): void => {
+ router.get(
+ {
+ options: {
+ tags: ['access:lists'],
+ },
+ path: `${LIST_ITEM_URL}/_find`,
+ validate: {
+ query: buildRouteValidation(findListItemSchema),
+ },
+ },
+ async (context, request, response) => {
+ const siemResponse = buildSiemResponse(response);
+ try {
+ const lists = getListClient(context);
+ const {
+ cursor,
+ filter: filterOrUndefined,
+ list_id: listId,
+ page: pageOrUndefined,
+ per_page: perPageOrUndefined,
+ sort_field: sortField,
+ sort_order: sortOrder,
+ } = request.query;
+
+ const page = pageOrUndefined ?? 1;
+ const perPage = perPageOrUndefined ?? 20;
+ const filter = filterOrUndefined ?? '';
+ const {
+ isValid,
+ errorMessage,
+ cursor: [currentIndexPosition, searchAfter],
+ } = decodeCursor({
+ cursor,
+ page,
+ perPage,
+ sortField,
+ });
+
+ if (!isValid) {
+ return siemResponse.error({
+ body: errorMessage,
+ statusCode: 400,
+ });
+ } else {
+ const exceptionList = await lists.findListItem({
+ currentIndexPosition,
+ filter,
+ listId,
+ page,
+ perPage,
+ searchAfter,
+ sortField,
+ sortOrder,
+ });
+ if (exceptionList == null) {
+ return siemResponse.error({
+ body: `list id: "${listId}" does not exist`,
+ statusCode: 404,
+ });
+ } else {
+ const [validated, errors] = validate(exceptionList, foundListItemSchema);
+ if (errors != null) {
+ return siemResponse.error({ body: errors, statusCode: 500 });
+ } else {
+ return response.ok({ body: validated ?? {} });
+ }
+ }
+ }
+ } catch (err) {
+ const error = transformError(err);
+ return siemResponse.error({
+ body: error.message,
+ statusCode: error.statusCode,
+ });
+ }
+ }
+ );
+};
diff --git a/x-pack/plugins/lists/server/routes/find_list_route.ts b/x-pack/plugins/lists/server/routes/find_list_route.ts
new file mode 100644
index 0000000000000..04b33e3d67075
--- /dev/null
+++ b/x-pack/plugins/lists/server/routes/find_list_route.ts
@@ -0,0 +1,89 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { IRouter } from 'kibana/server';
+
+import { LIST_URL } from '../../common/constants';
+import {
+ buildRouteValidation,
+ buildSiemResponse,
+ transformError,
+ validate,
+} from '../siem_server_deps';
+import { findListSchema, foundListSchema } from '../../common/schemas';
+import { decodeCursor } from '../services/utils';
+
+import { getListClient } from './utils';
+
+export const findListRoute = (router: IRouter): void => {
+ router.get(
+ {
+ options: {
+ tags: ['access:lists'],
+ },
+ path: `${LIST_URL}/_find`,
+ validate: {
+ query: buildRouteValidation(findListSchema),
+ },
+ },
+ async (context, request, response) => {
+ const siemResponse = buildSiemResponse(response);
+ try {
+ const lists = getListClient(context);
+ const {
+ cursor,
+ filter: filterOrUndefined,
+ page: pageOrUndefined,
+ per_page: perPageOrUndefined,
+ sort_field: sortField,
+ sort_order: sortOrder,
+ } = request.query;
+
+ const page = pageOrUndefined ?? 1;
+ const perPage = perPageOrUndefined ?? 20;
+ const filter = filterOrUndefined ?? '';
+ const {
+ isValid,
+ errorMessage,
+ cursor: [currentIndexPosition, searchAfter],
+ } = decodeCursor({
+ cursor,
+ page,
+ perPage,
+ sortField,
+ });
+ if (!isValid) {
+ return siemResponse.error({
+ body: errorMessage,
+ statusCode: 400,
+ });
+ } else {
+ const exceptionList = await lists.findList({
+ currentIndexPosition,
+ filter,
+ page,
+ perPage,
+ searchAfter,
+ sortField,
+ sortOrder,
+ });
+ const [validated, errors] = validate(exceptionList, foundListSchema);
+ if (errors != null) {
+ return siemResponse.error({ body: errors, statusCode: 500 });
+ } else {
+ return response.ok({ body: validated ?? {} });
+ }
+ }
+ } catch (err) {
+ const error = transformError(err);
+ return siemResponse.error({
+ body: error.message,
+ statusCode: error.statusCode,
+ });
+ }
+ }
+ );
+};
diff --git a/x-pack/plugins/lists/server/routes/index.ts b/x-pack/plugins/lists/server/routes/index.ts
index 97f497bca7183..72117c46213fe 100644
--- a/x-pack/plugins/lists/server/routes/index.ts
+++ b/x-pack/plugins/lists/server/routes/index.ts
@@ -17,6 +17,8 @@ export * from './delete_list_route';
export * from './export_list_item_route';
export * from './find_exception_list_item_route';
export * from './find_exception_list_route';
+export * from './find_list_item_route';
+export * from './find_list_route';
export * from './import_list_item_route';
export * from './init_routes';
export * from './patch_list_item_route';
diff --git a/x-pack/plugins/lists/server/routes/init_routes.ts b/x-pack/plugins/lists/server/routes/init_routes.ts
index 16f96d99505d8..e74fa471734b0 100644
--- a/x-pack/plugins/lists/server/routes/init_routes.ts
+++ b/x-pack/plugins/lists/server/routes/init_routes.ts
@@ -20,6 +20,8 @@ import {
exportListItemRoute,
findExceptionListItemRoute,
findExceptionListRoute,
+ findListItemRoute,
+ findListRoute,
importListItemRoute,
patchListItemRoute,
patchListRoute,
@@ -41,6 +43,7 @@ export const initRoutes = (router: IRouter): void => {
updateListRoute(router);
deleteListRoute(router);
patchListRoute(router);
+ findListRoute(router);
// list items
createListItemRoute(router);
@@ -50,6 +53,7 @@ export const initRoutes = (router: IRouter): void => {
patchListItemRoute(router);
exportListItemRoute(router);
importListItemRoute(router);
+ findListItemRoute(router);
// indexes of lists
createListIndexRoute(router);
diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts
index 77d37373549c7..083d4d7a0d479 100644
--- a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts
+++ b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts
@@ -13,7 +13,11 @@ import {
transformError,
validate,
} from '../siem_server_deps';
-import { exceptionListItemSchema, readExceptionListItemSchema } from '../../common/schemas';
+import {
+ ReadExceptionListItemSchemaDecoded,
+ exceptionListItemSchema,
+ readExceptionListItemSchema,
+} from '../../common/schemas';
import { getErrorMessageExceptionListItem, getExceptionListClient } from './utils';
@@ -25,20 +29,22 @@ export const readExceptionListItemRoute = (router: IRouter): void => {
},
path: EXCEPTION_LIST_ITEM_URL,
validate: {
- query: buildRouteValidation(readExceptionListItemSchema),
+ query: buildRouteValidation<
+ typeof readExceptionListItemSchema,
+ ReadExceptionListItemSchemaDecoded
+ >(readExceptionListItemSchema),
},
},
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
try {
- const { id, item_id: itemId } = request.query;
+ const { id, item_id: itemId, namespace_type: namespaceType } = request.query;
const exceptionLists = getExceptionListClient(context);
if (id != null || itemId != null) {
const exceptionListItem = await exceptionLists.getExceptionListItem({
id,
itemId,
- // TODO: Bubble this up
- namespaceType: 'single',
+ namespaceType,
});
if (exceptionListItem == null) {
return siemResponse.error({
diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts
index 1668124acdfce..c295f045b38c2 100644
--- a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts
+++ b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts
@@ -13,7 +13,11 @@ import {
transformError,
validate,
} from '../siem_server_deps';
-import { exceptionListSchema, readExceptionListSchema } from '../../common/schemas';
+import {
+ ReadExceptionListSchemaDecoded,
+ exceptionListSchema,
+ readExceptionListSchema,
+} from '../../common/schemas';
import { getErrorMessageExceptionList, getExceptionListClient } from './utils';
@@ -25,20 +29,21 @@ export const readExceptionListRoute = (router: IRouter): void => {
},
path: EXCEPTION_LIST_URL,
validate: {
- query: buildRouteValidation(readExceptionListSchema),
+ query: buildRouteValidation(
+ readExceptionListSchema
+ ),
},
},
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
try {
- const { id, list_id: listId } = request.query;
+ const { id, list_id: listId, namespace_type: namespaceType } = request.query;
const exceptionLists = getExceptionListClient(context);
if (id != null || listId != null) {
const exceptionList = await exceptionLists.getExceptionList({
id,
listId,
- // TODO: Bubble this up
- namespaceType: 'single',
+ namespaceType,
});
if (exceptionList == null) {
return siemResponse.error({
diff --git a/x-pack/plugins/lists/server/routes/read_list_index_route.ts b/x-pack/plugins/lists/server/routes/read_list_index_route.ts
index 248fc72666d70..21f539d97fc74 100644
--- a/x-pack/plugins/lists/server/routes/read_list_index_route.ts
+++ b/x-pack/plugins/lists/server/routes/read_list_index_route.ts
@@ -31,7 +31,7 @@ export const readListIndexRoute = (router: IRouter): void => {
if (listIndexExists || listItemIndexExists) {
const [validated, errors] = validate(
- { list_index: listIndexExists, lists_item_index: listItemIndexExists },
+ { list_index: listIndexExists, list_item_index: listItemIndexExists },
listItemIndexExistSchema
);
if (errors != null) {
diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts
index 478225ee35eb8..14b97bbe15206 100644
--- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts
+++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts
@@ -48,6 +48,7 @@ export const updateExceptionListItemRoute = (router: IRouter): void => {
comment,
entries,
item_id: itemId,
+ namespace_type: namespaceType,
tags,
} = request.body;
const exceptionLists = getExceptionListClient(context);
@@ -60,7 +61,7 @@ export const updateExceptionListItemRoute = (router: IRouter): void => {
itemId,
meta,
name,
- namespaceType: 'single', // TODO: Bubble this up
+ namespaceType,
tags,
type,
});
diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts
index a112c7422b952..fe45d403c040f 100644
--- a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts
+++ b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts
@@ -38,7 +38,17 @@ export const updateExceptionListRoute = (router: IRouter): void => {
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
try {
- const { _tags, tags, name, description, id, list_id: listId, meta, type } = request.body;
+ const {
+ _tags,
+ tags,
+ name,
+ description,
+ id,
+ list_id: listId,
+ meta,
+ namespace_type: namespaceType,
+ type,
+ } = request.body;
const exceptionLists = getExceptionListClient(context);
if (id == null && listId == null) {
return siemResponse.error({
@@ -53,7 +63,7 @@ export const updateExceptionListRoute = (router: IRouter): void => {
listId,
meta,
name,
- namespaceType: 'single', // TODO: Bubble this up
+ namespaceType,
tags,
type,
});
diff --git a/x-pack/plugins/lists/server/scripts/delete_all_lists.sh b/x-pack/plugins/lists/server/scripts/delete_all_lists.sh
deleted file mode 100755
index 5b65bb14414c7..0000000000000
--- a/x-pack/plugins/lists/server/scripts/delete_all_lists.sh
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/bin/sh
-
-#
-# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
-# or more contributor license agreements. Licensed under the Elastic License;
-# you may not use this file except in compliance with the Elastic License.
-#
-
-set -e
-./check_env_variables.sh
-
-# Example: ./delete_all_lists.sh
-# https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html
-
-
-# Delete all the main lists that have children items
-curl -s -k \
- -H "Content-Type: application/json" \
- -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
- -X POST ${ELASTICSEARCH_URL}/${KIBANA_INDEX}*/_delete_by_query \
- --data '{
- "query": {
- "exists": { "field": "siem_list" }
- }
- }' \
- | jq .
-
-# Delete all the list children items as well
-curl -s -k \
- -H "Content-Type: application/json" \
- -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
- -X POST ${ELASTICSEARCH_URL}/${KIBANA_INDEX}*/_delete_by_query \
- --data '{
- "query": {
- "exists": { "field": "siem_list_item" }
- }
- }' \
- | jq .
diff --git a/x-pack/plugins/lists/server/scripts/delete_exception_list.sh b/x-pack/plugins/lists/server/scripts/delete_exception_list.sh
index fe2ca501b4416..efdb6d03db60b 100755
--- a/x-pack/plugins/lists/server/scripts/delete_exception_list.sh
+++ b/x-pack/plugins/lists/server/scripts/delete_exception_list.sh
@@ -9,8 +9,12 @@
set -e
./check_env_variables.sh
+NAMESPACE_TYPE=${2-single}
+
# Example: ./delete_exception_list.sh ${list_id}
+# Example: ./delete_exception_list.sh ${list_id} single
+# Example: ./delete_exception_list.sh ${list_id} agnostic
curl -s -k \
-H 'kbn-xsrf: 123' \
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
- -X DELETE ${KIBANA_URL}${SPACE_URL}/api/exception_lists?list_id="$1" | jq .
+ -X DELETE "${KIBANA_URL}${SPACE_URL}/api/exception_lists?list_id=$1&namespace_type=${NAMESPACE_TYPE}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/delete_exception_list_by_id.sh b/x-pack/plugins/lists/server/scripts/delete_exception_list_by_id.sh
index a87881b385328..2eb4f93d93015 100755
--- a/x-pack/plugins/lists/server/scripts/delete_exception_list_by_id.sh
+++ b/x-pack/plugins/lists/server/scripts/delete_exception_list_by_id.sh
@@ -9,8 +9,12 @@
set -e
./check_env_variables.sh
+NAMESPACE_TYPE=${2-single}
+
# Example: ./delete_exception_list_by_id.sh ${list_id}
+# Example: ./delete_exception_list_by_id.sh ${list_id} single
+# Example: ./delete_exception_list_by_id.sh ${list_id} agnostic
curl -s -k \
-H 'kbn-xsrf: 123' \
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
- -X DELETE ${KIBANA_URL}${SPACE_URL}/api/exception_lists?id="$1" | jq .
+ -X DELETE "${KIBANA_URL}${SPACE_URL}/api/exception_lists?id=$1&namespace_type=${NAMESPACE_TYPE}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/delete_exception_list_item.sh b/x-pack/plugins/lists/server/scripts/delete_exception_list_item.sh
index 7e09452a23e11..7617b4c47b1bc 100755
--- a/x-pack/plugins/lists/server/scripts/delete_exception_list_item.sh
+++ b/x-pack/plugins/lists/server/scripts/delete_exception_list_item.sh
@@ -9,8 +9,12 @@
set -e
./check_env_variables.sh
+NAMESPACE_TYPE=${2-single}
+
# Example: ./delete_exception_list_item.sh ${item_id}
+# Example: ./delete_exception_list_item.sh ${item_id} single
+# Example: ./delete_exception_list_item.sh ${item_id} agnostic
curl -s -k \
-H 'kbn-xsrf: 123' \
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
- -X DELETE ${KIBANA_URL}${SPACE_URL}/api/exception_lists/items?item_id="$1" | jq .
+ -X DELETE "${KIBANA_URL}${SPACE_URL}/api/exception_lists/items?item_id=$1&namespace_type=${NAMESPACE_TYPE}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/delete_exception_list_item_by_id.sh b/x-pack/plugins/lists/server/scripts/delete_exception_list_item_by_id.sh
index bbfbc3135ddb8..0e18004909222 100755
--- a/x-pack/plugins/lists/server/scripts/delete_exception_list_item_by_id.sh
+++ b/x-pack/plugins/lists/server/scripts/delete_exception_list_item_by_id.sh
@@ -9,8 +9,12 @@
set -e
./check_env_variables.sh
+NAMESPACE_TYPE=${2-single}
+
# Example: ./delete_exception_list_item_by_id.sh ${list_id}
+# Example: ./delete_exception_list_item_by_id.sh ${list_id} single
+# Example: ./delete_exception_list_item_by_id.sh ${list_id} agnostic
curl -s -k \
-H 'kbn-xsrf: 123' \
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
- -X DELETE ${KIBANA_URL}${SPACE_URL}/api/exception_lists/items?id="$1" | jq .
+ -X DELETE "${KIBANA_URL}${SPACE_URL}/api/exception_lists/items?id=$1&namespace_type=${NAMESPACE_TYPE}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/delete_list.sh b/x-pack/plugins/lists/server/scripts/delete_list.sh
index ce9fdd6aa21d4..95aa8eddbdf8d 100755
--- a/x-pack/plugins/lists/server/scripts/delete_list.sh
+++ b/x-pack/plugins/lists/server/scripts/delete_list.sh
@@ -13,4 +13,4 @@ set -e
curl -s -k \
-H 'kbn-xsrf: 123' \
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
- -X DELETE ${KIBANA_URL}${SPACE_URL}/api/lists?id="$1" | jq .
+ -X DELETE "${KIBANA_URL}${SPACE_URL}/api/lists?id=$1" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_agnostic.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_agnostic.json
new file mode 100644
index 0000000000000..4121b13880660
--- /dev/null
+++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_agnostic.json
@@ -0,0 +1,9 @@
+{
+ "list_id": "endpoint_list",
+ "_tags": ["endpoint", "process", "malware", "os:linux"],
+ "tags": ["user added string for a tag", "malware"],
+ "type": "endpoint",
+ "description": "This is a sample agnostic endpoint type exception",
+ "name": "Sample Endpoint Exception List",
+ "namespace_type": "agnostic"
+}
diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_agnostic.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_agnostic.json
new file mode 100644
index 0000000000000..db0b11480b81a
--- /dev/null
+++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_agnostic.json
@@ -0,0 +1,22 @@
+{
+ "list_id": "endpoint_list",
+ "item_id": "endpoint_list_item",
+ "_tags": ["endpoint", "process", "malware", "os:linux"],
+ "tags": ["user added string for a tag", "malware"],
+ "type": "simple",
+ "description": "This is a sample agnostic endpoint type exception",
+ "name": "Sample Endpoint Exception List",
+ "namespace_type": "agnostic",
+ "entries": [
+ {
+ "field": "actingProcess.file.signer",
+ "operator": "included",
+ "match": "Elastic, N.V."
+ },
+ {
+ "field": "event.category",
+ "operator": "included",
+ "match_any": ["process", "malware"]
+ }
+ ]
+}
diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_agnostic.json b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_agnostic.json
new file mode 100644
index 0000000000000..72ddd15ebee47
--- /dev/null
+++ b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_agnostic.json
@@ -0,0 +1,16 @@
+{
+ "item_id": "endpoint_list_item",
+ "_tags": ["endpoint", "process", "malware", "os:windows"],
+ "tags": ["user added string for a tag", "malware"],
+ "type": "simple",
+ "description": "This is a sample agnostic change here this list",
+ "name": "Sample Endpoint Exception List update change",
+ "namespace_type": "agnostic",
+ "entries": [
+ {
+ "field": "event.category",
+ "operator": "included",
+ "match_any": ["process", "malware"]
+ }
+ ]
+}
diff --git a/x-pack/plugins/lists/server/scripts/find_exception_list_items.sh b/x-pack/plugins/lists/server/scripts/find_exception_list_items.sh
index 85c5b0e518fab..e3f21da56d1b7 100755
--- a/x-pack/plugins/lists/server/scripts/find_exception_list_items.sh
+++ b/x-pack/plugins/lists/server/scripts/find_exception_list_items.sh
@@ -10,7 +10,11 @@ set -e
./check_env_variables.sh
LIST_ID=${1:-endpoint_list}
+NAMESPACE_TYPE=${2-single}
+
# Example: ./find_exception_list_items.sh {list-id}
+# Example: ./find_exception_list_items.sh {list-id} single
+# Example: ./find_exception_list_items.sh {list-id} agnostic
curl -s -k \
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
- -X GET ${KIBANA_URL}${SPACE_URL}/api/exception_lists/items/_find?list_id=${LIST_ID} | jq .
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists/items/_find?list_id=${LIST_ID}&namespace_type=${NAMESPACE_TYPE}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/find_exception_list_items_by_filter.sh b/x-pack/plugins/lists/server/scripts/find_exception_list_items_by_filter.sh
new file mode 100755
index 0000000000000..57313275ccd0e
--- /dev/null
+++ b/x-pack/plugins/lists/server/scripts/find_exception_list_items_by_filter.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+#
+# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+# or more contributor license agreements. Licensed under the Elastic License;
+# you may not use this file except in compliance with the Elastic License.
+#
+
+set -e
+./check_env_variables.sh
+
+LIST_ID=${1:-endpoint_list}
+FILTER=${2:-'exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List'}
+NAMESPACE_TYPE=${3-single}
+
+# The %20 is just an encoded space that is typical of URL's.
+# The %22 is just an encoded quote of "
+# Table of them for testing if needed: https://www.w3schools.com/tags/ref_urlencode.asp
+
+# Example: ./find_exception_list_items_by_filter.sh endpoint_list exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List
+# Example: ./find_exception_list_items_by_filter.sh endpoint_list exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List single
+# Example: ./find_exception_list_items_by_filter.sh endpoint_list exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List agnostic
+#
+# Example: ./find_exception_list_items_by_filter.sh endpoint_list exception-list.attributes.entries.field:actingProcess.file.signer
+# Example: ./find_exception_list_items_by_filter.sh endpoint_list "exception-list.attributes.entries.field:actingProcess.file.signe*"
+# Example: ./find_exception_list_items_by_filter.sh endpoint_list "exception-list.attributes.entries.match:Elastic*%20AND%20exception-list.attributes.entries.field:actingProcess.file.signe*"
+curl -s -k \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists/items/_find?list_id=${LIST_ID}&filter=${FILTER}&namespace_type=${NAMESPACE_TYPE}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/find_exception_lists.sh b/x-pack/plugins/lists/server/scripts/find_exception_lists.sh
index a1ee184b3e5bb..d3420e53343a3 100755
--- a/x-pack/plugins/lists/server/scripts/find_exception_lists.sh
+++ b/x-pack/plugins/lists/server/scripts/find_exception_lists.sh
@@ -9,7 +9,11 @@
set -e
./check_env_variables.sh
+NAMESPACE_TYPE=${1-single}
+
# Example: ./find_exception_lists.sh {list-id}
+# Example: ./find_exception_lists.sh {list-id} single
+# Example: ./find_exception_lists.sh {list-id} agnostic
curl -s -k \
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
- -X GET ${KIBANA_URL}${SPACE_URL}/api/exception_lists/_find | jq .
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists/_find?namespace_type=${NAMESPACE_TYPE}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/find_exception_lists_by_filter.sh b/x-pack/plugins/lists/server/scripts/find_exception_lists_by_filter.sh
new file mode 100755
index 0000000000000..3f5600af76b83
--- /dev/null
+++ b/x-pack/plugins/lists/server/scripts/find_exception_lists_by_filter.sh
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+#
+# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+# or more contributor license agreements. Licensed under the Elastic License;
+# you may not use this file except in compliance with the Elastic License.
+#
+
+set -e
+./check_env_variables.sh
+
+FILTER=${1:-'exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List'}
+NAMESPACE_TYPE=${2-single}
+
+# The %20 is just an encoded space that is typical of URL's.
+# The %22 is just an encoded quote of "
+# Table of them for testing if needed: https://www.w3schools.com/tags/ref_urlencode.asp
+
+# Example get all lists by a particular name:
+# ./find_exception_lists_by_filter.sh exception-list.attributes.name:%20Sample%20Endpoint%20Exception%20List
+# ./find_exception_lists_by_filter.sh exception-list.attributes.tags:%20malware
+# ./find_exception_lists_by_filter.sh exception-list.attributes.tags:%20malware single
+# ./find_exception_lists_by_filter.sh exception-list.attributes.tags:%20malware agnostic
+curl -s -k \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists/_find?filter=${FILTER}&namespace_type=${NAMESPACE_TYPE}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/find_list_items.sh b/x-pack/plugins/lists/server/scripts/find_list_items.sh
new file mode 100755
index 0000000000000..c4a610e313fa8
--- /dev/null
+++ b/x-pack/plugins/lists/server/scripts/find_list_items.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+#
+# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+# or more contributor license agreements. Licensed under the Elastic License;
+# you may not use this file except in compliance with the Elastic License.
+#
+
+set -e
+./check_env_variables.sh
+
+PAGE=${1-1}
+PER_PAGE=${2-20}
+LIST_ID=${3-list-ip}
+
+# Example: ./find_list_items.sh 1 20 list-ip
+curl -s -k \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items/_find?list_id=${LIST_ID}&page=${PAGE}&per_page=${PER_PAGE}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/find_list_items_with_cursor.sh b/x-pack/plugins/lists/server/scripts/find_list_items_with_cursor.sh
new file mode 100755
index 0000000000000..3fd5178b2d9b1
--- /dev/null
+++ b/x-pack/plugins/lists/server/scripts/find_list_items_with_cursor.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+#
+# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+# or more contributor license agreements. Licensed under the Elastic License;
+# you may not use this file except in compliance with the Elastic License.
+#
+
+set -e
+./check_env_variables.sh
+
+PAGE=${1-1}
+PER_PAGE=${2-20}
+LIST_ID=${3-list-ip}
+CURSOR=${4-invalid}
+
+# Example:
+# ./find_list_items.sh 1 20 | jq .cursor
+# Copy the cursor into the argument below like so
+# ./find_list_items_with_cursor.sh 1 10 list-ip eyJwYWdlX2luZGV4IjoyMCwic2VhcmNoX2FmdGVyIjpbIjAyZDZlNGY3LWUzMzAtNGZkYi1iNTY0LTEzZjNiOTk1MjRiYSJdfQ==
+curl -s -k \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items/_find?list_id=${LIST_ID}&page=${PAGE}&per_page=${PER_PAGE}&cursor=${CURSOR}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/find_list_items_with_sort.sh b/x-pack/plugins/lists/server/scripts/find_list_items_with_sort.sh
new file mode 100755
index 0000000000000..dcea698be231d
--- /dev/null
+++ b/x-pack/plugins/lists/server/scripts/find_list_items_with_sort.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+#
+# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+# or more contributor license agreements. Licensed under the Elastic License;
+# you may not use this file except in compliance with the Elastic License.
+#
+
+set -e
+./check_env_variables.sh
+
+PAGE=${1-1}
+PER_PAGE=${2-20}
+SORT_FIELD=${3-value}
+SORT_ORDER=${4-asc}
+LIST_ID=${5-list-ip}
+
+# Example: ./find_list_items_with_sort.sh 1 20 value asc list-ip
+curl -s -k \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items/_find?list_id=${LIST_ID}&page=${PAGE}&per_page=${PER_PAGE}&sort_field=${SORT_FIELD}&sort_order=${SORT_ORDER}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/find_list_items_with_sort_cursor.sh b/x-pack/plugins/lists/server/scripts/find_list_items_with_sort_cursor.sh
new file mode 100755
index 0000000000000..07b67a9bd1c5f
--- /dev/null
+++ b/x-pack/plugins/lists/server/scripts/find_list_items_with_sort_cursor.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+#
+# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+# or more contributor license agreements. Licensed under the Elastic License;
+# you may not use this file except in compliance with the Elastic License.
+#
+
+set -e
+./check_env_variables.sh
+
+PAGE=${1-1}
+PER_PAGE=${2-20}
+SORT_FIELD=${3-value}
+SORT_ORDER=${4-asc}
+LIST_ID=${5-list-ip}
+CURSOR=${6-invalid}
+
+# Example: ./find_list_items_with_sort_cursor.sh 1 20 value asc list-ip
+curl -s -k \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items/_find?list_id=${LIST_ID}&page=${PAGE}&per_page=${PER_PAGE}&sort_field=${SORT_FIELD}&sort_order=${SORT_ORDER}&cursor=${CURSOR}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/find_lists.sh b/x-pack/plugins/lists/server/scripts/find_lists.sh
new file mode 100755
index 0000000000000..6ff673c91cad4
--- /dev/null
+++ b/x-pack/plugins/lists/server/scripts/find_lists.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+#
+# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+# or more contributor license agreements. Licensed under the Elastic License;
+# you may not use this file except in compliance with the Elastic License.
+#
+
+set -e
+./check_env_variables.sh
+
+PAGE=${1-1}
+PER_PAGE=${2-20}
+
+# Example: ./find_lists.sh 1 20
+curl -s -k \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/_find?page=${PAGE}&per_page=${PER_PAGE}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/find_lists_with_cursor.sh b/x-pack/plugins/lists/server/scripts/find_lists_with_cursor.sh
new file mode 100755
index 0000000000000..a3bff5c37d090
--- /dev/null
+++ b/x-pack/plugins/lists/server/scripts/find_lists_with_cursor.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+#
+# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+# or more contributor license agreements. Licensed under the Elastic License;
+# you may not use this file except in compliance with the Elastic License.
+#
+
+set -e
+./check_env_variables.sh
+
+PAGE=${1-1}
+PER_PAGE=${2-20}
+CURSOR=${3-invalid}
+
+# Example:
+# ./find_lists.sh 1 20 | jq .cursor
+# Copy the cursor into the argument below like so
+# ./find_lists_with_cursor.sh 1 10 eyJwYWdlX2luZGV4IjoyMCwic2VhcmNoX2FmdGVyIjpbIjAyZDZlNGY3LWUzMzAtNGZkYi1iNTY0LTEzZjNiOTk1MjRiYSJdfQ==
+curl -s -k \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/_find?page=${PAGE}&per_page=${PER_PAGE}&cursor=${CURSOR}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/find_lists_with_filter.sh b/x-pack/plugins/lists/server/scripts/find_lists_with_filter.sh
new file mode 100755
index 0000000000000..1919d13fdf793
--- /dev/null
+++ b/x-pack/plugins/lists/server/scripts/find_lists_with_filter.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+#
+# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+# or more contributor license agreements. Licensed under the Elastic License;
+# you may not use this file except in compliance with the Elastic License.
+#
+
+set -e
+./check_env_variables.sh
+
+PAGE=${1-1}
+PER_PAGE=${2-20}
+FILTER=${3-type:ip}
+# Example: ./find_lists_with_filter.sh 1 20 type:ip
+curl -s -k \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/_find?page=${PAGE}&per_page=${PER_PAGE}&filter=${FILTER}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/find_lists_with_sort.sh b/x-pack/plugins/lists/server/scripts/find_lists_with_sort.sh
new file mode 100755
index 0000000000000..411f3a396cdb3
--- /dev/null
+++ b/x-pack/plugins/lists/server/scripts/find_lists_with_sort.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+#
+# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+# or more contributor license agreements. Licensed under the Elastic License;
+# you may not use this file except in compliance with the Elastic License.
+#
+
+set -e
+./check_env_variables.sh
+
+PAGE=${1-1}
+PER_PAGE=${2-20}
+SORT_FIELD=${3-name}
+SORT_ORDER=${4-asc}
+
+# Example: ./find_lists_with_sort.sh 1 20 name asc
+curl -s -k \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/_find?page=${PAGE}&per_page=${PER_PAGE}&sort_field=${SORT_FIELD}&sort_order=${SORT_ORDER}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/find_lists_with_sort_cursor.sh b/x-pack/plugins/lists/server/scripts/find_lists_with_sort_cursor.sh
new file mode 100755
index 0000000000000..c706eb68869ef
--- /dev/null
+++ b/x-pack/plugins/lists/server/scripts/find_lists_with_sort_cursor.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+#
+# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+# or more contributor license agreements. Licensed under the Elastic License;
+# you may not use this file except in compliance with the Elastic License.
+#
+
+set -e
+./check_env_variables.sh
+
+PAGE=${1-1}
+PER_PAGE=${2-20}
+SORT_FIELD=${3-name}
+SORT_ORDER=${4-asc}
+CURSOR=${5-invalid}
+
+# Example: ./find_lists_with_sort_cursor.sh 1 20 name asc
+curl -s -k \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/_find?page=${PAGE}&per_page=${PER_PAGE}&sort_field=${SORT_FIELD}&sort_order=${SORT_ORDER}&cursor=${CURSOR}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/get_exception_list.sh b/x-pack/plugins/lists/server/scripts/get_exception_list.sh
index 34e6de2576879..9aa15a08dec14 100755
--- a/x-pack/plugins/lists/server/scripts/get_exception_list.sh
+++ b/x-pack/plugins/lists/server/scripts/get_exception_list.sh
@@ -9,7 +9,10 @@
set -e
./check_env_variables.sh
-# Example: ./get_exception_list.sh {id}
+NAMESPACE_TYPE=${2-single}
+
+# Example: ./get_exception_list.sh {id} single
+# Example: ./get_exception_list.sh {id} agnostic
curl -s -k \
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
- -X GET ${KIBANA_URL}${SPACE_URL}/api/exception_lists?list_id="$1" | jq .
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists?list_id=$1&namespace_type=${NAMESPACE_TYPE}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/get_exception_list_by_id.sh b/x-pack/plugins/lists/server/scripts/get_exception_list_by_id.sh
index 0420a1f702328..bcd6721b6fd00 100755
--- a/x-pack/plugins/lists/server/scripts/get_exception_list_by_id.sh
+++ b/x-pack/plugins/lists/server/scripts/get_exception_list_by_id.sh
@@ -9,7 +9,9 @@
set -e
./check_env_variables.sh
+NAMESPACE_TYPE=${2-single}
+
# Example: ./get_exception_list_by_id.sh {id}
curl -s -k \
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
- -X GET ${KIBANA_URL}${SPACE_URL}/api/exception_lists?id="$1" | jq .
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists?id=$1&namespace_type=${NAMESPACE_TYPE}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/get_exception_list_item.sh b/x-pack/plugins/lists/server/scripts/get_exception_list_item.sh
index ac8337aab8368..141bbe60f193f 100755
--- a/x-pack/plugins/lists/server/scripts/get_exception_list_item.sh
+++ b/x-pack/plugins/lists/server/scripts/get_exception_list_item.sh
@@ -9,7 +9,11 @@
set -e
./check_env_variables.sh
+NAMESPACE_TYPE=${2-single}
+
# Example: ./get_exception_list_item.sh {id}
+# Example: ./get_exception_list_item.sh {id} single
+# Example: ./get_exception_list_item.sh {id} agnostic
curl -s -k \
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
- -X GET ${KIBANA_URL}${SPACE_URL}/api/exception_lists/items?item_id="$1" | jq .
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists/items?item_id=$1&namespace_type=${NAMESPACE_TYPE}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/get_exception_list_item_by_id.sh b/x-pack/plugins/lists/server/scripts/get_exception_list_item_by_id.sh
index 575a529c69906..97a90c28daebd 100755
--- a/x-pack/plugins/lists/server/scripts/get_exception_list_item_by_id.sh
+++ b/x-pack/plugins/lists/server/scripts/get_exception_list_item_by_id.sh
@@ -9,7 +9,11 @@
set -e
./check_env_variables.sh
+NAMESPACE_TYPE=${2-single}
+
# Example: ./get_exception_list_item_by_id.sh {id}
+# Example: ./get_exception_list_item_by_id.sh {id} single
+# Example: ./get_exception_list_item_by_id.sh {id} agnostic
curl -s -k \
-u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
- -X GET ${KIBANA_URL}${SPACE_URL}/api/exception_lists/items?id="$1" | jq .
+ -X GET "${KIBANA_URL}${SPACE_URL}/api/exception_lists/items?id=$1&namespace_type=${NAMESPACE_TYPE}" | jq .
diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_auto_id.json b/x-pack/plugins/lists/server/scripts/lists/new/list_auto_id.json
new file mode 100644
index 0000000000000..ef48ba8f67009
--- /dev/null
+++ b/x-pack/plugins/lists/server/scripts/lists/new/list_auto_id.json
@@ -0,0 +1,5 @@
+{
+ "name": "Simple list with a type of ip and an auto created id",
+ "description": "list with an auto created id",
+ "type": "ip"
+}
diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts
index 7ba832e72bb8e..c6d4bc006ef0b 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts
@@ -15,12 +15,12 @@ import {
ListId,
MetaOrUndefined,
Name,
+ NamespaceType,
Tags,
_Tags,
} from '../../../common/schemas';
import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils';
-import { NamespaceType } from './types';
interface CreateExceptionListOptions {
_tags: _Tags;
@@ -68,5 +68,5 @@ export const createExceptionList = async ({
type,
updated_by: user,
});
- return transformSavedObjectToExceptionList({ savedObject });
+ return transformSavedObjectToExceptionList({ namespaceType, savedObject });
};
diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts
index 4a6dc1da97854..44e87ab06f52b 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts
@@ -18,12 +18,12 @@ import {
ListId,
MetaOrUndefined,
Name,
+ NamespaceType,
Tags,
_Tags,
} from '../../../common/schemas';
import { getSavedObjectType, transformSavedObjectToExceptionListItem } from './utils';
-import { NamespaceType } from './types';
interface CreateExceptionListItemOptions {
_tags: _Tags;
@@ -77,5 +77,5 @@ export const createExceptionListItem = async ({
type,
updated_by: user,
});
- return transformSavedObjectToExceptionListItem({ savedObject });
+ return transformSavedObjectToExceptionListItem({ namespaceType, savedObject });
};
diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts
index 6904438c8d275..afeed6b5e2cde 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts
@@ -6,10 +6,14 @@
import { SavedObjectsClientContract } from 'kibana/server';
-import { ExceptionListSchema, IdOrUndefined, ListIdOrUndefined } from '../../../common/schemas';
+import {
+ ExceptionListSchema,
+ IdOrUndefined,
+ ListIdOrUndefined,
+ NamespaceType,
+} from '../../../common/schemas';
import { getSavedObjectType } from './utils';
-import { NamespaceType } from './types';
import { getExceptionList } from './get_exception_list';
import { deleteExceptionListItemByList } from './delete_exception_list_items_by_list';
diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts
index 3b2d991281cd6..8dce1f1f79e35 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts
@@ -6,10 +6,14 @@
import { SavedObjectsClientContract } from 'kibana/server';
-import { ExceptionListItemSchema, IdOrUndefined, ItemIdOrUndefined } from '../../../common/schemas';
+import {
+ ExceptionListItemSchema,
+ IdOrUndefined,
+ ItemIdOrUndefined,
+ NamespaceType,
+} from '../../../common/schemas';
import { getSavedObjectType } from './utils';
-import { NamespaceType } from './types';
import { getExceptionListItem } from './get_exception_list_item';
interface DeleteExceptionListItemOptions {
diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts
index 31bf1ffacbbb2..e835ffae02c9e 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts
@@ -5,10 +5,9 @@
*/
import { SavedObjectsClientContract } from '../../../../../../src/core/server/';
-import { ListId } from '../../../common/schemas';
+import { ListId, NamespaceType } from '../../../common/schemas';
import { findExceptionListItem } from './find_exception_list_item';
-import { NamespaceType } from './types';
import { getSavedObjectType } from './utils';
const PER_PAGE = 100;
diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
index 6e71ed1b3e59d..efd117a3c38f4 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts
@@ -7,6 +7,7 @@
import { SavedObjectsClientContract } from 'kibana/server';
import {
+ ExceptionListItemSchema,
ExceptionListSchema,
FoundExceptionListItemSchema,
FoundExceptionListSchema,
@@ -59,7 +60,7 @@ export class ExceptionListClient {
itemId,
id,
namespaceType,
- }: GetExceptionListItemOptions): Promise => {
+ }: GetExceptionListItemOptions): Promise => {
const { savedObjectsClient } = this;
return getExceptionListItem({ id, itemId, namespaceType, savedObjectsClient });
};
@@ -142,7 +143,7 @@ export class ExceptionListClient {
namespaceType,
tags,
type,
- }: CreateExceptionListItemOptions): Promise => {
+ }: CreateExceptionListItemOptions): Promise => {
const { savedObjectsClient, user } = this;
return createExceptionListItem({
_tags,
@@ -173,7 +174,7 @@ export class ExceptionListClient {
namespaceType,
tags,
type,
- }: UpdateExceptionListItemOptions): Promise => {
+ }: UpdateExceptionListItemOptions): Promise => {
const { savedObjectsClient, user } = this;
return updateExceptionListItem({
_tags,
@@ -196,7 +197,7 @@ export class ExceptionListClient {
id,
itemId,
namespaceType,
- }: DeleteExceptionListItemOptions): Promise => {
+ }: DeleteExceptionListItemOptions): Promise => {
const { savedObjectsClient } = this;
return deleteExceptionListItem({
id,
diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts
index cecd6bf3397a7..0ac543afee9f9 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts
@@ -14,6 +14,7 @@ import {
EntriesArrayOrUndefined,
ExceptionListType,
ExceptionListTypeOrUndefined,
+ FilterOrUndefined,
IdOrUndefined,
ItemId,
ItemIdOrUndefined,
@@ -22,14 +23,17 @@ import {
MetaOrUndefined,
Name,
NameOrUndefined,
+ NamespaceType,
+ PageOrUndefined,
+ PerPageOrUndefined,
+ SortFieldOrUndefined,
+ SortOrderOrUndefined,
Tags,
TagsOrUndefined,
_Tags,
_TagsOrUndefined,
} from '../../../common/schemas';
-import { NamespaceType } from './types';
-
export interface ConstructorOptions {
user: string;
savedObjectsClient: SavedObjectsClientContract;
@@ -113,18 +117,18 @@ export interface UpdateExceptionListItemOptions {
export interface FindExceptionListItemOptions {
listId: ListId;
namespaceType: NamespaceType;
- filter: string | undefined;
- perPage: number | undefined;
- page: number | undefined;
- sortField: string | undefined;
- sortOrder: string | undefined;
+ filter: FilterOrUndefined;
+ perPage: PerPageOrUndefined;
+ page: PageOrUndefined;
+ sortField: SortFieldOrUndefined;
+ sortOrder: SortOrderOrUndefined;
}
export interface FindExceptionListOptions {
namespaceType: NamespaceType;
- filter: string | undefined;
- perPage: number | undefined;
- page: number | undefined;
- sortField: string | undefined;
- sortOrder: string | undefined;
+ filter: FilterOrUndefined;
+ perPage: PerPageOrUndefined;
+ page: PageOrUndefined;
+ sortField: SortFieldOrUndefined;
+ sortOrder: SortOrderOrUndefined;
}
diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts
index 539dda673208b..6a8fbf3306971 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts
@@ -6,20 +6,28 @@
import { SavedObjectsClientContract } from 'kibana/server';
-import { ExceptionListSoSchema, FoundExceptionListSchema } from '../../../common/schemas';
+import {
+ ExceptionListSoSchema,
+ FilterOrUndefined,
+ FoundExceptionListSchema,
+ NamespaceType,
+ PageOrUndefined,
+ PerPageOrUndefined,
+ SortFieldOrUndefined,
+ SortOrderOrUndefined,
+} from '../../../common/schemas';
import { SavedObjectType } from '../../saved_objects';
import { getSavedObjectType, transformSavedObjectsToFounExceptionList } from './utils';
-import { NamespaceType } from './types';
interface FindExceptionListOptions {
namespaceType: NamespaceType;
savedObjectsClient: SavedObjectsClientContract;
- filter: string | undefined;
- perPage: number | undefined;
- page: number | undefined;
- sortField: string | undefined;
- sortOrder: string | undefined;
+ filter: FilterOrUndefined;
+ perPage: PerPageOrUndefined;
+ page: PageOrUndefined;
+ sortField: SortFieldOrUndefined;
+ sortOrder: SortOrderOrUndefined;
}
export const findExceptionList = async ({
@@ -40,14 +48,14 @@ export const findExceptionList = async ({
sortOrder,
type: savedObjectType,
});
- return transformSavedObjectsToFounExceptionList({ savedObjectsFindResponse });
+ return transformSavedObjectsToFounExceptionList({ namespaceType, savedObjectsFindResponse });
};
export const getExceptionListFilter = ({
filter,
savedObjectType,
}: {
- filter: string | undefined;
+ filter: FilterOrUndefined;
savedObjectType: SavedObjectType;
}): string => {
if (filter == null) {
diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts
index d635cafbd3b1b..c3b09a5f44b15 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts
@@ -8,24 +8,29 @@ import { SavedObjectsClientContract } from 'kibana/server';
import {
ExceptionListSoSchema,
+ FilterOrUndefined,
FoundExceptionListItemSchema,
ListId,
+ NamespaceType,
+ PageOrUndefined,
+ PerPageOrUndefined,
+ SortFieldOrUndefined,
+ SortOrderOrUndefined,
} from '../../../common/schemas';
import { SavedObjectType } from '../../saved_objects';
import { getSavedObjectType, transformSavedObjectsToFounExceptionListItem } from './utils';
-import { NamespaceType } from './types';
import { getExceptionList } from './get_exception_list';
interface FindExceptionListItemOptions {
listId: ListId;
namespaceType: NamespaceType;
savedObjectsClient: SavedObjectsClientContract;
- filter: string | undefined;
- perPage: number | undefined;
- page: number | undefined;
- sortField: string | undefined;
- sortOrder: string | undefined;
+ filter: FilterOrUndefined;
+ perPage: PerPageOrUndefined;
+ page: PageOrUndefined;
+ sortField: SortFieldOrUndefined;
+ sortOrder: SortOrderOrUndefined;
}
export const findExceptionListItem = async ({
@@ -56,7 +61,10 @@ export const findExceptionListItem = async ({
sortOrder,
type: savedObjectType,
});
- return transformSavedObjectsToFounExceptionListItem({ savedObjectsFindResponse });
+ return transformSavedObjectsToFounExceptionListItem({
+ namespaceType,
+ savedObjectsFindResponse,
+ });
}
};
@@ -66,7 +74,7 @@ export const getExceptionListItemFilter = ({
savedObjectType,
}: {
listId: ListId;
- filter: string | undefined;
+ filter: FilterOrUndefined;
savedObjectType: SavedObjectType;
}): string => {
if (filter == null) {
diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts
index 8b28443b4e30c..8f511d140b0ff 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts
@@ -13,10 +13,10 @@ import {
ExceptionListSoSchema,
IdOrUndefined,
ListIdOrUndefined,
+ NamespaceType,
} from '../../../common/schemas';
import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils';
-import { NamespaceType } from './types';
interface GetExceptionListOptions {
id: IdOrUndefined;
@@ -35,7 +35,7 @@ export const getExceptionList = async ({
if (id != null) {
try {
const savedObject = await savedObjectsClient.get(savedObjectType, id);
- return transformSavedObjectToExceptionList({ savedObject });
+ return transformSavedObjectToExceptionList({ namespaceType, savedObject });
} catch (err) {
if (SavedObjectsErrorHelpers.isNotFoundError(err)) {
return null;
@@ -54,7 +54,10 @@ export const getExceptionList = async ({
type: savedObjectType,
});
if (savedObject.saved_objects[0] != null) {
- return transformSavedObjectToExceptionList({ savedObject: savedObject.saved_objects[0] });
+ return transformSavedObjectToExceptionList({
+ namespaceType,
+ savedObject: savedObject.saved_objects[0],
+ });
} else {
return null;
}
diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts
index 7ef3e4af3d604..d7efdc054c48c 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts
@@ -13,10 +13,10 @@ import {
ExceptionListSoSchema,
IdOrUndefined,
ItemIdOrUndefined,
+ NamespaceType,
} from '../../../common/schemas';
import { getSavedObjectType, transformSavedObjectToExceptionListItem } from './utils';
-import { NamespaceType } from './types';
interface GetExceptionListItemOptions {
id: IdOrUndefined;
@@ -35,7 +35,7 @@ export const getExceptionListItem = async ({
if (id != null) {
try {
const savedObject = await savedObjectsClient.get(savedObjectType, id);
- return transformSavedObjectToExceptionListItem({ savedObject });
+ return transformSavedObjectToExceptionListItem({ namespaceType, savedObject });
} catch (err) {
if (SavedObjectsErrorHelpers.isNotFoundError(err)) {
return null;
@@ -55,6 +55,7 @@ export const getExceptionListItem = async ({
});
if (savedObject.saved_objects[0] != null) {
return transformSavedObjectToExceptionListItem({
+ namespaceType,
savedObject: savedObject.saved_objects[0],
});
} else {
diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts
index 6c5ccb5e1f2fd..e4d6718ddc29f 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts
@@ -15,12 +15,12 @@ import {
ListIdOrUndefined,
MetaOrUndefined,
NameOrUndefined,
+ NamespaceType,
TagsOrUndefined,
_TagsOrUndefined,
} from '../../../common/schemas';
import { getSavedObjectType, transformSavedObjectUpdateToExceptionList } from './utils';
-import { NamespaceType } from './types';
import { getExceptionList } from './get_exception_list';
interface UpdateExceptionListOptions {
@@ -69,6 +69,6 @@ export const updateExceptionList = async ({
updated_by: user,
}
);
- return transformSavedObjectUpdateToExceptionList({ exceptionList, savedObject });
+ return transformSavedObjectUpdateToExceptionList({ exceptionList, namespaceType, savedObject });
}
};
diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts
index 4e955d4281c4d..39c319a944e38 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts
@@ -17,12 +17,12 @@ import {
ItemIdOrUndefined,
MetaOrUndefined,
NameOrUndefined,
+ NamespaceType,
TagsOrUndefined,
_TagsOrUndefined,
} from '../../../common/schemas';
import { getSavedObjectType, transformSavedObjectUpdateToExceptionListItem } from './utils';
-import { NamespaceType } from './types';
import { getExceptionListItem } from './get_exception_list_item';
interface UpdateExceptionListItemOptions {
@@ -82,6 +82,10 @@ export const updateExceptionListItem = async ({
updated_by: user,
}
);
- return transformSavedObjectUpdateToExceptionListItem({ exceptionListItem, savedObject });
+ return transformSavedObjectUpdateToExceptionListItem({
+ exceptionListItem,
+ namespaceType,
+ savedObject,
+ });
}
};
diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.ts
index 28dfb9c1cddaf..82a98f4bdd3e2 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts
@@ -12,6 +12,7 @@ import {
ExceptionListSoSchema,
FoundExceptionListItemSchema,
FoundExceptionListSchema,
+ NamespaceType,
} from '../../../common/schemas';
import {
SavedObjectType,
@@ -19,8 +20,6 @@ import {
exceptionListSavedObjectType,
} from '../../saved_objects';
-import { NamespaceType } from './types';
-
export const getSavedObjectType = ({
namespaceType,
}: {
@@ -35,8 +34,10 @@ export const getSavedObjectType = ({
export const transformSavedObjectToExceptionList = ({
savedObject,
+ namespaceType,
}: {
savedObject: SavedObject;
+ namespaceType: NamespaceType;
}): ExceptionListSchema => {
const dateNow = new Date().toISOString();
const {
@@ -68,6 +69,7 @@ export const transformSavedObjectToExceptionList = ({
list_id,
meta,
name,
+ namespace_type: namespaceType,
tags,
tie_breaker_id,
type,
@@ -79,9 +81,11 @@ export const transformSavedObjectToExceptionList = ({
export const transformSavedObjectUpdateToExceptionList = ({
exceptionList,
savedObject,
+ namespaceType,
}: {
exceptionList: ExceptionListSchema;
savedObject: SavedObjectsUpdateResponse;
+ namespaceType: NamespaceType;
}): ExceptionListSchema => {
const dateNow = new Date().toISOString();
const {
@@ -101,6 +105,7 @@ export const transformSavedObjectUpdateToExceptionList = ({
list_id: exceptionList.list_id,
meta: meta ?? exceptionList.meta,
name: name ?? exceptionList.name,
+ namespace_type: namespaceType,
tags: tags ?? exceptionList.tags,
tie_breaker_id: exceptionList.tie_breaker_id,
type: type ?? exceptionList.type,
@@ -111,8 +116,10 @@ export const transformSavedObjectUpdateToExceptionList = ({
export const transformSavedObjectToExceptionListItem = ({
savedObject,
+ namespaceType,
}: {
savedObject: SavedObject;
+ namespaceType: NamespaceType;
}): ExceptionListItemSchema => {
const dateNow = new Date().toISOString();
const {
@@ -150,6 +157,7 @@ export const transformSavedObjectToExceptionListItem = ({
list_id,
meta,
name,
+ namespace_type: namespaceType,
tags,
tie_breaker_id,
type,
@@ -161,9 +169,11 @@ export const transformSavedObjectToExceptionListItem = ({
export const transformSavedObjectUpdateToExceptionListItem = ({
exceptionListItem,
savedObject,
+ namespaceType,
}: {
exceptionListItem: ExceptionListItemSchema;
savedObject: SavedObjectsUpdateResponse;
+ namespaceType: NamespaceType;
}): ExceptionListItemSchema => {
const dateNow = new Date().toISOString();
const {
@@ -196,6 +206,7 @@ export const transformSavedObjectUpdateToExceptionListItem = ({
list_id: exceptionListItem.list_id,
meta: meta ?? exceptionListItem.meta,
name: name ?? exceptionListItem.name,
+ namespace_type: namespaceType,
tags: tags ?? exceptionListItem.tags,
tie_breaker_id: exceptionListItem.tie_breaker_id,
type: type ?? exceptionListItem.type,
@@ -206,12 +217,14 @@ export const transformSavedObjectUpdateToExceptionListItem = ({
export const transformSavedObjectsToFounExceptionListItem = ({
savedObjectsFindResponse,
+ namespaceType,
}: {
savedObjectsFindResponse: SavedObjectsFindResponse;
+ namespaceType: NamespaceType;
}): FoundExceptionListItemSchema => {
return {
data: savedObjectsFindResponse.saved_objects.map((savedObject) =>
- transformSavedObjectToExceptionListItem({ savedObject })
+ transformSavedObjectToExceptionListItem({ namespaceType, savedObject })
),
page: savedObjectsFindResponse.page,
per_page: savedObjectsFindResponse.per_page,
@@ -221,12 +234,14 @@ export const transformSavedObjectsToFounExceptionListItem = ({
export const transformSavedObjectsToFounExceptionList = ({
savedObjectsFindResponse,
+ namespaceType,
}: {
savedObjectsFindResponse: SavedObjectsFindResponse;
+ namespaceType: NamespaceType;
}): FoundExceptionListSchema => {
return {
data: savedObjectsFindResponse.saved_objects.map((savedObject) =>
- transformSavedObjectToExceptionList({ savedObject })
+ transformSavedObjectToExceptionList({ namespaceType, savedObject })
),
page: savedObjectsFindResponse.page,
per_page: savedObjectsFindResponse.per_page,
diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts
index 83a118b795192..d46b9b4703fcb 100644
--- a/x-pack/plugins/lists/server/services/items/create_list_item.ts
+++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts
@@ -58,7 +58,7 @@ export const createListItem = async ({
...transformListItemToElasticQuery({ type, value }),
};
- const response: CreateDocumentResponse = await callCluster('index', {
+ const response = await callCluster('index', {
body,
id,
index: listItemIndex,
diff --git a/x-pack/plugins/lists/server/services/items/find_list_item.ts b/x-pack/plugins/lists/server/services/items/find_list_item.ts
new file mode 100644
index 0000000000000..d10e6466d03d0
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/items/find_list_item.ts
@@ -0,0 +1,116 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { APICaller } from 'kibana/server';
+
+import {
+ Filter,
+ FoundListItemSchema,
+ ListId,
+ Page,
+ PerPage,
+ SearchEsListItemSchema,
+ SortFieldOrUndefined,
+ SortOrderOrUndefined,
+} from '../../../common/schemas';
+import { getList } from '../lists';
+import {
+ encodeCursor,
+ getQueryFilter,
+ getSearchAfterWithTieBreaker,
+ getSortWithTieBreaker,
+ scrollToStartPage,
+ transformElasticToListItem,
+} from '../utils';
+
+interface FindListItemOptions {
+ listId: ListId;
+ filter: Filter;
+ currentIndexPosition: number;
+ searchAfter: string[] | undefined;
+ perPage: PerPage;
+ page: Page;
+ sortField: SortFieldOrUndefined;
+ sortOrder: SortOrderOrUndefined;
+ callCluster: APICaller;
+ listIndex: string;
+ listItemIndex: string;
+}
+
+export const findListItem = async ({
+ callCluster,
+ currentIndexPosition,
+ filter,
+ listId,
+ page,
+ perPage,
+ searchAfter,
+ sortField: sortFieldWithPossibleValue,
+ listIndex,
+ listItemIndex,
+ sortOrder,
+}: FindListItemOptions): Promise => {
+ const query = getQueryFilter({ filter });
+ const list = await getList({ callCluster, id: listId, listIndex });
+ if (list == null) {
+ return null;
+ } else {
+ const sortField =
+ sortFieldWithPossibleValue === 'value' ? list.type : sortFieldWithPossibleValue;
+ const scroll = await scrollToStartPage({
+ callCluster,
+ currentIndexPosition,
+ filter,
+ hopSize: 100,
+ index: listItemIndex,
+ page,
+ perPage,
+ searchAfter,
+ sortField,
+ sortOrder,
+ });
+
+ const { count } = await callCluster('count', {
+ body: {
+ query,
+ },
+ ignoreUnavailable: true,
+ index: listItemIndex,
+ });
+
+ if (scroll.validSearchAfterFound) {
+ const response = await callCluster('search', {
+ body: {
+ query,
+ search_after: scroll.searchAfter,
+ sort: getSortWithTieBreaker({ sortField, sortOrder }),
+ },
+ ignoreUnavailable: true,
+ index: listItemIndex,
+ size: perPage,
+ });
+ return {
+ cursor: encodeCursor({
+ page,
+ perPage,
+ searchAfter: getSearchAfterWithTieBreaker({ response, sortField }),
+ }),
+ data: transformElasticToListItem({ response, type: list.type }),
+ page,
+ per_page: perPage,
+ total: count,
+ };
+ } else {
+ return {
+ cursor: encodeCursor({ page, perPage, searchAfter: undefined }),
+ data: [],
+ page,
+ per_page: perPage,
+ total: count,
+ };
+ }
+ }
+};
diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.ts b/x-pack/plugins/lists/server/services/items/get_list_item.ts
index 83b30d336ccd4..296d1e4e82184 100644
--- a/x-pack/plugins/lists/server/services/items/get_list_item.ts
+++ b/x-pack/plugins/lists/server/services/items/get_list_item.ts
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SearchResponse } from 'elasticsearch';
import { APICaller } from 'kibana/server';
import { Id, ListItemSchema, SearchEsListItemSchema } from '../../../common/schemas';
@@ -21,7 +20,7 @@ export const getListItem = async ({
callCluster,
listItemIndex,
}: GetListItemOptions): Promise => {
- const listItemES: SearchResponse = await callCluster('search', {
+ const listItemES = await callCluster('search', {
body: {
query: {
term: {
diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts
index 29b9b01754027..cf0ccf3f10aa6 100644
--- a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts
+++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SearchResponse } from 'elasticsearch';
import { APICaller } from 'kibana/server';
import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas';
@@ -25,7 +24,7 @@ export const getListItemByValues = async ({
type,
value,
}: GetListItemByValuesOptions): Promise => {
- const response: SearchResponse = await callCluster('search', {
+ const response = await callCluster('search', {
body: {
query: {
bool: {
diff --git a/x-pack/plugins/lists/server/services/items/index.ts b/x-pack/plugins/lists/server/services/items/index.ts
index ee1d83fabca31..bc04ba88b943e 100644
--- a/x-pack/plugins/lists/server/services/items/index.ts
+++ b/x-pack/plugins/lists/server/services/items/index.ts
@@ -8,12 +8,13 @@ export * from './buffer_lines';
export * from './create_list_item';
export * from './create_list_items_bulk';
export * from './delete_list_item_by_value';
+export * from './delete_list_item';
+export * from './find_list_item';
export * from './get_list_item_by_value';
export * from './get_list_item';
export * from './get_list_item_by_values';
+export * from './get_list_item_template';
+export * from './get_list_item_index';
export * from './update_list_item';
export * from './write_lines_to_bulk_list_items';
export * from './write_list_items_to_stream';
-export * from './get_list_item_template';
-export * from './delete_list_item';
-export * from './get_list_item_index';
diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts
index 6a71b2a0caf41..6a428b4be854d 100644
--- a/x-pack/plugins/lists/server/services/items/update_list_item.ts
+++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts
@@ -48,7 +48,7 @@ export const updateListItem = async ({
...transformListItemToElasticQuery({ type: listItem.type, value: value ?? listItem.value }),
};
- const response: CreateDocumentResponse = await callCluster('update', {
+ const response = await callCluster('update', {
body: {
doc,
},
diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts
index 10d8581ccdbc0..f485f557433c6 100644
--- a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts
+++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts
@@ -114,7 +114,7 @@ export const getResponse = async ({
listItemIndex,
size = SIZE,
}: GetResponseOptions): Promise> => {
- return callCluster('search', {
+ return callCluster('search', {
body: {
query: {
term: {
diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts
index ddbc99c88a877..0d2ee606a066d 100644
--- a/x-pack/plugins/lists/server/services/lists/create_list.ts
+++ b/x-pack/plugins/lists/server/services/lists/create_list.ts
@@ -55,7 +55,7 @@ export const createList = async ({
updated_at: createdAt,
updated_by: user,
};
- const response: CreateDocumentResponse = await callCluster('index', {
+ const response = await callCluster('index', {
body,
id,
index: listIndex,
diff --git a/x-pack/plugins/lists/server/services/lists/find_list.ts b/x-pack/plugins/lists/server/services/lists/find_list.ts
new file mode 100644
index 0000000000000..41dcdfcd0f8db
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/lists/find_list.ts
@@ -0,0 +1,104 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { APICaller } from 'kibana/server';
+
+import {
+ Filter,
+ FoundListSchema,
+ Page,
+ PerPage,
+ SearchEsListSchema,
+ SortFieldOrUndefined,
+ SortOrderOrUndefined,
+} from '../../../common/schemas';
+import {
+ encodeCursor,
+ getQueryFilter,
+ getSearchAfterWithTieBreaker,
+ getSortWithTieBreaker,
+ scrollToStartPage,
+ transformElasticToList,
+} from '../utils';
+
+interface FindListOptions {
+ filter: Filter;
+ currentIndexPosition: number;
+ searchAfter: string[] | undefined;
+ perPage: PerPage;
+ page: Page;
+ sortField: SortFieldOrUndefined;
+ sortOrder: SortOrderOrUndefined;
+ callCluster: APICaller;
+ listIndex: string;
+}
+
+export const findList = async ({
+ callCluster,
+ currentIndexPosition,
+ filter,
+ page,
+ perPage,
+ searchAfter,
+ sortField,
+ listIndex,
+ sortOrder,
+}: FindListOptions): Promise => {
+ const query = getQueryFilter({ filter });
+
+ const scroll = await scrollToStartPage({
+ callCluster,
+ currentIndexPosition,
+ filter,
+ hopSize: 100,
+ index: listIndex,
+ page,
+ perPage,
+ searchAfter,
+ sortField,
+ sortOrder,
+ });
+
+ const { count } = await callCluster('count', {
+ body: {
+ query,
+ },
+ ignoreUnavailable: true,
+ index: listIndex,
+ });
+
+ if (scroll.validSearchAfterFound) {
+ const response = await callCluster('search', {
+ body: {
+ query,
+ search_after: scroll.searchAfter,
+ sort: getSortWithTieBreaker({ sortField, sortOrder }),
+ },
+ ignoreUnavailable: true,
+ index: listIndex,
+ size: perPage,
+ });
+ return {
+ cursor: encodeCursor({
+ page,
+ perPage,
+ searchAfter: getSearchAfterWithTieBreaker({ response, sortField }),
+ }),
+ data: transformElasticToList({ response }),
+ page,
+ per_page: perPage,
+ total: count,
+ };
+ } else {
+ return {
+ cursor: encodeCursor({ page, perPage, searchAfter: undefined }),
+ data: [],
+ page,
+ per_page: perPage,
+ total: count,
+ };
+ }
+};
diff --git a/x-pack/plugins/lists/server/services/lists/get_list.ts b/x-pack/plugins/lists/server/services/lists/get_list.ts
index c04bd504ad8c0..386232bfeee1f 100644
--- a/x-pack/plugins/lists/server/services/lists/get_list.ts
+++ b/x-pack/plugins/lists/server/services/lists/get_list.ts
@@ -4,10 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SearchResponse } from 'elasticsearch';
import { APICaller } from 'kibana/server';
import { Id, ListSchema, SearchEsListSchema } from '../../../common/schemas';
+import { transformElasticToList } from '../utils/transform_elastic_to_list';
interface GetListOptions {
id: Id;
@@ -20,7 +20,7 @@ export const getList = async ({
callCluster,
listIndex,
}: GetListOptions): Promise => {
- const result: SearchResponse = await callCluster('search', {
+ const response = await callCluster('search', {
body: {
query: {
term: {
@@ -31,12 +31,6 @@ export const getList = async ({
ignoreUnavailable: true,
index: listIndex,
});
- if (result.hits.hits.length) {
- return {
- id: result.hits.hits[0]._id,
- ...result.hits.hits[0]._source,
- };
- } else {
- return null;
- }
+ const list = transformElasticToList({ response });
+ return list[0] ?? null;
};
diff --git a/x-pack/plugins/lists/server/services/lists/index.ts b/x-pack/plugins/lists/server/services/lists/index.ts
index f704ef0b05b82..bafeb929a8d53 100644
--- a/x-pack/plugins/lists/server/services/lists/index.ts
+++ b/x-pack/plugins/lists/server/services/lists/index.ts
@@ -6,6 +6,7 @@
export * from './create_list';
export * from './delete_list';
+export * from './find_list';
export * from './get_list';
export * from './get_list_template';
export * from './update_list';
diff --git a/x-pack/plugins/lists/server/services/lists/list_client.ts b/x-pack/plugins/lists/server/services/lists/list_client.ts
index cba48115c746c..5a7d20c7d64d5 100644
--- a/x-pack/plugins/lists/server/services/lists/list_client.ts
+++ b/x-pack/plugins/lists/server/services/lists/list_client.ts
@@ -6,11 +6,18 @@
import { APICaller } from 'kibana/server';
-import { ListItemArraySchema, ListItemSchema, ListSchema } from '../../../common/schemas';
+import {
+ FoundListItemSchema,
+ FoundListSchema,
+ ListItemArraySchema,
+ ListItemSchema,
+ ListSchema,
+} from '../../../common/schemas';
import { ConfigType } from '../../config';
import {
createList,
deleteList,
+ findList,
getList,
getListIndex,
getListTemplate,
@@ -21,6 +28,7 @@ import {
deleteListItem,
deleteListItemByValue,
exportListItemsToStream,
+ findListItem,
getListItem,
getListItemByValue,
getListItemByValues,
@@ -52,6 +60,8 @@ import {
DeleteListItemOptions,
DeleteListOptions,
ExportListItemsToStreamOptions,
+ FindListItemOptions,
+ FindListOptions,
GetListItemByValueOptions,
GetListItemOptions,
GetListItemsByValueOptions,
@@ -410,4 +420,56 @@ export class ListClient {
value,
});
};
+
+ public findList = async ({
+ filter,
+ currentIndexPosition,
+ perPage,
+ page,
+ sortField,
+ sortOrder,
+ searchAfter,
+ }: FindListOptions): Promise => {
+ const { callCluster } = this;
+ const listIndex = this.getListIndex();
+ return findList({
+ callCluster,
+ currentIndexPosition,
+ filter,
+ listIndex,
+ page,
+ perPage,
+ searchAfter,
+ sortField,
+ sortOrder,
+ });
+ };
+
+ public findListItem = async ({
+ listId,
+ filter,
+ currentIndexPosition,
+ perPage,
+ page,
+ sortField,
+ sortOrder,
+ searchAfter,
+ }: FindListItemOptions): Promise => {
+ const { callCluster } = this;
+ const listIndex = this.getListIndex();
+ const listItemIndex = this.getListItemIndex();
+ return findListItem({
+ callCluster,
+ currentIndexPosition,
+ filter,
+ listId,
+ listIndex,
+ listItemIndex,
+ page,
+ perPage,
+ searchAfter,
+ sortField,
+ sortOrder,
+ });
+ };
}
diff --git a/x-pack/plugins/lists/server/services/lists/list_client_types.ts b/x-pack/plugins/lists/server/services/lists/list_client_types.ts
index d66575e7a30db..4171b6ee9f165 100644
--- a/x-pack/plugins/lists/server/services/lists/list_client_types.ts
+++ b/x-pack/plugins/lists/server/services/lists/list_client_types.ts
@@ -11,11 +11,17 @@ import { APICaller } from 'kibana/server';
import {
Description,
DescriptionOrUndefined,
+ Filter,
Id,
IdOrUndefined,
+ ListId,
MetaOrUndefined,
Name,
NameOrUndefined,
+ Page,
+ PerPage,
+ SortFieldOrUndefined,
+ SortOrderOrUndefined,
Type,
} from '../../../common/schemas';
import { ConfigType } from '../../config';
@@ -110,3 +116,24 @@ export interface GetListItemsByValueOptions {
listId: string;
value: string[];
}
+
+export interface FindListOptions {
+ currentIndexPosition: number;
+ filter: Filter;
+ perPage: PerPage;
+ page: Page;
+ searchAfter: string[] | undefined;
+ sortField: SortFieldOrUndefined;
+ sortOrder: SortOrderOrUndefined;
+}
+
+export interface FindListItemOptions {
+ currentIndexPosition: number;
+ filter: Filter;
+ listId: ListId;
+ perPage: PerPage;
+ page: Page;
+ searchAfter: string[] | undefined;
+ sortField: SortFieldOrUndefined;
+ sortOrder: SortOrderOrUndefined;
+}
diff --git a/x-pack/plugins/lists/server/services/exception_lists/types.ts b/x-pack/plugins/lists/server/services/lists/types.ts
similarity index 72%
rename from x-pack/plugins/lists/server/services/exception_lists/types.ts
rename to x-pack/plugins/lists/server/services/lists/types.ts
index dbb188bc2754a..2e0e4b7d038e7 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/types.ts
+++ b/x-pack/plugins/lists/server/services/lists/types.ts
@@ -3,4 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-export type NamespaceType = 'agnostic' | 'single';
+
+interface Scroll {
+ searchAfter: string[] | undefined;
+ validSearchAfterFound: boolean;
+}
diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts
index 9859adf062485..28be50e9d6ac8 100644
--- a/x-pack/plugins/lists/server/services/lists/update_list.ts
+++ b/x-pack/plugins/lists/server/services/lists/update_list.ts
@@ -51,7 +51,7 @@ export const updateList = async ({
updated_at: updatedAt,
updated_by: user,
};
- const response: CreateDocumentResponse = await callCluster('update', {
+ const response = await callCluster('update', {
body: { doc },
id,
index: listIndex,
diff --git a/x-pack/plugins/lists/server/services/utils/calculate_scroll_math.ts b/x-pack/plugins/lists/server/services/utils/calculate_scroll_math.ts
new file mode 100644
index 0000000000000..6ec240d844f84
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/utils/calculate_scroll_math.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Page, PerPage } from '../../../common/schemas';
+
+interface CalculateScrollMathOptions {
+ perPage: PerPage;
+ page: Page;
+ hopSize: number;
+ currentIndexPosition: number;
+}
+
+interface CalculateScrollMathReturn {
+ hops: number;
+ leftOverAfterHops: number;
+}
+
+export const calculateScrollMath = ({
+ currentIndexPosition,
+ page,
+ perPage,
+ hopSize,
+}: CalculateScrollMathOptions): CalculateScrollMathReturn => {
+ const startPageIndex = (page - 1) * perPage - currentIndexPosition;
+ if (startPageIndex < 0) {
+ // This should never be hit but just in case I do a check. We do validate higher above this
+ // before the current index position gets to this point but to be safe we add this line.
+ throw new Error(
+ `page: ${page}, perPage ${perPage} and currentIndex ${currentIndexPosition} are less than zero`
+ );
+ }
+ const hops = Math.floor(startPageIndex / hopSize);
+ const leftOverAfterHops = startPageIndex - hops * hopSize;
+ return {
+ hops,
+ leftOverAfterHops,
+ };
+};
diff --git a/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts b/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts
new file mode 100644
index 0000000000000..205d61f204ba6
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/utils/encode_decode_cursor.ts
@@ -0,0 +1,127 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import * as t from 'io-ts';
+import { fold } from 'fp-ts/lib/Either';
+import { pipe } from 'fp-ts/lib/pipeable';
+
+import { CursorOrUndefined, SortFieldOrUndefined } from '../../../common/schemas';
+import { exactCheck } from '../../../common/siem_common_deps';
+
+/**
+ * Used only internally for this ad-hoc opaque cursor structure to keep track of the
+ * current page_index that the search_after is currently on. The format of an array
+ * is to be consistent with other compact forms of opaque nature such as a saved object versioning.
+ *
+ * The format is [index of item, search_after_array]
+ */
+
+// TODO: Use PositiveInteger from siem once that type is outside of server and in common
+export const contextCursor = t.tuple([t.number, t.union([t.array(t.string), t.undefined])]);
+
+export type ContextCursor = t.TypeOf;
+
+export interface EncodeCursorOptions {
+ searchAfter: string[] | undefined;
+ page: number;
+ perPage: number;
+}
+
+export const encodeCursor = ({ searchAfter, page, perPage }: EncodeCursorOptions): string => {
+ const index = searchAfter != null ? page * perPage : 0;
+ const encodedCursor = searchAfter != null ? [index, searchAfter] : [index];
+ const scrollStringed = JSON.stringify(encodedCursor);
+ return Buffer.from(scrollStringed).toString('base64');
+};
+
+export interface DecodeCursorOptions {
+ cursor: CursorOrUndefined;
+ page: number;
+ perPage: number;
+ sortField: SortFieldOrUndefined;
+}
+
+export interface DecodeCursor {
+ cursor: ContextCursor;
+ isValid: boolean;
+ errorMessage: string;
+}
+
+export const decodeCursor = ({
+ cursor,
+ page,
+ perPage,
+ sortField,
+}: DecodeCursorOptions): DecodeCursor => {
+ if (cursor == null) {
+ return {
+ cursor: [0, undefined],
+ errorMessage: '',
+ isValid: true,
+ };
+ } else {
+ const fromBuffer = Buffer.from(cursor, 'base64').toString();
+ const parsed = parseOrUndefined(fromBuffer);
+ if (parsed == null) {
+ return {
+ cursor: [0, undefined],
+ errorMessage: 'Error parsing JSON from base64 encoded cursor',
+ isValid: false,
+ };
+ } else {
+ const decodedCursor = contextCursor.decode(parsed);
+ const checked = exactCheck(parsed, decodedCursor);
+
+ const onLeft = (): ContextCursor | undefined => undefined;
+ const onRight = (schema: ContextCursor): ContextCursor | undefined => schema;
+ const cursorOrUndefined = pipe(checked, fold(onLeft, onRight));
+
+ const startPageIndex = (page - 1) * perPage;
+ if (cursorOrUndefined == null) {
+ return {
+ cursor: [0, undefined],
+ errorMessage: 'Error decoding cursor structure',
+ isValid: false,
+ };
+ } else {
+ const [index, searchAfter] = cursorOrUndefined;
+ if (index < 0) {
+ return {
+ cursor: [0, undefined],
+ errorMessage: 'index of cursor cannot be less 0',
+ isValid: false,
+ };
+ } else if (index > startPageIndex) {
+ return {
+ cursor: [0, undefined],
+ errorMessage: `index: ${index} of cursor cannot be greater than the start page index: ${startPageIndex}`,
+ isValid: false,
+ };
+ } else if (searchAfter != null && searchAfter.length > 1 && sortField == null) {
+ return {
+ cursor: [0, undefined],
+ errorMessage: '',
+ isValid: false,
+ };
+ } else {
+ return {
+ cursor: [index, searchAfter != null ? searchAfter : undefined],
+ errorMessage: '',
+ isValid: true,
+ };
+ }
+ }
+ }
+ }
+};
+
+export const parseOrUndefined = (input: string): ContextCursor | undefined => {
+ try {
+ return JSON.parse(input);
+ } catch (err) {
+ return undefined;
+ }
+};
diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter.test.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter.test.ts
new file mode 100644
index 0000000000000..50c266eb5d573
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/utils/get_query_filter.test.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { getQueryFilter } from './get_query_filter';
+
+describe('get_query_filter', () => {
+ test('it should work with a basic kuery', () => {
+ const esQuery = getQueryFilter({ filter: 'type: ip' });
+ expect(esQuery).toEqual({
+ bool: {
+ filter: [
+ {
+ bool: {
+ minimum_should_match: 1,
+ should: [
+ {
+ match: {
+ type: 'ip',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ must: [],
+ must_not: [],
+ should: [],
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter.ts
new file mode 100644
index 0000000000000..cf0dd5b6250e5
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/utils/get_query_filter.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { DslQuery, EsQueryConfig } from 'src/plugins/data/common';
+
+import { Filter, Query, esQuery } from '../../../../../../src/plugins/data/server';
+
+export interface GetQueryFilterOptions {
+ filter: string;
+}
+
+export interface GetQueryFilterReturn {
+ bool: { must: DslQuery[]; filter: Filter[]; should: never[]; must_not: Filter[] };
+}
+
+export const getQueryFilter = ({ filter }: GetQueryFilterOptions): GetQueryFilterReturn => {
+ const kqlQuery: Query = {
+ language: 'kuery',
+ query: filter,
+ };
+ const config: EsQueryConfig = {
+ allowLeadingWildcards: true,
+ dateFormatTZ: 'Zulu',
+ ignoreFilterIfFieldNotInIndex: false,
+ queryStringOptions: { analyze_wildcard: true },
+ };
+
+ return esQuery.buildEsQuery(undefined, kqlQuery, [], config);
+};
diff --git a/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts b/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts
new file mode 100644
index 0000000000000..9721baefbe5ee
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/utils/get_search_after_scroll.ts
@@ -0,0 +1,64 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { APICaller } from 'kibana/server';
+
+import { Filter, SortFieldOrUndefined, SortOrderOrUndefined } from '../../../common/schemas';
+
+import { getQueryFilter } from './get_query_filter';
+import { getSortWithTieBreaker } from './get_sort_with_tie_breaker';
+import { getSourceWithTieBreaker } from './get_source_with_tie_breaker';
+import { TieBreaker, getSearchAfterWithTieBreaker } from './get_search_after_with_tie_breaker';
+
+interface GetSearchAfterOptions {
+ callCluster: APICaller;
+ filter: Filter;
+ hops: number;
+ hopSize: number;
+ searchAfter: string[] | undefined;
+ index: string;
+ sortField: SortFieldOrUndefined;
+ sortOrder: SortOrderOrUndefined;
+}
+
+export const getSearchAfterScroll = async ({
+ callCluster,
+ filter,
+ hopSize,
+ hops,
+ searchAfter,
+ sortField,
+ sortOrder,
+ index,
+}: GetSearchAfterOptions): Promise => {
+ const query = getQueryFilter({ filter });
+ let newSearchAfter = searchAfter;
+ for (let i = 0; i < hops; ++i) {
+ const response = await callCluster>('search', {
+ body: {
+ _source: getSourceWithTieBreaker({ sortField }),
+ query,
+ search_after: newSearchAfter,
+ sort: getSortWithTieBreaker({ sortField, sortOrder }),
+ },
+ ignoreUnavailable: true,
+ index,
+ size: hopSize,
+ });
+ if (response.hits.hits.length > 0) {
+ newSearchAfter = getSearchAfterWithTieBreaker({ response, sortField });
+ } else {
+ return {
+ searchAfter: undefined,
+ validSearchAfterFound: false,
+ };
+ }
+ }
+ return {
+ searchAfter: newSearchAfter,
+ validSearchAfterFound: true,
+ };
+};
diff --git a/x-pack/plugins/lists/server/services/utils/get_search_after_with_tie_breaker.ts b/x-pack/plugins/lists/server/services/utils/get_search_after_with_tie_breaker.ts
new file mode 100644
index 0000000000000..b5d44fbc9fd84
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/utils/get_search_after_with_tie_breaker.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SearchResponse } from 'elasticsearch';
+
+import { SortFieldOrUndefined } from '../../../common/schemas';
+
+export type TieBreaker = T & {
+ tie_breaker_id: string;
+};
+
+interface GetSearchAfterWithTieBreakerOptions {
+ response: SearchResponse>;
+ sortField: SortFieldOrUndefined;
+}
+
+export const getSearchAfterWithTieBreaker = ({
+ response,
+ sortField,
+}: GetSearchAfterWithTieBreakerOptions): string[] | undefined => {
+ if (response.hits.hits.length === 0) {
+ return undefined;
+ } else {
+ const lastEsElement = response.hits.hits[response.hits.hits.length - 1];
+ if (sortField == null) {
+ return [lastEsElement._source.tie_breaker_id];
+ } else {
+ const [[, sortValue]] = Object.entries(lastEsElement._source).filter(
+ ([key]) => key === sortField
+ );
+ if (typeof sortValue === 'string') {
+ return [sortValue, lastEsElement._source.tie_breaker_id];
+ } else {
+ return [lastEsElement._source.tie_breaker_id];
+ }
+ }
+ }
+};
diff --git a/x-pack/plugins/lists/server/services/utils/get_sort_with_tie_breaker.ts b/x-pack/plugins/lists/server/services/utils/get_sort_with_tie_breaker.ts
new file mode 100644
index 0000000000000..fee65cce580a0
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/utils/get_sort_with_tie_breaker.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SortFieldOrUndefined, SortOrderOrUndefined } from '../../../common/schemas';
+
+export interface SortWithTieBreakerReturn {
+ tie_breaker_id: 'asc';
+ [key: string]: string;
+}
+
+export const getSortWithTieBreaker = ({
+ sortField,
+ sortOrder,
+}: {
+ sortField: SortFieldOrUndefined;
+ sortOrder: SortOrderOrUndefined;
+}): SortWithTieBreakerReturn[] | undefined => {
+ const ascOrDesc = sortOrder ?? 'asc';
+ if (sortField != null) {
+ return [{ [sortField]: ascOrDesc, tie_breaker_id: 'asc' }];
+ } else {
+ return [{ tie_breaker_id: 'asc' }];
+ }
+};
diff --git a/x-pack/plugins/lists/server/services/utils/get_source_with_tie_breaker.ts b/x-pack/plugins/lists/server/services/utils/get_source_with_tie_breaker.ts
new file mode 100644
index 0000000000000..76cdd22f710e1
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/utils/get_source_with_tie_breaker.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SortFieldOrUndefined } from '../../../common/schemas';
+
+export const getSourceWithTieBreaker = ({
+ sortField,
+}: {
+ sortField: SortFieldOrUndefined;
+}): string[] => {
+ return sortField != null ? ['tie_breaker_id', sortField] : ['tie_breaker_id'];
+};
diff --git a/x-pack/plugins/lists/server/services/utils/index.ts b/x-pack/plugins/lists/server/services/utils/index.ts
index e6365e689f761..28bb3cea29e61 100644
--- a/x-pack/plugins/lists/server/services/utils/index.ts
+++ b/x-pack/plugins/lists/server/services/utils/index.ts
@@ -5,6 +5,14 @@
*/
export * from './derive_type_from_es_type';
+export * from './encode_decode_cursor';
export * from './get_query_filter_from_type_value';
+export * from './get_query_filter';
+export * from './get_search_after_scroll';
+export * from './get_search_after_with_tie_breaker';
+export * from './get_sort_with_tie_breaker';
+export * from './get_source_with_tie_breaker';
+export * from './scroll_to_start_page';
export * from './transform_elastic_to_list_item';
+export * from './transform_elastic_to_list';
export * from './transform_list_item_to_elastic_query';
diff --git a/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts b/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts
new file mode 100644
index 0000000000000..16e07044dc0d4
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/utils/scroll_to_start_page.ts
@@ -0,0 +1,94 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { APICaller } from 'kibana/server';
+
+import { Filter, SortFieldOrUndefined, SortOrderOrUndefined } from '../../../common/schemas';
+
+import { calculateScrollMath } from './calculate_scroll_math';
+import { getSearchAfterScroll } from './get_search_after_scroll';
+
+interface ScrollToStartPageOptions {
+ callCluster: APICaller;
+ filter: Filter;
+ sortField: SortFieldOrUndefined;
+ sortOrder: SortOrderOrUndefined;
+ page: number;
+ perPage: number;
+ hopSize: number;
+ index: string;
+ currentIndexPosition: number;
+ searchAfter: string[] | undefined;
+}
+
+export const scrollToStartPage = async ({
+ callCluster,
+ filter,
+ hopSize,
+ currentIndexPosition,
+ searchAfter,
+ page,
+ perPage,
+ sortOrder,
+ sortField,
+ index,
+}: ScrollToStartPageOptions): Promise => {
+ const { hops, leftOverAfterHops } = calculateScrollMath({
+ currentIndexPosition,
+ hopSize,
+ page,
+ perPage,
+ });
+
+ if (hops === 0 && leftOverAfterHops === 0 && currentIndexPosition === 0) {
+ // We want to use a valid searchAfter of undefined to start at the start of our list
+ return {
+ searchAfter: undefined,
+ validSearchAfterFound: true,
+ };
+ } else if (hops === 0 && leftOverAfterHops === 0 && currentIndexPosition > 0) {
+ return {
+ searchAfter,
+ validSearchAfterFound: true,
+ };
+ } else if (hops > 0) {
+ const scroll = await getSearchAfterScroll({
+ callCluster,
+ filter,
+ hopSize,
+ hops,
+ index,
+ searchAfter,
+ sortField,
+ sortOrder,
+ });
+ if (scroll.validSearchAfterFound && leftOverAfterHops > 0) {
+ return getSearchAfterScroll({
+ callCluster,
+ filter,
+ hopSize: leftOverAfterHops,
+ hops: 1,
+ index,
+ searchAfter: scroll.searchAfter,
+ sortField,
+ sortOrder,
+ });
+ } else {
+ return scroll;
+ }
+ } else {
+ return getSearchAfterScroll({
+ callCluster,
+ filter,
+ hopSize: leftOverAfterHops,
+ hops: 1,
+ index,
+ searchAfter,
+ sortField,
+ sortOrder,
+ });
+ }
+};
diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts
new file mode 100644
index 0000000000000..bb1ae1d4b9ff3
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SearchResponse } from 'elasticsearch';
+
+import { ListArraySchema, SearchEsListSchema } from '../../../common/schemas';
+
+export interface TransformElasticToListOptions {
+ response: SearchResponse;
+}
+
+export const transformElasticToList = ({
+ response,
+}: TransformElasticToListOptions): ListArraySchema => {
+ return response.hits.hits.map((hit) => {
+ return {
+ id: hit._id,
+ ...hit._source,
+ };
+ });
+};
diff --git a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts
index 4bdafcabaad06..b412375874f68 100644
--- a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts
+++ b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts
@@ -132,6 +132,7 @@ export type SourceDescriptor =
export type LayerDescriptor = {
__dataRequests?: DataRequestDescriptor[];
__isInErrorState?: boolean;
+ __isPreviewLayer?: boolean;
__errorMessage?: string;
__trackedLayerDescriptor?: LayerDescriptor;
alpha?: number;
diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts
index cac79093ce437..51e251a5d8e20 100644
--- a/x-pack/plugins/maps/public/actions/layer_actions.ts
+++ b/x-pack/plugins/maps/public/actions/layer_actions.ts
@@ -9,10 +9,10 @@ import { Query } from 'src/plugins/data/public';
import { MapStoreState } from '../reducers/store';
import {
getLayerById,
+ getLayerList,
getLayerListRaw,
getSelectedLayerId,
getMapReady,
- getTransientLayerId,
} from '../selectors/map_selectors';
import { FLYOUT_STATE } from '../reducers/ui';
import { cancelRequest } from '../reducers/non_serializable_instances';
@@ -27,7 +27,6 @@ import {
SET_JOINS,
SET_LAYER_VISIBILITY,
SET_SELECTED_LAYER,
- SET_TRANSIENT_LAYER,
SET_WAITING_FOR_READY_HIDDEN_LAYERS,
TRACK_CURRENT_LAYER_STATE,
UPDATE_LAYER_ORDER,
@@ -139,6 +138,41 @@ export function addLayerWithoutDataSync(layerDescriptor: LayerDescriptor) {
};
}
+export function addPreviewLayers(layerDescriptors: LayerDescriptor[]) {
+ return (dispatch: Dispatch) => {
+ dispatch(removePreviewLayers());
+
+ layerDescriptors.forEach((layerDescriptor) => {
+ dispatch(addLayer({ ...layerDescriptor, __isPreviewLayer: true }));
+ });
+ };
+}
+
+export function removePreviewLayers() {
+ return (dispatch: Dispatch, getState: () => MapStoreState) => {
+ getLayerList(getState()).forEach((layer) => {
+ if (layer.isPreviewLayer()) {
+ dispatch(removeLayer(layer.getId()));
+ }
+ });
+ };
+}
+
+export function promotePreviewLayers() {
+ return (dispatch: Dispatch, getState: () => MapStoreState) => {
+ getLayerList(getState()).forEach((layer) => {
+ if (layer.isPreviewLayer()) {
+ dispatch({
+ type: UPDATE_LAYER_PROP,
+ id: layer.getId(),
+ propName: '__isPreviewLayer',
+ newValue: false,
+ });
+ }
+ });
+ };
+}
+
export function setLayerVisibility(layerId: string, makeVisible: boolean) {
return async (dispatch: Dispatch, getState: () => MapStoreState) => {
// if the current-state is invisible, we also want to sync data
@@ -193,31 +227,17 @@ export function setSelectedLayer(layerId: string | null) {
};
}
-export function removeTransientLayer() {
+export function setFirstPreviewLayerToSelectedLayer() {
return async (dispatch: Dispatch, getState: () => MapStoreState) => {
- const transientLayerId = getTransientLayerId(getState());
- if (transientLayerId) {
- await dispatch(removeLayerFromLayerList(transientLayerId));
- await dispatch(setTransientLayer(null));
+ const firstPreviewLayer = getLayerList(getState()).find((layer) => {
+ return layer.isPreviewLayer();
+ });
+ if (firstPreviewLayer) {
+ dispatch(setSelectedLayer(firstPreviewLayer.getId()));
}
};
}
-export function setTransientLayer(layerId: string | null) {
- return {
- type: SET_TRANSIENT_LAYER,
- transientLayerId: layerId,
- };
-}
-
-export function clearTransientLayerStateAndCloseFlyout() {
- return async (dispatch: Dispatch) => {
- await dispatch(updateFlyout(FLYOUT_STATE.NONE));
- await dispatch(setSelectedLayer(null));
- await dispatch(removeTransientLayer());
- };
-}
-
export function updateLayerOrder(newLayerOrder: number[]) {
return {
type: UPDATE_LAYER_ORDER,
diff --git a/x-pack/plugins/maps/public/actions/map_action_constants.ts b/x-pack/plugins/maps/public/actions/map_action_constants.ts
index 0a32dba119429..25a86e4c50d07 100644
--- a/x-pack/plugins/maps/public/actions/map_action_constants.ts
+++ b/x-pack/plugins/maps/public/actions/map_action_constants.ts
@@ -5,7 +5,6 @@
*/
export const SET_SELECTED_LAYER = 'SET_SELECTED_LAYER';
-export const SET_TRANSIENT_LAYER = 'SET_TRANSIENT_LAYER';
export const UPDATE_LAYER_ORDER = 'UPDATE_LAYER_ORDER';
export const ADD_LAYER = 'ADD_LAYER';
export const SET_LAYER_ERROR_STATUS = 'SET_LAYER_ERROR_STATUS';
diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx
index 263e9888cd059..5d54166e08fb7 100644
--- a/x-pack/plugins/maps/public/classes/layers/layer.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx
@@ -80,6 +80,7 @@ export interface ILayer {
getInFlightRequestTokens(): symbol[];
getPrevRequestToken(dataId: string): symbol | undefined;
destroy: () => void;
+ isPreviewLayer: () => boolean;
}
export type Footnote = {
icon: ReactElement;
@@ -179,6 +180,10 @@ export class AbstractLayer implements ILayer {
return this.getSource().isJoinable();
}
+ isPreviewLayer(): boolean {
+ return !!this._descriptor.__isPreviewLayer;
+ }
+
supportsElasticsearchFilters(): boolean {
return this.getSource().isESSource();
}
diff --git a/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts b/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts
index 7698fb7c0947e..2bdeb6446cf28 100644
--- a/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts
+++ b/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts
@@ -9,7 +9,7 @@ import { ReactElement } from 'react';
import { LayerDescriptor } from '../../../common/descriptor_types';
export type RenderWizardArguments = {
- previewLayer: (layerDescriptor: LayerDescriptor | null, isIndexingSource?: boolean) => void;
+ previewLayers: (layerDescriptors: LayerDescriptor[], isIndexingSource?: boolean) => void;
mapColors: string[];
// upload arguments
isIndexingTriggered: boolean;
diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/observability_layer_template.tsx b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/observability_layer_template.tsx
index bfd78d5490059..3f3c556dcae1e 100644
--- a/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/observability_layer_template.tsx
+++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/observability_layer_template.tsx
@@ -53,13 +53,12 @@ export class ObservabilityLayerTemplate extends Component {
function previewGeojsonFile(geojsonFile: unknown, name: string) {
if (!geojsonFile) {
- previewLayer(null);
+ previewLayers([]);
return;
}
const sourceDescriptor = GeojsonFileSource.createDescriptor(geojsonFile, name);
const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors);
// TODO figure out a better way to handle passing this information back to layer_addpanel
- previewLayer(layerDescriptor, true);
+ previewLayers([layerDescriptor], true);
}
function viewIndexedData(indexResponses: {
@@ -72,7 +72,7 @@ export const uploadLayerWizardConfig: LayerWizard = {
)
);
if (!indexPatternId || !geoField) {
- previewLayer(null);
+ previewLayers([]);
} else {
const esSearchSourceConfig = {
indexPatternId,
@@ -85,7 +85,7 @@ export const uploadLayerWizardConfig: LayerWizard = {
? SCALING_TYPES.CLUSTERS
: SCALING_TYPES.LIMIT,
};
- previewLayer(createDefaultLayerDescriptor(esSearchSourceConfig, mapColors));
+ previewLayers([createDefaultLayerDescriptor(esSearchSourceConfig, mapColors)]);
importSuccessHandler(indexResponses);
}
}
diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx
index 4f1edca75b308..7eec84ef5bb2e 100644
--- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx
@@ -22,11 +22,11 @@ export const emsBoundariesLayerWizardConfig: LayerWizard = {
defaultMessage: 'Administrative boundaries from Elastic Maps Service',
}),
icon: 'emsApp',
- renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => {
+ renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => {
const onSourceConfigChange = (sourceConfig: Partial) => {
const sourceDescriptor = EMSFileSource.createDescriptor(sourceConfig);
const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors);
- previewLayer(layerDescriptor);
+ previewLayers([layerDescriptor]);
};
return ;
},
diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx
index 7a25609c6a5d1..60e67b1ae7053 100644
--- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx
@@ -22,12 +22,12 @@ export const emsBaseMapLayerWizardConfig: LayerWizard = {
defaultMessage: 'Tile map service from Elastic Maps Service',
}),
icon: 'emsApp',
- renderWizard: ({ previewLayer }: RenderWizardArguments) => {
+ renderWizard: ({ previewLayers }: RenderWizardArguments) => {
const onSourceConfigChange = (sourceConfig: unknown) => {
const layerDescriptor = VectorTileLayer.createDescriptor({
sourceDescriptor: EMSTMSSource.createDescriptor(sourceConfig),
});
- previewLayer(layerDescriptor);
+ previewLayers([layerDescriptor]);
};
return ;
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx
index 4e75ae8823385..b9d5faa8e18f1 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx
@@ -27,7 +27,6 @@ import {
VECTOR_STYLES,
STYLE_TYPE,
} from '../../../../common/constants';
-// @ts-ignore
import { COLOR_GRADIENTS } from '../../styles/color_utils';
export const clustersLayerWizardConfig: LayerWizard = {
@@ -35,10 +34,10 @@ export const clustersLayerWizardConfig: LayerWizard = {
defaultMessage: 'Geospatial data grouped in grids with metrics for each gridded cell',
}),
icon: 'logoElasticsearch',
- renderWizard: ({ previewLayer }: RenderWizardArguments) => {
+ renderWizard: ({ previewLayers }: RenderWizardArguments) => {
const onSourceConfigChange = (sourceConfig: Partial) => {
if (!sourceConfig) {
- previewLayer(null);
+ previewLayers([]);
return;
}
@@ -94,7 +93,7 @@ export const clustersLayerWizardConfig: LayerWizard = {
},
}),
});
- previewLayer(layerDescriptor);
+ previewLayers([layerDescriptor]);
};
return (
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx
index d0e45cb05ca06..79252c7febf8c 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx
@@ -21,17 +21,17 @@ export const heatmapLayerWizardConfig: LayerWizard = {
defaultMessage: 'Geospatial data grouped in grids to show density',
}),
icon: 'logoElasticsearch',
- renderWizard: ({ previewLayer }: RenderWizardArguments) => {
+ renderWizard: ({ previewLayers }: RenderWizardArguments) => {
const onSourceConfigChange = (sourceConfig: Partial) => {
if (!sourceConfig) {
- previewLayer(null);
+ previewLayers([]);
return;
}
const layerDescriptor = HeatmapLayer.createDescriptor({
sourceDescriptor: ESGeoGridSource.createDescriptor(sourceConfig),
});
- previewLayer(layerDescriptor);
+ previewLayers([layerDescriptor]);
};
return (
diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx
index bda1a6650c48a..5169af9bdddf2 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx
@@ -17,7 +17,6 @@ import {
VECTOR_STYLES,
STYLE_TYPE,
} from '../../../../common/constants';
-// @ts-ignore
import { COLOR_GRADIENTS } from '../../styles/color_utils';
// @ts-ignore
import { CreateSourceEditor } from './create_source_editor';
@@ -29,10 +28,10 @@ export const point2PointLayerWizardConfig: LayerWizard = {
defaultMessage: 'Aggregated data paths between the source and destination',
}),
icon: 'logoElasticsearch',
- renderWizard: ({ previewLayer }: RenderWizardArguments) => {
+ renderWizard: ({ previewLayers }: RenderWizardArguments) => {
const onSourceConfigChange = (sourceConfig: unknown) => {
if (!sourceConfig) {
- previewLayer(null);
+ previewLayers([]);
return;
}
@@ -65,7 +64,7 @@ export const point2PointLayerWizardConfig: LayerWizard = {
},
}),
});
- previewLayer(layerDescriptor);
+ previewLayers([layerDescriptor]);
};
return ;
diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx
index 8898735427ccb..888de2e7297cb 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx
@@ -28,14 +28,14 @@ export const esDocumentsLayerWizardConfig: LayerWizard = {
defaultMessage: 'Vector data from a Kibana index pattern',
}),
icon: 'logoElasticsearch',
- renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => {
+ renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => {
const onSourceConfigChange = (sourceConfig: unknown) => {
if (!sourceConfig) {
- previewLayer(null);
+ previewLayers([]);
return;
}
- previewLayer(createDefaultLayerDescriptor(sourceConfig, mapColors));
+ previewLayers([createDefaultLayerDescriptor(sourceConfig, mapColors)]);
};
return ;
},
diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx
index 309cb3abd83b2..b778dc0076459 100644
--- a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx
@@ -24,11 +24,11 @@ export const kibanaRegionMapLayerWizardConfig: LayerWizard = {
defaultMessage: 'Vector data from hosted GeoJSON configured in kibana.yml',
}),
icon: 'logoKibana',
- renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => {
+ renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => {
const onSourceConfigChange = (sourceConfig: unknown) => {
const sourceDescriptor = KibanaRegionmapSource.createDescriptor(sourceConfig);
const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors);
- previewLayer(layerDescriptor);
+ previewLayers([layerDescriptor]);
};
return ;
diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx
index 46513985ed1ab..227c0182b98de 100644
--- a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx
@@ -24,12 +24,12 @@ export const kibanaBasemapLayerWizardConfig: LayerWizard = {
defaultMessage: 'Tile map service configured in kibana.yml',
}),
icon: 'logoKibana',
- renderWizard: ({ previewLayer }: RenderWizardArguments) => {
+ renderWizard: ({ previewLayers }: RenderWizardArguments) => {
const onSourceConfigChange = () => {
const layerDescriptor = TileLayer.createDescriptor({
sourceDescriptor: KibanaTilemapSource.createDescriptor(),
});
- previewLayer(layerDescriptor);
+ previewLayers([layerDescriptor]);
};
return ;
},
diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx
index 86f8108d5e23b..c29302a2058b2 100644
--- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx
@@ -19,11 +19,11 @@ export const mvtVectorSourceWizardConfig: LayerWizard = {
defaultMessage: 'Vector source wizard',
}),
icon: 'grid',
- renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => {
+ renderWizard: ({ previewLayers, mapColors }: RenderWizardArguments) => {
const onSourceConfigChange = (sourceConfig: MVTSingleLayerVectorSourceConfig) => {
const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor(sourceConfig);
const layerDescriptor = TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors);
- previewLayer(layerDescriptor);
+ previewLayers([layerDescriptor]);
};
return ;
diff --git a/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx
index 9261b8866d115..62eeef234f414 100644
--- a/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx
@@ -18,17 +18,17 @@ export const wmsLayerWizardConfig: LayerWizard = {
defaultMessage: 'Maps from OGC Standard WMS',
}),
icon: 'grid',
- renderWizard: ({ previewLayer }: RenderWizardArguments) => {
+ renderWizard: ({ previewLayers }: RenderWizardArguments) => {
const onSourceConfigChange = (sourceConfig: unknown) => {
if (!sourceConfig) {
- previewLayer(null);
+ previewLayers([]);
return;
}
const layerDescriptor = TileLayer.createDescriptor({
sourceDescriptor: WMSSource.createDescriptor(sourceConfig),
});
- previewLayer(layerDescriptor);
+ previewLayers([layerDescriptor]);
};
return ;
},
diff --git a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx
index 574aaa262569f..b99b17c1d22d4 100644
--- a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx
@@ -16,12 +16,12 @@ export const tmsLayerWizardConfig: LayerWizard = {
defaultMessage: 'Tile map service configured in interface',
}),
icon: 'grid',
- renderWizard: ({ previewLayer }: RenderWizardArguments) => {
+ renderWizard: ({ previewLayers }: RenderWizardArguments) => {
const onSourceConfigChange = (sourceConfig: XYZTMSSourceConfig) => {
const layerDescriptor = TileLayer.createDescriptor({
sourceDescriptor: XYZTMSSource.createDescriptor(sourceConfig),
});
- previewLayer(layerDescriptor);
+ previewLayers([layerDescriptor]);
};
return ;
},
diff --git a/x-pack/plugins/maps/public/classes/styles/color_utils.test.js b/x-pack/plugins/maps/public/classes/styles/color_utils.test.ts
similarity index 100%
rename from x-pack/plugins/maps/public/classes/styles/color_utils.test.js
rename to x-pack/plugins/maps/public/classes/styles/color_utils.test.ts
diff --git a/x-pack/plugins/maps/public/classes/styles/color_utils.js b/x-pack/plugins/maps/public/classes/styles/color_utils.tsx
similarity index 53%
rename from x-pack/plugins/maps/public/classes/styles/color_utils.js
rename to x-pack/plugins/maps/public/classes/styles/color_utils.tsx
index 9dc79c006dffe..116e03096b0f5 100644
--- a/x-pack/plugins/maps/public/classes/styles/color_utils.js
+++ b/x-pack/plugins/maps/public/classes/styles/color_utils.tsx
@@ -7,73 +7,85 @@
import React from 'react';
import tinycolor from 'tinycolor2';
import chroma from 'chroma-js';
+// @ts-ignore
import { euiPaletteColorBlind } from '@elastic/eui/lib/services';
import { ColorGradient } from './components/color_gradient';
-import { vislibColorMaps } from '../../../../../../src/plugins/charts/public';
+import { RawColorSchema, vislibColorMaps } from '../../../../../../src/plugins/charts/public';
export const GRADIENT_INTERVALS = 8;
-export const DEFAULT_FILL_COLORS = euiPaletteColorBlind();
-export const DEFAULT_LINE_COLORS = [
- ...DEFAULT_FILL_COLORS.map((color) => tinycolor(color).darken().toHexString()),
+export const DEFAULT_FILL_COLORS: string[] = euiPaletteColorBlind();
+export const DEFAULT_LINE_COLORS: string[] = [
+ ...DEFAULT_FILL_COLORS.map((color: string) => tinycolor(color).darken().toHexString()),
// Explicitly add black & white as border color options
'#000',
'#FFF',
];
-function getLegendColors(colorRamp, numLegendColors = 4) {
+function getRGBColors(colorRamp: Array<[number, number[]]>, numLegendColors: number = 4): string[] {
const colors = [];
- colors[0] = getColor(colorRamp, 0);
+ colors[0] = getRGBColor(colorRamp, 0);
for (let i = 1; i < numLegendColors - 1; i++) {
- colors[i] = getColor(colorRamp, Math.floor((colorRamp.length * i) / numLegendColors));
+ colors[i] = getRGBColor(colorRamp, Math.floor((colorRamp.length * i) / numLegendColors));
}
- colors[numLegendColors - 1] = getColor(colorRamp, colorRamp.length - 1);
+ colors[numLegendColors - 1] = getRGBColor(colorRamp, colorRamp.length - 1);
return colors;
}
-function getColor(colorRamp, i) {
- const color = colorRamp[i][1];
- const red = Math.floor(color[0] * 255);
- const green = Math.floor(color[1] * 255);
- const blue = Math.floor(color[2] * 255);
+function getRGBColor(colorRamp: Array<[number, number[]]>, i: number): string {
+ const rgbArray = colorRamp[i][1];
+ const red = Math.floor(rgbArray[0] * 255);
+ const green = Math.floor(rgbArray[1] * 255);
+ const blue = Math.floor(rgbArray[2] * 255);
return `rgb(${red},${green},${blue})`;
}
-function getColorRamp(colorRampName) {
- const colorRamp = vislibColorMaps[colorRampName];
- if (!colorRamp) {
+function getColorSchema(colorRampName: string): RawColorSchema {
+ const colorSchema = vislibColorMaps[colorRampName];
+ if (!colorSchema) {
throw new Error(
`${colorRampName} not found. Expected one of following values: ${Object.keys(
vislibColorMaps
)}`
);
}
- return colorRamp;
+ return colorSchema;
}
-export function getRGBColorRangeStrings(colorRampName, numberColors = GRADIENT_INTERVALS) {
- const colorRamp = getColorRamp(colorRampName);
- return getLegendColors(colorRamp.value, numberColors);
+export function getRGBColorRangeStrings(
+ colorRampName: string,
+ numberColors: number = GRADIENT_INTERVALS
+): string[] {
+ const colorSchema = getColorSchema(colorRampName);
+ return getRGBColors(colorSchema.value, numberColors);
}
-export function getHexColorRangeStrings(colorRampName, numberColors = GRADIENT_INTERVALS) {
+export function getHexColorRangeStrings(
+ colorRampName: string,
+ numberColors: number = GRADIENT_INTERVALS
+): string[] {
return getRGBColorRangeStrings(colorRampName, numberColors).map((rgbColor) =>
chroma(rgbColor).hex()
);
}
-export function getColorRampCenterColor(colorRampName) {
+export function getColorRampCenterColor(colorRampName: string): string | null {
if (!colorRampName) {
return null;
}
- const colorRamp = getColorRamp(colorRampName);
- const centerIndex = Math.floor(colorRamp.value.length / 2);
- return getColor(colorRamp.value, centerIndex);
+ const colorSchema = getColorSchema(colorRampName);
+ const centerIndex = Math.floor(colorSchema.value.length / 2);
+ return getRGBColor(colorSchema.value, centerIndex);
}
// Returns an array of color stops
// [ stop_input_1: number, stop_output_1: color, stop_input_n: number, stop_output_n: color ]
-export function getOrdinalMbColorRampStops(colorRampName, min, max, numberColors) {
+export function getOrdinalMbColorRampStops(
+ colorRampName: string,
+ min: number,
+ max: number,
+ numberColors: number
+): Array | null {
if (!colorRampName) {
return null;
}
@@ -84,15 +96,18 @@ export function getOrdinalMbColorRampStops(colorRampName, min, max, numberColors
const hexColors = getHexColorRangeStrings(colorRampName, numberColors);
if (max === min) {
- //just return single stop value
+ // just return single stop value
return [max, hexColors[hexColors.length - 1]];
}
const delta = max - min;
- return hexColors.reduce((accu, stopColor, idx, srcArr) => {
- const stopNumber = min + (delta * idx) / srcArr.length;
- return [...accu, stopNumber, stopColor];
- }, []);
+ return hexColors.reduce(
+ (accu: Array, stopColor: string, idx: number, srcArr: string[]) => {
+ const stopNumber = min + (delta * idx) / srcArr.length;
+ return [...accu, stopNumber, stopColor];
+ },
+ []
+ );
}
export const COLOR_GRADIENTS = Object.keys(vislibColorMaps).map((colorRampName) => ({
@@ -102,7 +117,7 @@ export const COLOR_GRADIENTS = Object.keys(vislibColorMaps).map((colorRampName)
export const COLOR_RAMP_NAMES = Object.keys(vislibColorMaps);
-export function getLinearGradient(colorStrings) {
+export function getLinearGradient(colorStrings: string[]): string {
const intervals = colorStrings.length;
let linearGradient = `linear-gradient(to right, ${colorStrings[0]} 0%,`;
for (let i = 1; i < intervals - 1; i++) {
@@ -112,7 +127,12 @@ export function getLinearGradient(colorStrings) {
return `${linearGradient} ${colorStrings[colorStrings.length - 1]} 100%)`;
}
-const COLOR_PALETTES_CONFIGS = [
+export interface ColorPalette {
+ id: string;
+ colors: string[];
+}
+
+const COLOR_PALETTES_CONFIGS: ColorPalette[] = [
{
id: 'palette_0',
colors: euiPaletteColorBlind(),
@@ -127,14 +147,14 @@ const COLOR_PALETTES_CONFIGS = [
},
];
-export function getColorPalette(paletteId) {
- const palette = COLOR_PALETTES_CONFIGS.find((palette) => palette.id === paletteId);
+export function getColorPalette(paletteId: string): string[] | null {
+ const palette = COLOR_PALETTES_CONFIGS.find(({ id }: ColorPalette) => id === paletteId);
return palette ? palette.colors : null;
}
export const COLOR_PALETTES = COLOR_PALETTES_CONFIGS.map((palette) => {
const paletteDisplay = palette.colors.map((color) => {
- const style = {
+ const style: React.CSSProperties = {
backgroundColor: color,
width: `${100 / palette.colors.length}%`,
position: 'relative',
diff --git a/x-pack/plugins/maps/public/classes/styles/components/color_gradient.js b/x-pack/plugins/maps/public/classes/styles/components/color_gradient.tsx
similarity index 72%
rename from x-pack/plugins/maps/public/classes/styles/components/color_gradient.js
rename to x-pack/plugins/maps/public/classes/styles/components/color_gradient.tsx
index bf7e88df3a694..b29146062e46d 100644
--- a/x-pack/plugins/maps/public/classes/styles/components/color_gradient.js
+++ b/x-pack/plugins/maps/public/classes/styles/components/color_gradient.tsx
@@ -11,17 +11,20 @@ import {
getRGBColorRangeStrings,
getLinearGradient,
} from '../color_utils';
-import classNames from 'classnames';
-export const ColorGradient = ({ colorRamp, colorRampName, className }) => {
+interface Props {
+ colorRamp?: string[];
+ colorRampName?: string;
+}
+
+export const ColorGradient = ({ colorRamp, colorRampName }: Props) => {
if (!colorRamp && (!colorRampName || !COLOR_RAMP_NAMES.includes(colorRampName))) {
return null;
}
- const classes = classNames('mapColorGradient', className);
const rgbColorStrings = colorRampName
? getRGBColorRangeStrings(colorRampName, GRADIENT_INTERVALS)
- : colorRamp;
+ : colorRamp!;
const background = getLinearGradient(rgbColorStrings);
- return ;
+ return ;
};
diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap b/x-pack/plugins/maps/public/classes/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.tsx.snap
similarity index 100%
rename from x-pack/plugins/maps/public/classes/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap
rename to x-pack/plugins/maps/public/classes/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.tsx.snap
diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_constants.js b/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_constants.ts
similarity index 100%
rename from x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_constants.js
rename to x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_constants.ts
diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.js b/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.tsx
similarity index 100%
rename from x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.js
rename to x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.tsx
diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.js b/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.tsx
similarity index 85%
rename from x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.js
rename to x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.tsx
index 6d38a7985269e..d15fdbd79de75 100644
--- a/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.js
+++ b/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.tsx
@@ -15,8 +15,13 @@ import {
HEATMAP_COLOR_RAMP_LABEL,
} from './heatmap_constants';
-export function HeatmapStyleEditor({ colorRampName, onHeatmapColorChange }) {
- const onColorRampChange = (selectedColorRampName) => {
+interface Props {
+ colorRampName: string;
+ onHeatmapColorChange: ({ colorRampName }: { colorRampName: string }) => void;
+}
+
+export function HeatmapStyleEditor({ colorRampName, onHeatmapColorChange }: Props) {
+ const onColorRampChange = (selectedColorRampName: string) => {
onHeatmapColorChange({
colorRampName: selectedColorRampName,
});
diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.js b/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.js
index 3b5bcf591c2a6..5f920d0ba52d3 100644
--- a/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.js
+++ b/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.js
@@ -91,6 +91,7 @@ export class HeatmapStyle extends AbstractStyle {
MAX_RANGE,
GRADIENT_INTERVALS
);
+ // TODO handle null
mbMap.setPaintProperty(layerId, 'heatmap-color', [
'interpolate',
['linear'],
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/extract_color_from_style_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/extract_color_from_style_property.ts
index 71f77bc313191..dadb3f201fa33 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/extract_color_from_style_property.ts
+++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/extract_color_from_style_property.ts
@@ -43,7 +43,7 @@ export function extractColorFromStyleProperty(
}
const palette = getColorPalette(dynamicOptions.colorCategory);
- return palette[0];
+ return palette ? palette[0] : defaultColor;
} else {
// return middle of gradient for dynamic style property
if (dynamicOptions.useCustomColorRamp) {
@@ -58,6 +58,7 @@ export function extractColorFromStyleProperty(
if (!dynamicOptions.color) {
return defaultColor;
}
- return getColorRampCenterColor(dynamicOptions.color);
+ const centerColor = getColorRampCenterColor(dynamicOptions.color);
+ return centerColor ? centerColor : defaultColor;
}
}
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.test.ts b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.test.ts
new file mode 100644
index 0000000000000..bc032639dd07d
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.test.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+jest.mock('../../../kibana_services', () => {
+ const mockUiSettings = {
+ get: () => {
+ return undefined;
+ },
+ };
+ return {
+ getUiSettings: () => {
+ return mockUiSettings;
+ },
+ };
+});
+
+import { VECTOR_STYLES } from '../../../../common/constants';
+import { getDefaultStaticProperties } from './vector_style_defaults';
+
+describe('getDefaultStaticProperties', () => {
+ test('Should use first color in DEFAULT_*_COLORS when no colors are used on the map', () => {
+ const styleProperties = getDefaultStaticProperties([]);
+ expect(styleProperties[VECTOR_STYLES.FILL_COLOR]!.options.color).toBe('#54B399');
+ expect(styleProperties[VECTOR_STYLES.LINE_COLOR]!.options.color).toBe('#41937c');
+ });
+
+ test('Should next color in DEFAULT_*_COLORS when colors are used on the map', () => {
+ const styleProperties = getDefaultStaticProperties(['#54B399']);
+ expect(styleProperties[VECTOR_STYLES.FILL_COLOR]!.options.color).toBe('#6092C0');
+ expect(styleProperties[VECTOR_STYLES.LINE_COLOR]!.options.color).toBe('#4379aa');
+ });
+});
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts
index 86602381cf615..a6878a0d760c7 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts
+++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts
@@ -16,7 +16,6 @@ import {
COLOR_PALETTES,
DEFAULT_FILL_COLORS,
DEFAULT_LINE_COLORS,
- // @ts-ignore
} from '../color_utils';
import { VectorStylePropertiesDescriptor } from '../../../../common/descriptor_types';
// @ts-ignore
@@ -58,9 +57,13 @@ export function getDefaultProperties(mapColors: string[] = []): VectorStylePrope
export function getDefaultStaticProperties(
mapColors: string[] = []
): VectorStylePropertiesDescriptor {
- // Colors must be state-aware to reduce unnecessary incrementation
- const lastColor = mapColors.pop();
- const nextColorIndex = (DEFAULT_FILL_COLORS.indexOf(lastColor) + 1) % DEFAULT_FILL_COLORS.length;
+ let nextColorIndex = 0;
+ if (mapColors.length) {
+ const lastColor = mapColors[mapColors.length - 1];
+ if (DEFAULT_FILL_COLORS.includes(lastColor)) {
+ nextColorIndex = (DEFAULT_FILL_COLORS.indexOf(lastColor) + 1) % DEFAULT_FILL_COLORS.length;
+ }
+ }
const nextFillColor = DEFAULT_FILL_COLORS[nextColorIndex];
const nextLineColor = DEFAULT_LINE_COLORS[nextColorIndex];
diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx
index 75fb7a5bc4acc..b287064938ce5 100644
--- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx
+++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx
@@ -24,7 +24,7 @@ export const FlyoutBody = (props: Props) => {
}
const renderWizardArgs = {
- previewLayer: props.previewLayer,
+ previewLayers: props.previewLayers,
mapColors: props.mapColors,
isIndexingTriggered: props.isIndexingTriggered,
onRemove: props.onRemove,
diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts
index 968429ce91226..470e83f2d8090 100644
--- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts
+++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts
@@ -7,22 +7,24 @@
import { AnyAction, Dispatch } from 'redux';
import { connect } from 'react-redux';
import { FlyoutFooter } from './view';
-import { getSelectedLayer } from '../../../selectors/map_selectors';
-import { clearTransientLayerStateAndCloseFlyout } from '../../../actions';
+import { hasPreviewLayers, isLoadingPreviewLayers } from '../../../selectors/map_selectors';
+import { removePreviewLayers, updateFlyout } from '../../../actions';
import { MapStoreState } from '../../../reducers/store';
+import { FLYOUT_STATE } from '../../../reducers/ui';
function mapStateToProps(state: MapStoreState) {
- const selectedLayer = getSelectedLayer(state);
- const hasLayerSelected = !!selectedLayer;
return {
- hasLayerSelected,
- isLoading: hasLayerSelected && selectedLayer!.isLayerLoading(),
+ hasPreviewLayers: hasPreviewLayers(state),
+ isLoading: isLoadingPreviewLayers(state),
};
}
function mapDispatchToProps(dispatch: Dispatch) {
return {
- closeFlyout: () => dispatch(clearTransientLayerStateAndCloseFlyout()),
+ closeFlyout: () => {
+ dispatch(updateFlyout(FLYOUT_STATE.NONE));
+ dispatch(removePreviewLayers());
+ },
};
}
diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx
index 6f4d25a9c6c3e..2e122324c50fb 100644
--- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx
+++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx
@@ -20,7 +20,7 @@ interface Props {
disableNextButton: boolean;
nextButtonText: string;
closeFlyout: () => void;
- hasLayerSelected: boolean;
+ hasPreviewLayers: boolean;
isLoading: boolean;
}
@@ -30,14 +30,14 @@ export const FlyoutFooter = ({
disableNextButton,
nextButtonText,
closeFlyout,
- hasLayerSelected,
+ hasPreviewLayers,
isLoading,
}: Props) => {
const nextButton = showNextButton ? (
) {
return {
- previewLayer: async (layerDescriptor: LayerDescriptor) => {
- await dispatch(setSelectedLayer(null));
- await dispatch(removeTransientLayer());
- dispatch(addLayer(layerDescriptor));
- dispatch(setSelectedLayer(layerDescriptor.id));
- dispatch(setTransientLayer(layerDescriptor.id));
+ addPreviewLayers: (layerDescriptors: LayerDescriptor[]) => {
+ dispatch(addPreviewLayers(layerDescriptors));
},
- removeTransientLayer: () => {
- dispatch(setSelectedLayer(null));
- dispatch(removeTransientLayer());
- },
- selectLayerAndAdd: () => {
- dispatch(setTransientLayer(null));
+ promotePreviewLayers: () => {
+ dispatch(setFirstPreviewLayerToSelectedLayer());
dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL));
+ dispatch(promotePreviewLayers());
},
setIndexingTriggered: () => dispatch(updateIndexingStage(INDEXING_STAGE.TRIGGERED)),
resetIndexing: () => dispatch(updateIndexingStage(null)),
diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx
index d382a4085fe19..c1b6dcc1e12a6 100644
--- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx
+++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx
@@ -17,17 +17,15 @@ interface Props {
isIndexingReady: boolean;
isIndexingSuccess: boolean;
isIndexingTriggered: boolean;
- previewLayer: (layerDescriptor: LayerDescriptor) => void;
- removeTransientLayer: () => void;
+ addPreviewLayers: (layerDescriptors: LayerDescriptor[]) => void;
+ promotePreviewLayers: () => void;
resetIndexing: () => void;
- selectLayerAndAdd: () => void;
setIndexingTriggered: () => void;
}
interface State {
importView: boolean;
isIndexingSource: boolean;
- layerDescriptor: LayerDescriptor | null;
layerImportAddReady: boolean;
layerWizard: LayerWizard | null;
}
@@ -37,7 +35,6 @@ export class AddLayerPanel extends Component {
state = {
layerWizard: null,
- layerDescriptor: null, // TODO get this from redux store instead of storing locally
isIndexingSource: false,
importView: false,
layerImportAddReady: false,
@@ -57,21 +54,13 @@ export class AddLayerPanel extends Component {
}
}
- _previewLayer = (layerDescriptor: LayerDescriptor | null, isIndexingSource?: boolean) => {
+ _previewLayers = (layerDescriptors: LayerDescriptor[], isIndexingSource?: boolean) => {
if (!this._isMounted) {
return;
}
- if (!layerDescriptor) {
- this.setState({
- layerDescriptor: null,
- isIndexingSource: false,
- });
- this.props.removeTransientLayer();
- return;
- }
- this.setState({ layerDescriptor, isIndexingSource: !!isIndexingSource });
- this.props.previewLayer(layerDescriptor);
+ this.setState({ isIndexingSource: layerDescriptors.length ? !!isIndexingSource : false });
+ this.props.addPreviewLayers(layerDescriptors);
};
_clearLayerData = ({ keepSourceType = false }: { keepSourceType: boolean }) => {
@@ -80,7 +69,6 @@ export class AddLayerPanel extends Component {
}
const newState: Partial = {
- layerDescriptor: null,
isIndexingSource: false,
};
if (!keepSourceType) {
@@ -90,7 +78,7 @@ export class AddLayerPanel extends Component {
// @ts-ignore
this.setState(newState);
- this.props.removeTransientLayer();
+ this.props.addPreviewLayers([]);
};
_onWizardSelect = (layerWizard: LayerWizard) => {
@@ -101,7 +89,7 @@ export class AddLayerPanel extends Component {
if (this.state.isIndexingSource && !this.props.isIndexingTriggered) {
this.props.setIndexingTriggered();
} else {
- this.props.selectLayerAndAdd();
+ this.props.promotePreviewLayers();
if (this.state.importView) {
this.setState({
layerImportAddReady: false,
@@ -126,7 +114,7 @@ export class AddLayerPanel extends Component {
});
const isNextBtnEnabled = this.state.importView
? this.props.isIndexingReady || this.props.isIndexingSuccess
- : !!this.state.layerDescriptor;
+ : true;
return (
@@ -141,7 +129,7 @@ export class AddLayerPanel extends Component {
onClear={() => this._clearLayerData({ keepSourceType: false })}
onRemove={() => this._clearLayerData({ keepSourceType: true })}
onWizardSelect={this._onWizardSelect}
- previewLayer={this._previewLayer}
+ previewLayers={this._previewLayers}
/>
{
- await dispatch(removeTransientLayer());
await dispatch(setSelectedLayer(layerId));
dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL));
},
diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js
index c0ce24fef9cd8..b17078ae37113 100644
--- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js
+++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js
@@ -239,7 +239,8 @@ export class TOCEntry extends React.Component {
'mapTocEntry-isDragging': this.props.isDragging,
'mapTocEntry-isDraggingOver': this.props.isDraggingOver,
'mapTocEntry-isSelected':
- this.props.selectedLayer && this.props.selectedLayer.getId() === this.props.layer.getId(),
+ this.props.layer.isPreviewLayer() ||
+ (this.props.selectedLayer && this.props.selectedLayer.getId() === this.props.layer.getId()),
});
return (
diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js
index 90d756484c47f..543be9395d0bc 100644
--- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js
+++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js
@@ -21,6 +21,9 @@ const mockLayer = {
getDisplayName: () => {
return 'layer 1';
},
+ isPreviewLayer: () => {
+ return false;
+ },
isVisible: () => {
return true;
},
diff --git a/x-pack/plugins/maps/public/reducers/map.d.ts b/x-pack/plugins/maps/public/reducers/map.d.ts
index 8fc655b2c837a..33794fcf8657d 100644
--- a/x-pack/plugins/maps/public/reducers/map.d.ts
+++ b/x-pack/plugins/maps/public/reducers/map.d.ts
@@ -66,7 +66,6 @@ export type MapState = {
openTooltips: TooltipState[];
mapState: MapContext;
selectedLayerId: string | null;
- __transientLayerId: string | null;
layerList: LayerDescriptor[];
waitingForMapReadyLayerList: LayerDescriptor[];
settings: MapSettings;
diff --git a/x-pack/plugins/maps/public/reducers/map.js b/x-pack/plugins/maps/public/reducers/map.js
index c5f3968b749f1..9a661fe4833a8 100644
--- a/x-pack/plugins/maps/public/reducers/map.js
+++ b/x-pack/plugins/maps/public/reducers/map.js
@@ -6,7 +6,6 @@
import {
SET_SELECTED_LAYER,
- SET_TRANSIENT_LAYER,
UPDATE_LAYER_ORDER,
LAYER_DATA_LOAD_STARTED,
LAYER_DATA_LOAD_ENDED,
@@ -126,7 +125,6 @@ export const DEFAULT_MAP_STATE = {
hideViewControl: false,
},
selectedLayerId: null,
- __transientLayerId: null,
layerList: [],
waitingForMapReadyLayerList: [],
settings: getDefaultMapSettings(),
@@ -285,9 +283,6 @@ export function map(state = DEFAULT_MAP_STATE, action) {
case SET_SELECTED_LAYER:
const selectedMatch = state.layerList.find((layer) => layer.id === action.selectedLayerId);
return { ...state, selectedLayerId: selectedMatch ? action.selectedLayerId : null };
- case SET_TRANSIENT_LAYER:
- const transientMatch = state.layerList.find((layer) => layer.id === action.transientLayerId);
- return { ...state, __transientLayerId: transientMatch ? action.transientLayerId : null };
case UPDATE_LAYER_ORDER:
return {
...state,
diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts
index 0789222b0bf38..fd887d360c2e0 100644
--- a/x-pack/plugins/maps/public/selectors/map_selectors.ts
+++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts
@@ -137,9 +137,6 @@ export const getSelectedLayerId = ({ map }: MapStoreState): string | null => {
return !map.selectedLayerId || !map.layerList ? null : map.selectedLayerId;
};
-export const getTransientLayerId = ({ map }: MapStoreState): string | null =>
- map.__transientLayerId;
-
export const getLayerListRaw = ({ map }: MapStoreState): LayerDescriptor[] =>
map.layerList ? map.layerList : [];
@@ -331,15 +328,28 @@ export const getSelectedLayer = createSelector(
}
);
-export const getMapColors = createSelector(
- getTransientLayerId,
- getLayerListRaw,
- (transientLayerId, layerList) =>
- layerList.reduce((accu: string[], layer: LayerDescriptor) => {
- if (layer.id === transientLayerId) {
- return accu;
- }
- const color: string | undefined = _.get(layer, 'style.properties.fillColor.options.color');
+export const hasPreviewLayers = createSelector(getLayerList, (layerList) => {
+ return layerList.some((layer) => {
+ return layer.isPreviewLayer();
+ });
+});
+
+export const isLoadingPreviewLayers = createSelector(getLayerList, (layerList) => {
+ return layerList.some((layer) => {
+ return layer.isPreviewLayer() && layer.isLayerLoading();
+ });
+});
+
+export const getMapColors = createSelector(getLayerListRaw, (layerList) =>
+ layerList
+ .filter((layerDescriptor) => {
+ return !layerDescriptor.__isPreviewLayer;
+ })
+ .reduce((accu: string[], layerDescriptor: LayerDescriptor) => {
+ const color: string | undefined = _.get(
+ layerDescriptor,
+ 'style.properties.fillColor.options.color'
+ );
if (color) accu.push(color);
return accu;
}, [])
@@ -373,24 +383,20 @@ export const getQueryableUniqueIndexPatternIds = createSelector(getLayerList, (l
return _.uniq(indexPatternIds);
});
-export const hasDirtyState = createSelector(
- getLayerListRaw,
- getTransientLayerId,
- (layerListRaw, transientLayerId) => {
- if (transientLayerId) {
+export const hasDirtyState = createSelector(getLayerListRaw, (layerListRaw) => {
+ return layerListRaw.some((layerDescriptor) => {
+ if (layerDescriptor.__isPreviewLayer) {
return true;
}
- return layerListRaw.some((layerDescriptor) => {
- const trackedState = layerDescriptor[TRACKED_LAYER_DESCRIPTOR];
- if (!trackedState) {
- return false;
- }
- const currentState = copyPersistentState(layerDescriptor);
- return !_.isEqual(currentState, trackedState);
- });
- }
-);
+ const trackedState = layerDescriptor[TRACKED_LAYER_DESCRIPTOR];
+ if (!trackedState) {
+ return false;
+ }
+ const currentState = copyPersistentState(layerDescriptor);
+ return !_.isEqual(currentState, trackedState);
+ });
+});
export const areLayersLoaded = createSelector(
getLayerList,
diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts
index 7c510b33d564c..0af8141a2a641 100644
--- a/x-pack/plugins/ml/server/routes/job_validation.ts
+++ b/x-pack/plugins/ml/server/routes/job_validation.ts
@@ -66,7 +66,7 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization,
let errorResp;
const resp = await estimateBucketSpanFactory(
context.ml!.mlClient.callAsCurrentUser,
- context.core.elasticsearch.legacy.client.callAsInternalUser,
+ context.ml!.mlClient.callAsInternalUser,
mlLicense.isSecurityEnabled() === false
)(request.body)
// this catch gets triggered when the estimation code runs without error
@@ -187,7 +187,7 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization,
context.ml!.mlClient.callAsCurrentUser,
request.body,
version,
- context.core.elasticsearch.legacy.client.callAsInternalUser,
+ context.ml!.mlClient.callAsInternalUser,
mlLicense.isSecurityEnabled() === false
);
diff --git a/x-pack/plugins/ml/server/shared_services/providers/system.ts b/x-pack/plugins/ml/server/shared_services/providers/system.ts
index 698ac8e6261e5..33a4d854dd3e9 100644
--- a/x-pack/plugins/ml/server/shared_services/providers/system.ts
+++ b/x-pack/plugins/ml/server/shared_services/providers/system.ts
@@ -23,7 +23,7 @@ export interface MlSystemProvider {
): {
mlCapabilities(): Promise;
mlInfo(): Promise;
- mlSearch(searchParams: SearchParams): Promise>;
+ mlAnomalySearch(searchParams: SearchParams): Promise>;
};
}
@@ -68,7 +68,7 @@ export function getMlSystemProvider(
cloudId,
};
},
- async mlSearch(searchParams: SearchParams): Promise> {
+ async mlAnomalySearch(searchParams: SearchParams): Promise> {
isFullLicense();
return callAsCurrentUser('search', {
...searchParams,
diff --git a/x-pack/plugins/security/common/licensing/license_features.ts b/x-pack/plugins/security/common/licensing/license_features.ts
index 571d2630b2b17..8576e4bbc3555 100644
--- a/x-pack/plugins/security/common/licensing/license_features.ts
+++ b/x-pack/plugins/security/common/licensing/license_features.ts
@@ -38,6 +38,11 @@ export interface SecurityLicenseFeatures {
*/
readonly allowAccessAgreement: boolean;
+ /**
+ * Indicates whether we allow logging of audit events.
+ */
+ readonly allowAuditLogging: boolean;
+
/**
* Indicates whether we allow users to define document level security in roles.
*/
diff --git a/x-pack/plugins/security/common/licensing/license_service.test.ts b/x-pack/plugins/security/common/licensing/license_service.test.ts
index 89901d663d82a..77e6460b7669a 100644
--- a/x-pack/plugins/security/common/licensing/license_service.test.ts
+++ b/x-pack/plugins/security/common/licensing/license_service.test.ts
@@ -24,6 +24,7 @@ describe('license features', function () {
layout: 'error-es-unavailable',
allowRbac: false,
allowSubFeaturePrivileges: false,
+ allowAuditLogging: false,
});
});
@@ -44,6 +45,7 @@ describe('license features', function () {
layout: 'error-xpack-unavailable',
allowRbac: false,
allowSubFeaturePrivileges: false,
+ allowAuditLogging: false,
});
});
@@ -63,6 +65,7 @@ describe('license features', function () {
Array [
Object {
"allowAccessAgreement": false,
+ "allowAuditLogging": false,
"allowLogin": false,
"allowRbac": false,
"allowRoleDocumentLevelSecurity": false,
@@ -82,6 +85,7 @@ describe('license features', function () {
Array [
Object {
"allowAccessAgreement": true,
+ "allowAuditLogging": true,
"allowLogin": true,
"allowRbac": true,
"allowRoleDocumentLevelSecurity": true,
@@ -118,6 +122,7 @@ describe('license features', function () {
allowRoleFieldLevelSecurity: false,
allowRbac: true,
allowSubFeaturePrivileges: false,
+ allowAuditLogging: false,
});
expect(getFeatureSpy).toHaveBeenCalledTimes(1);
expect(getFeatureSpy).toHaveBeenCalledWith('security');
@@ -141,6 +146,7 @@ describe('license features', function () {
allowRoleFieldLevelSecurity: false,
allowRbac: false,
allowSubFeaturePrivileges: false,
+ allowAuditLogging: false,
});
});
@@ -163,6 +169,7 @@ describe('license features', function () {
allowRoleFieldLevelSecurity: false,
allowRbac: true,
allowSubFeaturePrivileges: true,
+ allowAuditLogging: true,
});
});
@@ -185,6 +192,30 @@ describe('license features', function () {
allowRoleFieldLevelSecurity: true,
allowRbac: true,
allowSubFeaturePrivileges: true,
+ allowAuditLogging: true,
+ });
+ });
+
+ it('should allow all basic features + audit logging for standard license', () => {
+ const mockRawLicense = licensingMock.createLicense({
+ license: { mode: 'standard', type: 'standard' },
+ features: { security: { isEnabled: true, isAvailable: true } },
+ });
+
+ const serviceSetup = new SecurityLicenseService().setup({
+ license$: of(mockRawLicense),
+ });
+ expect(serviceSetup.license.getFeatures()).toEqual({
+ showLogin: true,
+ allowLogin: true,
+ showLinks: true,
+ showRoleMappingsManagement: false,
+ allowAccessAgreement: false,
+ allowRoleDocumentLevelSecurity: false,
+ allowRoleFieldLevelSecurity: false,
+ allowRbac: true,
+ allowSubFeaturePrivileges: false,
+ allowAuditLogging: true,
});
});
});
diff --git a/x-pack/plugins/security/common/licensing/license_service.ts b/x-pack/plugins/security/common/licensing/license_service.ts
index 53cae857e5d66..75c7670f28a67 100644
--- a/x-pack/plugins/security/common/licensing/license_service.ts
+++ b/x-pack/plugins/security/common/licensing/license_service.ts
@@ -72,6 +72,7 @@ export class SecurityLicenseService {
showLinks: false,
showRoleMappingsManagement: false,
allowAccessAgreement: false,
+ allowAuditLogging: false,
allowRoleDocumentLevelSecurity: false,
allowRoleFieldLevelSecurity: false,
allowRbac: false,
@@ -90,6 +91,7 @@ export class SecurityLicenseService {
showLinks: false,
showRoleMappingsManagement: false,
allowAccessAgreement: false,
+ allowAuditLogging: false,
allowRoleDocumentLevelSecurity: false,
allowRoleFieldLevelSecurity: false,
allowRbac: false,
@@ -97,6 +99,7 @@ export class SecurityLicenseService {
};
}
+ const isLicenseStandardOrBetter = rawLicense.hasAtLeast('standard');
const isLicenseGoldOrBetter = rawLicense.hasAtLeast('gold');
const isLicensePlatinumOrBetter = rawLicense.hasAtLeast('platinum');
return {
@@ -105,6 +108,7 @@ export class SecurityLicenseService {
showLinks: true,
showRoleMappingsManagement: isLicenseGoldOrBetter,
allowAccessAgreement: isLicenseGoldOrBetter,
+ allowAuditLogging: isLicenseStandardOrBetter,
allowSubFeaturePrivileges: isLicenseGoldOrBetter,
// Only platinum and trial licenses are compliant with field- and document-level security.
allowRoleDocumentLevelSecurity: isLicensePlatinumOrBetter,
diff --git a/x-pack/plugins/security/server/audit/audit_service.test.ts b/x-pack/plugins/security/server/audit/audit_service.test.ts
new file mode 100644
index 0000000000000..94a2ada8df1da
--- /dev/null
+++ b/x-pack/plugins/security/server/audit/audit_service.test.ts
@@ -0,0 +1,165 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { AuditService } from './audit_service';
+import { loggingServiceMock } from 'src/core/server/mocks';
+import { licenseMock } from '../../common/licensing/index.mock';
+import { ConfigSchema, ConfigType } from '../config';
+import { SecurityLicenseFeatures } from '../../common/licensing';
+import { BehaviorSubject } from 'rxjs';
+
+const createConfig = (settings: Partial) => {
+ return ConfigSchema.validate(settings);
+};
+
+const config = createConfig({
+ enabled: true,
+});
+
+describe('#setup', () => {
+ it('returns the expected contract', () => {
+ const logger = loggingServiceMock.createLogger();
+ const auditService = new AuditService(logger);
+ const license = licenseMock.create();
+ expect(auditService.setup({ license, config })).toMatchInlineSnapshot(`
+ Object {
+ "getLogger": [Function],
+ }
+ `);
+ });
+});
+
+test(`calls the underlying logger with the provided message and requisite tags`, () => {
+ const pluginId = 'foo';
+
+ const logger = loggingServiceMock.createLogger();
+ const license = licenseMock.create();
+ license.features$ = new BehaviorSubject({
+ allowAuditLogging: true,
+ } as SecurityLicenseFeatures).asObservable();
+
+ const auditService = new AuditService(logger).setup({ license, config });
+
+ const auditLogger = auditService.getLogger(pluginId);
+
+ const eventType = 'bar';
+ const message = 'this is my audit message';
+ auditLogger.log(eventType, message);
+
+ expect(logger.info).toHaveBeenCalledTimes(1);
+ expect(logger.info).toHaveBeenCalledWith(message, {
+ eventType,
+ tags: [pluginId, eventType],
+ });
+});
+
+test(`calls the underlying logger with the provided metadata`, () => {
+ const pluginId = 'foo';
+
+ const logger = loggingServiceMock.createLogger();
+ const license = licenseMock.create();
+ license.features$ = new BehaviorSubject({
+ allowAuditLogging: true,
+ } as SecurityLicenseFeatures).asObservable();
+
+ const auditService = new AuditService(logger).setup({ license, config });
+
+ const auditLogger = auditService.getLogger(pluginId);
+
+ const eventType = 'bar';
+ const message = 'this is my audit message';
+ const metadata = Object.freeze({
+ property1: 'value1',
+ property2: false,
+ property3: 123,
+ });
+ auditLogger.log(eventType, message, metadata);
+
+ expect(logger.info).toHaveBeenCalledTimes(1);
+ expect(logger.info).toHaveBeenCalledWith(message, {
+ eventType,
+ tags: [pluginId, eventType],
+ property1: 'value1',
+ property2: false,
+ property3: 123,
+ });
+});
+
+test(`does not call the underlying logger if license does not support audit logging`, () => {
+ const pluginId = 'foo';
+
+ const logger = loggingServiceMock.createLogger();
+ const license = licenseMock.create();
+ license.features$ = new BehaviorSubject({
+ allowAuditLogging: false,
+ } as SecurityLicenseFeatures).asObservable();
+
+ const auditService = new AuditService(logger).setup({ license, config });
+
+ const auditLogger = auditService.getLogger(pluginId);
+
+ const eventType = 'bar';
+ const message = 'this is my audit message';
+ auditLogger.log(eventType, message);
+
+ expect(logger.info).not.toHaveBeenCalled();
+});
+
+test(`does not call the underlying logger if security audit logging is not enabled`, () => {
+ const pluginId = 'foo';
+
+ const logger = loggingServiceMock.createLogger();
+ const license = licenseMock.create();
+ license.features$ = new BehaviorSubject({
+ allowAuditLogging: true,
+ } as SecurityLicenseFeatures).asObservable();
+
+ const auditService = new AuditService(logger).setup({
+ license,
+ config: createConfig({
+ enabled: false,
+ }),
+ });
+
+ const auditLogger = auditService.getLogger(pluginId);
+
+ const eventType = 'bar';
+ const message = 'this is my audit message';
+ auditLogger.log(eventType, message);
+
+ expect(logger.info).not.toHaveBeenCalled();
+});
+
+test(`calls the underlying logger after license upgrade`, () => {
+ const pluginId = 'foo';
+
+ const logger = loggingServiceMock.createLogger();
+ const license = licenseMock.create();
+
+ const features$ = new BehaviorSubject({
+ allowAuditLogging: false,
+ } as SecurityLicenseFeatures);
+
+ license.features$ = features$.asObservable();
+
+ const auditService = new AuditService(logger).setup({ license, config });
+
+ const auditLogger = auditService.getLogger(pluginId);
+
+ const eventType = 'bar';
+ const message = 'this is my audit message';
+ auditLogger.log(eventType, message);
+
+ expect(logger.info).not.toHaveBeenCalled();
+
+ // perform license upgrade
+ features$.next({
+ allowAuditLogging: true,
+ } as SecurityLicenseFeatures);
+
+ auditLogger.log(eventType, message);
+
+ expect(logger.info).toHaveBeenCalledTimes(1);
+});
diff --git a/x-pack/plugins/security/server/audit/audit_service.ts b/x-pack/plugins/security/server/audit/audit_service.ts
new file mode 100644
index 0000000000000..93e69fd2601e9
--- /dev/null
+++ b/x-pack/plugins/security/server/audit/audit_service.ts
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Subscription } from 'rxjs';
+import { Logger } from '../../../../../src/core/server';
+import { SecurityLicense } from '../../common/licensing';
+import { ConfigType } from '../config';
+
+export interface AuditLogger {
+ log: (eventType: string, message: string, data?: Record) => void;
+}
+
+export interface AuditServiceSetup {
+ getLogger: (id?: string) => AuditLogger;
+}
+
+interface AuditServiceSetupParams {
+ license: SecurityLicense;
+ config: ConfigType['audit'];
+}
+
+export class AuditService {
+ private licenseFeaturesSubscription?: Subscription;
+ private auditLoggingEnabled = false;
+
+ constructor(private readonly logger: Logger) {}
+
+ setup({ license, config }: AuditServiceSetupParams): AuditServiceSetup {
+ if (config.enabled) {
+ this.licenseFeaturesSubscription = license.features$.subscribe(({ allowAuditLogging }) => {
+ this.auditLoggingEnabled = allowAuditLogging;
+ });
+ }
+
+ return {
+ getLogger: (id?: string): AuditLogger => {
+ return {
+ log: (eventType: string, message: string, data?: Record) => {
+ if (!this.auditLoggingEnabled) {
+ return;
+ }
+
+ this.logger.info(message, {
+ tags: id ? [id, eventType] : [eventType],
+ eventType,
+ ...data,
+ });
+ },
+ };
+ },
+ };
+ }
+
+ stop() {
+ if (this.licenseFeaturesSubscription) {
+ this.licenseFeaturesSubscription.unsubscribe();
+ this.licenseFeaturesSubscription = undefined;
+ }
+ }
+}
diff --git a/x-pack/plugins/security/server/audit/index.mock.ts b/x-pack/plugins/security/server/audit/index.mock.ts
index 888aa3361faf0..07341cc06e889 100644
--- a/x-pack/plugins/security/server/audit/index.mock.ts
+++ b/x-pack/plugins/security/server/audit/index.mock.ts
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SecurityAuditLogger } from './audit_logger';
+import { SecurityAuditLogger } from './security_audit_logger';
+import { AuditService } from './audit_service';
export const securityAuditLoggerMock = {
create() {
@@ -15,3 +16,11 @@ export const securityAuditLoggerMock = {
} as unknown) as jest.Mocked;
},
};
+
+export const auditServiceMock = {
+ create() {
+ return {
+ getLogger: jest.fn(),
+ } as jest.Mocked>;
+ },
+};
diff --git a/x-pack/plugins/security/server/audit/index.ts b/x-pack/plugins/security/server/audit/index.ts
index 3ab253151b805..3db160c703e34 100644
--- a/x-pack/plugins/security/server/audit/index.ts
+++ b/x-pack/plugins/security/server/audit/index.ts
@@ -4,4 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { SecurityAuditLogger } from './audit_logger';
+export { AuditService, AuditServiceSetup, AuditLogger } from './audit_service';
+export { SecurityAuditLogger } from './security_audit_logger';
diff --git a/x-pack/plugins/security/server/audit/audit_logger.test.ts b/x-pack/plugins/security/server/audit/security_audit_logger.test.ts
similarity index 91%
rename from x-pack/plugins/security/server/audit/audit_logger.test.ts
rename to x-pack/plugins/security/server/audit/security_audit_logger.test.ts
index 4dfd69a2ccb1f..c6883f681cf41 100644
--- a/x-pack/plugins/security/server/audit/audit_logger.test.ts
+++ b/x-pack/plugins/security/server/audit/security_audit_logger.test.ts
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { SecurityAuditLogger } from './audit_logger';
+import { SecurityAuditLogger } from './security_audit_logger';
const createMockAuditLogger = () => {
return {
@@ -14,7 +14,7 @@ const createMockAuditLogger = () => {
describe(`#savedObjectsAuthorizationFailure`, () => {
test('logs via auditLogger', () => {
const auditLogger = createMockAuditLogger();
- const securityAuditLogger = new SecurityAuditLogger(() => auditLogger);
+ const securityAuditLogger = new SecurityAuditLogger(auditLogger);
const username = 'foo-user';
const action = 'foo-action';
const types = ['foo-type-1', 'foo-type-2'];
@@ -64,7 +64,7 @@ describe(`#savedObjectsAuthorizationFailure`, () => {
describe(`#savedObjectsAuthorizationSuccess`, () => {
test('logs via auditLogger', () => {
const auditLogger = createMockAuditLogger();
- const securityAuditLogger = new SecurityAuditLogger(() => auditLogger);
+ const securityAuditLogger = new SecurityAuditLogger(auditLogger);
const username = 'foo-user';
const action = 'foo-action';
const types = ['foo-type-1', 'foo-type-2'];
@@ -96,7 +96,7 @@ describe(`#savedObjectsAuthorizationSuccess`, () => {
describe(`#accessAgreementAcknowledged`, () => {
test('logs via auditLogger', () => {
const auditLogger = createMockAuditLogger();
- const securityAuditLogger = new SecurityAuditLogger(() => auditLogger);
+ const securityAuditLogger = new SecurityAuditLogger(auditLogger);
const username = 'foo-user';
const provider = { type: 'saml', name: 'saml1' };
diff --git a/x-pack/plugins/security/server/audit/audit_logger.ts b/x-pack/plugins/security/server/audit/security_audit_logger.ts
similarity index 89%
rename from x-pack/plugins/security/server/audit/audit_logger.ts
rename to x-pack/plugins/security/server/audit/security_audit_logger.ts
index d7243ecbe13f8..87f7201f85665 100644
--- a/x-pack/plugins/security/server/audit/audit_logger.ts
+++ b/x-pack/plugins/security/server/audit/security_audit_logger.ts
@@ -5,10 +5,10 @@
*/
import { AuthenticationProvider } from '../../common/types';
-import { LegacyAPI } from '../plugin';
+import { AuditLogger } from './audit_service';
export class SecurityAuditLogger {
- constructor(private readonly getAuditLogger: () => LegacyAPI['auditLogger']) {}
+ constructor(private readonly logger: AuditLogger) {}
savedObjectsAuthorizationFailure(
username: string,
@@ -23,7 +23,7 @@ export class SecurityAuditLogger {
const missingString = missing
.map(({ spaceId, privilege }) => `${spaceId ? `(${spaceId})` : ''}${privilege}`)
.join(',');
- this.getAuditLogger().log(
+ this.logger.log(
'saved_objects_authorization_failure',
`${username} unauthorized to [${action}] [${typesString}]${spacesString}: missing [${missingString}]`,
{
@@ -46,7 +46,7 @@ export class SecurityAuditLogger {
) {
const typesString = types.join(',');
const spacesString = spaceIds.length ? ` in [${spaceIds.join(',')}]` : '';
- this.getAuditLogger().log(
+ this.logger.log(
'saved_objects_authorization_success',
`${username} authorized to [${action}] [${typesString}]${spacesString}`,
{
@@ -60,7 +60,7 @@ export class SecurityAuditLogger {
}
accessAgreementAcknowledged(username: string, provider: AuthenticationProvider) {
- this.getAuditLogger().log(
+ this.logger.log(
'access_agreement_acknowledged',
`${username} acknowledged access agreement (${provider.type}/${provider.name}).`,
{ username, provider }
diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts
index 0de86c72002c9..a0a06b537213d 100644
--- a/x-pack/plugins/security/server/index.ts
+++ b/x-pack/plugins/security/server/index.ts
@@ -27,6 +27,7 @@ export {
SAMLLogin,
OIDCLogin,
} from './authentication';
+export { AuditLogger } from './audit';
export { SecurityPluginSetup };
export { AuthenticatedUser } from '../common/model';
diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts
index a6407366bbd3b..72a946d6c5155 100644
--- a/x-pack/plugins/security/server/mocks.ts
+++ b/x-pack/plugins/security/server/mocks.ts
@@ -9,10 +9,12 @@ import { SecurityPluginSetup } from './plugin';
import { authenticationMock } from './authentication/index.mock';
import { authorizationMock } from './authorization/index.mock';
import { licenseMock } from '../common/licensing/index.mock';
+import { auditServiceMock } from './audit/index.mock';
function createSetupMock() {
const mockAuthz = authorizationMock.create();
return {
+ audit: auditServiceMock.create(),
authc: authenticationMock.create(),
authz: {
actions: mockAuthz.actions,
diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts
index d58c999ddccdf..fc49bdd9bc0c3 100644
--- a/x-pack/plugins/security/server/plugin.test.ts
+++ b/x-pack/plugins/security/server/plugin.test.ts
@@ -25,6 +25,7 @@ describe('Security Plugin', () => {
idleTimeout: 1500,
lifespan: null,
},
+ audit: { enabled: false },
authc: {
selector: { enabled: false },
providers: ['saml', 'token'],
@@ -50,9 +51,11 @@ describe('Security Plugin', () => {
await expect(plugin.setup(mockCoreSetup, mockDependencies)).resolves.toMatchInlineSnapshot(`
Object {
"__legacyCompat": Object {
- "registerLegacyAPI": [Function],
"registerPrivilegesWithCluster": [Function],
},
+ "audit": Object {
+ "getLogger": [Function],
+ },
"authc": Object {
"areAPIKeysEnabled": [Function],
"createAPIKey": [Function],
diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts
index 89cffde92d564..cdfc6f0ae542f 100644
--- a/x-pack/plugins/security/server/plugin.ts
+++ b/x-pack/plugins/security/server/plugin.ts
@@ -24,7 +24,7 @@ import { ConfigSchema, createConfig } from './config';
import { defineRoutes } from './routes';
import { SecurityLicenseService, SecurityLicense } from '../common/licensing';
import { setupSavedObjects } from './saved_objects';
-import { SecurityAuditLogger } from './audit';
+import { AuditService, SecurityAuditLogger, AuditServiceSetup } from './audit';
import { elasticsearchClientPlugin } from './elasticsearch_client_plugin';
export type SpacesService = Pick<
@@ -34,16 +34,6 @@ export type SpacesService = Pick<
export type FeaturesService = Pick;
-/**
- * Describes a set of APIs that is available in the legacy platform only and required by this plugin
- * to function properly.
- */
-export interface LegacyAPI {
- auditLogger: {
- log: (eventType: string, message: string, data?: Record) => void;
- };
-}
-
/**
* Describes public Security plugin contract returned at the `setup` stage.
*/
@@ -60,6 +50,7 @@ export interface SecurityPluginSetup {
>;
authz: Pick;
license: SecurityLicense;
+ audit: Pick;
/**
* If Spaces plugin is available it's supposed to register its SpacesService with Security plugin
@@ -72,7 +63,6 @@ export interface SecurityPluginSetup {
registerSpacesService: (service: SpacesService) => void;
__legacyCompat: {
- registerLegacyAPI: (legacyAPI: LegacyAPI) => void;
registerPrivilegesWithCluster: () => void;
};
}
@@ -90,14 +80,7 @@ export class Plugin {
private clusterClient?: ICustomClusterClient;
private spacesService?: SpacesService | symbol = Symbol('not accessed');
private securityLicenseService?: SecurityLicenseService;
-
- private legacyAPI?: LegacyAPI;
- private readonly getLegacyAPI = () => {
- if (!this.legacyAPI) {
- throw new Error('Legacy API is not registered!');
- }
- return this.legacyAPI;
- };
+ private readonly auditService = new AuditService(this.initializerContext.logger.get('audit'));
private readonly getSpacesService = () => {
// Changing property value from Symbol to undefined denotes the fact that property was accessed.
@@ -135,7 +118,9 @@ export class Plugin {
license$: licensing.license$,
});
- const auditLogger = new SecurityAuditLogger(() => this.getLegacyAPI().auditLogger);
+ const audit = this.auditService.setup({ license, config: config.audit });
+ const auditLogger = new SecurityAuditLogger(audit.getLogger());
+
const authc = await setupAuthentication({
auditLogger,
http: core.http,
@@ -178,6 +163,10 @@ export class Plugin {
});
return deepFreeze({
+ audit: {
+ getLogger: audit.getLogger,
+ },
+
authc: {
isAuthenticated: authc.isAuthenticated,
getCurrentUser: authc.getCurrentUser,
@@ -205,8 +194,6 @@ export class Plugin {
},
__legacyCompat: {
- registerLegacyAPI: (legacyAPI: LegacyAPI) => (this.legacyAPI = legacyAPI),
-
registerPrivilegesWithCluster: async () => await authz.registerPrivilegesWithCluster(),
},
});
@@ -228,6 +215,8 @@ export class Plugin {
this.securityLicenseService.stop();
this.securityLicenseService = undefined;
}
+
+ this.auditService.stop();
}
private wasSpacesServiceAccessed() {
diff --git a/x-pack/plugins/security/server/routes/views/login.test.ts b/x-pack/plugins/security/server/routes/views/login.test.ts
index 5c41a48bf5ee4..fee3adbb19f97 100644
--- a/x-pack/plugins/security/server/routes/views/login.test.ts
+++ b/x-pack/plugins/security/server/routes/views/login.test.ts
@@ -172,6 +172,7 @@ describe('Login view routes', () => {
showLinks: false,
showRoleMappingsManagement: true,
allowSubFeaturePrivileges: true,
+ allowAuditLogging: true,
showLogin: true,
});
diff --git a/x-pack/plugins/siem/kibana.json b/x-pack/plugins/siem/kibana.json
index 1106781fd45e4..6b43b41df8eee 100644
--- a/x-pack/plugins/siem/kibana.json
+++ b/x-pack/plugins/siem/kibana.json
@@ -24,7 +24,8 @@
"newsfeed",
"security",
"spaces",
- "usageCollection"
+ "usageCollection",
+ "lists"
],
"server": true,
"ui": true
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/README.md b/x-pack/plugins/siem/server/lib/detection_engine/README.md
index 610e82fd5f6ee..695165e1990a9 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/README.md
+++ b/x-pack/plugins/siem/server/lib/detection_engine/README.md
@@ -165,3 +165,12 @@ go about doing so.
`./signals/set_status_with_id.sh open` will update the status of the sample signal to open
`./signals/set_status_with_query.sh closed` will update the status of the signals in the result of the query to closed.
`./signals/set_status_with_query.sh open` will update the status of the signals in the result of the query to open.
+
+### Large List Exceptions
+
+To test out the functionality of large lists with rules, the user will need to import a list and post a rule with a reference to that exception list. The following outlines an example using the sample json rule provided in the repo.
+
+* First, set the appropriate env var in order to enable exceptions features`export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true` and `export ELASTIC_XPACK_SIEM_EXCEPTIONS_LISTS=true` and start kibana
+* Second, import a list of ips from a file called `ci-badguys.txt`. The command should look like this:
+`cd $HOME/kibana/x-pack/plugins/lists/server/scripts && ./import_list_items_by_filename.sh ip ~/ci-badguys.txt`
+* Then, from the detection engine scripts folder (`cd kibana/x-pack/plugins/siem/server/lib/detection_engine/scripts`) run `./post_rule.sh rules/queries/lists/query_with_list_plugin.json`
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list_plugin.json b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list_plugin.json
new file mode 100644
index 0000000000000..fa6fe6ac71117
--- /dev/null
+++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list_plugin.json
@@ -0,0 +1,24 @@
+{
+ "name": "Query with a list",
+ "description": "Query with a list only generate signals if source.ip is not in list",
+ "rule_id": "query-with-list",
+ "risk_score": 2,
+ "severity": "high",
+ "type": "query",
+ "query": "host.name: *",
+ "interval": "30s",
+ "language": "kuery",
+ "exceptions_list": [
+ {
+ "field": "source.ip",
+ "values_operator": "excluded",
+ "values_type": "list",
+ "values": [
+ {
+ "id": "ci-badguys.txt",
+ "name": "ip"
+ }
+ ]
+ }
+ ]
+}
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts
index 251a1e6d118ff..2d75ba4f42d12 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts
@@ -101,7 +101,10 @@ export const sampleDocNoSortIdNoVersion = (someUuid: string = sampleIdGuid): Sig
},
});
-export const sampleDocWithSortId = (someUuid: string = sampleIdGuid): SignalSourceHit => ({
+export const sampleDocWithSortId = (
+ someUuid: string = sampleIdGuid,
+ ip?: string
+): SignalSourceHit => ({
_index: 'myFakeSignalIndex',
_type: 'doc',
_score: 100,
@@ -110,6 +113,9 @@ export const sampleDocWithSortId = (someUuid: string = sampleIdGuid): SignalSour
_source: {
someKey: 'someValue',
'@timestamp': '2020-04-20T21:27:45+0000',
+ source: {
+ ip: ip ?? '127.0.0.1',
+ },
},
sort: ['1234567891111'],
});
@@ -313,7 +319,8 @@ export const sampleDocSearchResultsNoSortIdNoHits = (
export const repeatedSearchResultsWithSortId = (
total: number,
pageSize: number,
- guids: string[]
+ guids: string[],
+ ips?: string[]
) => ({
took: 10,
timed_out: false,
@@ -327,7 +334,7 @@ export const repeatedSearchResultsWithSortId = (
total,
max_score: 100,
hits: Array.from({ length: pageSize }).map((x, index) => ({
- ...sampleDocWithSortId(guids[index]),
+ ...sampleDocWithSortId(guids[index], ips ? ips[index] : '127.0.0.1'),
})),
},
});
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts
index 9ac4d4087016a..5862e6c481431 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts
@@ -86,5 +86,5 @@ export const bulkCreateMlSignals = async (
const anomalyResults = params.someResult;
const ecsResults = transformAnomalyResultsToEcs(anomalyResults);
- return singleBulkCreate({ ...params, someResult: ecsResults });
+ return singleBulkCreate({ ...params, filteredEvents: ecsResults });
};
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.test.ts
new file mode 100644
index 0000000000000..86efdb6603493
--- /dev/null
+++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.test.ts
@@ -0,0 +1,242 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import uuid from 'uuid';
+import { filterEventsAgainstList } from './filter_events_with_list';
+import { mockLogger, repeatedSearchResultsWithSortId } from './__mocks__/es_results';
+
+import { ListClient } from '../../../../../lists/server';
+
+const someGuids = Array.from({ length: 13 }).map((x) => uuid.v4());
+
+describe('filterEventsAgainstList', () => {
+ it('should respond with eventSearchResult if exceptionList is empty', async () => {
+ const res = await filterEventsAgainstList({
+ logger: mockLogger,
+ listClient: ({
+ getListItemByValues: async () => [],
+ } as unknown) as ListClient,
+ exceptionsList: undefined,
+ eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [
+ '1.1.1.1',
+ '2.2.2.2',
+ '3.3.3.3',
+ '7.7.7.7',
+ ]),
+ });
+ expect(res.hits.hits.length).toEqual(4);
+ });
+
+ it('should throw an error if malformed exception list present', async () => {
+ let message = '';
+ try {
+ await filterEventsAgainstList({
+ logger: mockLogger,
+ listClient: ({
+ getListItemByValues: async () => [],
+ } as unknown) as ListClient,
+ exceptionsList: [
+ {
+ field: 'source.ip',
+ values_operator: 'excluded',
+ values_type: 'list',
+ values: undefined,
+ },
+ ],
+ eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [
+ '1.1.1.1',
+ '2.2.2.2',
+ '3.3.3.3',
+ '7.7.7.7',
+ ]),
+ });
+ } catch (exc) {
+ message = exc.message;
+ }
+ expect(message).toEqual(
+ 'Failed to query lists index. Reason: Malformed exception list provided'
+ );
+ });
+
+ it('should throw an error if unsupported exception type', async () => {
+ let message = '';
+ try {
+ await filterEventsAgainstList({
+ logger: mockLogger,
+ listClient: ({
+ getListItemByValues: async () => [],
+ } as unknown) as ListClient,
+ exceptionsList: [
+ {
+ field: 'source.ip',
+ values_operator: 'excluded',
+ values_type: 'list',
+ values: [
+ {
+ id: 'ci-badguys.txt',
+ name: 'unsupportedListPluginType',
+ },
+ ],
+ },
+ ],
+ eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [
+ '1.1.1.1',
+ '2.2.2.2',
+ '3.3.3.3',
+ '7.7.7.7',
+ ]),
+ });
+ } catch (exc) {
+ message = exc.message;
+ }
+ expect(message).toEqual(
+ 'Failed to query lists index. Reason: Unsupported list type used, please use one of ip,keyword'
+ );
+ });
+
+ describe('operator_type is includes', () => {
+ it('should respond with same list if no items match value list', async () => {
+ const res = await filterEventsAgainstList({
+ logger: mockLogger,
+ listClient: ({
+ getListItemByValues: async () => [],
+ } as unknown) as ListClient,
+ exceptionsList: [
+ {
+ field: 'source.ip',
+ values_operator: 'included',
+ values_type: 'list',
+ values: [
+ {
+ id: 'ci-badguys.txt',
+ name: 'ip',
+ },
+ ],
+ },
+ ],
+ eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3)),
+ });
+ expect(res.hits.hits.length).toEqual(4);
+ });
+ it('should respond with less items in the list if some values match', async () => {
+ let outerType = '';
+ let outerListId = '';
+ const res = await filterEventsAgainstList({
+ logger: mockLogger,
+ listClient: ({
+ getListItemByValues: async ({
+ value,
+ type,
+ listId,
+ }: {
+ type: string;
+ listId: string;
+ value: string[];
+ }) => {
+ outerType = type;
+ outerListId = listId;
+ return value.slice(0, 2).map((item) => ({
+ value: item,
+ }));
+ },
+ } as unknown) as ListClient,
+ exceptionsList: [
+ {
+ field: 'source.ip',
+ values_operator: 'included',
+ values_type: 'list',
+ values: [
+ {
+ id: 'ci-badguys.txt',
+ name: 'ip',
+ },
+ ],
+ },
+ ],
+ eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [
+ '1.1.1.1',
+ '2.2.2.2',
+ '3.3.3.3',
+ '7.7.7.7',
+ ]),
+ });
+ expect(outerType).toEqual('ip');
+ expect(outerListId).toEqual('ci-badguys.txt');
+ expect(res.hits.hits.length).toEqual(2);
+ });
+ });
+ describe('operator type is excluded', () => {
+ it('should respond with empty list if no items match value list', async () => {
+ const res = await filterEventsAgainstList({
+ logger: mockLogger,
+ listClient: ({
+ getListItemByValues: async () => [],
+ } as unknown) as ListClient,
+ exceptionsList: [
+ {
+ field: 'source.ip',
+ values_operator: 'excluded',
+ values_type: 'list',
+ values: [
+ {
+ id: 'ci-badguys.txt',
+ name: 'ip',
+ },
+ ],
+ },
+ ],
+ eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3)),
+ });
+ expect(res.hits.hits.length).toEqual(0);
+ });
+ it('should respond with less items in the list if some values match', async () => {
+ let outerType = '';
+ let outerListId = '';
+ const res = await filterEventsAgainstList({
+ logger: mockLogger,
+ listClient: ({
+ getListItemByValues: async ({
+ value,
+ type,
+ listId,
+ }: {
+ type: string;
+ listId: string;
+ value: string[];
+ }) => {
+ outerType = type;
+ outerListId = listId;
+ return value.slice(0, 2).map((item) => ({
+ value: item,
+ }));
+ },
+ } as unknown) as ListClient,
+ exceptionsList: [
+ {
+ field: 'source.ip',
+ values_operator: 'excluded',
+ values_type: 'list',
+ values: [
+ {
+ id: 'ci-badguys.txt',
+ name: 'ip',
+ },
+ ],
+ },
+ ],
+ eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [
+ '1.1.1.1',
+ '2.2.2.2',
+ '3.3.3.3',
+ '7.7.7.7',
+ ]),
+ });
+ expect(outerType).toEqual('ip');
+ expect(outerListId).toEqual('ci-badguys.txt');
+ expect(res.hits.hits.length).toEqual(2);
+ });
+ });
+});
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.ts
new file mode 100644
index 0000000000000..400bb5dda46e7
--- /dev/null
+++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/filter_events_with_list.ts
@@ -0,0 +1,111 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { get } from 'lodash/fp';
+import { Logger } from 'src/core/server';
+
+import { type } from '../../../../../lists/common/schemas/common';
+import { ListClient } from '../../../../../lists/server';
+import { SignalSearchResponse, SearchTypes } from './types';
+import { RuleAlertParams } from '../types';
+import { List } from '../routes/schemas/types/lists_default_array';
+
+interface FilterEventsAgainstList {
+ listClient: ListClient;
+ exceptionsList: RuleAlertParams['exceptions_list'];
+ logger: Logger;
+ eventSearchResult: SignalSearchResponse;
+}
+
+export const filterEventsAgainstList = async ({
+ listClient,
+ exceptionsList,
+ logger,
+ eventSearchResult,
+}: FilterEventsAgainstList): Promise => {
+ try {
+ if (exceptionsList == null || exceptionsList.length === 0) {
+ return eventSearchResult;
+ }
+
+ // narrow unioned type to be single
+ const isStringableType = (val: SearchTypes) =>
+ ['string', 'number', 'boolean'].includes(typeof val);
+ // grab the signals with values found in the given exception lists.
+ const filteredHitsPromises = exceptionsList
+ .filter((exceptionItem: List) => exceptionItem.values_type === 'list')
+ .map(async (exceptionItem: List) => {
+ if (exceptionItem.values == null || exceptionItem.values.length === 0) {
+ throw new Error('Malformed exception list provided');
+ }
+ if (!type.is(exceptionItem.values[0].name)) {
+ throw new Error(
+ `Unsupported list type used, please use one of ${Object.keys(type.keys).join()}`
+ );
+ }
+ if (!exceptionItem.values[0].id) {
+ throw new Error(`Missing list id for exception on field ${exceptionItem.field}`);
+ }
+ // acquire the list values we are checking for.
+ const valuesOfGivenType = eventSearchResult.hits.hits.reduce((acc, searchResultItem) => {
+ const valueField = get(exceptionItem.field, searchResultItem._source);
+ if (valueField != null && isStringableType(valueField)) {
+ acc.add(valueField.toString());
+ }
+ return acc;
+ }, new Set());
+
+ // matched will contain any list items that matched with the
+ // values passed in from the Set.
+ const matchedListItems = await listClient.getListItemByValues({
+ listId: exceptionItem.values[0].id,
+ type: exceptionItem.values[0].name,
+ value: [...valuesOfGivenType],
+ });
+
+ // create a set of list values that were a hit - easier to work with
+ const matchedListItemsSet = new Set(
+ matchedListItems.map((item) => item.value)
+ );
+
+ // do a single search after with these values.
+ // painless script to do nested query in elasticsearch
+ // filter out the search results that match with the values found in the list.
+ const operator = exceptionItem.values_operator;
+ const filteredEvents = eventSearchResult.hits.hits.filter((item) => {
+ const eventItem = get(exceptionItem.field, item._source);
+ if (operator === 'included') {
+ if (eventItem != null) {
+ return !matchedListItemsSet.has(eventItem);
+ }
+ } else if (operator === 'excluded') {
+ if (eventItem != null) {
+ return matchedListItemsSet.has(eventItem);
+ }
+ }
+ return false;
+ });
+ const diff = eventSearchResult.hits.hits.length - filteredEvents.length;
+ logger.debug(`Lists filtered out ${diff} events`);
+ return filteredEvents;
+ });
+
+ const filteredHits = await Promise.all(filteredHitsPromises);
+ const toReturn: SignalSearchResponse = {
+ took: eventSearchResult.took,
+ timed_out: eventSearchResult.timed_out,
+ _shards: eventSearchResult._shards,
+ hits: {
+ total: filteredHits.length,
+ max_score: eventSearchResult.hits.max_score,
+ hits: filteredHits.flat(),
+ },
+ };
+
+ return toReturn;
+ } catch (exc) {
+ throw new Error(`Failed to query lists index. Reason: ${exc.message}`);
+ }
+};
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts
index 342976f3fd0fc..e95b713105fc6 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts
@@ -27,14 +27,14 @@ export const findMlSignals = async ({
from: string;
to: string;
}) => {
- const { mlSearch } = ml.mlSystemProvider(callCluster, request);
+ const { mlAnomalySearch } = ml.mlSystemProvider(callCluster, request);
const params = {
jobIds: [jobId],
threshold: anomalyThreshold,
earliestMs: dateMath.parse(from)?.valueOf() ?? 0,
latestMs: dateMath.parse(to)?.valueOf() ?? 0,
};
- const relevantAnomalies = await getAnomalies(params, mlSearch);
+ const relevantAnomalies = await getAnomalies(params, mlAnomalySearch);
return relevantAnomalies;
};
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
index 208f0e680722d..7479ab54af6e6 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
@@ -10,58 +10,28 @@ import {
sampleRuleGuid,
mockLogger,
repeatedSearchResultsWithSortId,
- sampleBulkCreateDuplicateResult,
- sampleDocSearchResultsNoSortId,
- sampleDocSearchResultsNoSortIdNoHits,
} from './__mocks__/es_results';
import { searchAfterAndBulkCreate } from './search_after_bulk_create';
import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants';
import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks';
import uuid from 'uuid';
+import { ListClient } from '../../../../../lists/server';
+import { ListItemArraySchema } from '../../../../../lists/common/schemas';
describe('searchAfterAndBulkCreate', () => {
let mockService: AlertServicesMock;
let inputIndexPattern: string[] = [];
+ const someGuids = Array.from({ length: 13 }).map(() => uuid.v4());
beforeEach(() => {
jest.clearAllMocks();
inputIndexPattern = ['auditbeat-*'];
mockService = alertsMock.createAlertServices();
});
- test('if successful with empty search results', async () => {
- const sampleParams = sampleRuleAlertParams();
- const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({
- someResult: sampleEmptyDocSearchResults(),
- ruleParams: sampleParams,
- services: mockService,
- logger: mockLogger,
- id: sampleRuleGuid,
- inputIndexPattern,
- signalsIndex: DEFAULT_SIGNALS_INDEX,
- name: 'rule-name',
- actions: [],
- createdAt: '2020-01-28T15:58:34.810Z',
- updatedAt: '2020-01-28T15:59:14.004Z',
- createdBy: 'elastic',
- updatedBy: 'elastic',
- interval: '5m',
- enabled: true,
- pageSize: 1,
- filter: undefined,
- refresh: false,
- tags: ['some fake tag 1', 'some fake tag 2'],
- throttle: 'no_actions',
- });
- expect(mockService.callCluster).toHaveBeenCalledTimes(0);
- expect(success).toEqual(true);
- expect(createdSignalsCount).toEqual(0);
- expect(lastLookBackDate).toBeNull();
- });
-
- test('if successful iteration of while loop with maxDocs', async () => {
+ test('should return success with number of searches less than max signals', async () => {
const sampleParams = sampleRuleAlertParams(30);
- const someGuids = Array.from({ length: 13 }).map((x) => uuid.v4());
mockService.callCluster
+ .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3)))
.mockResolvedValueOnce({
took: 100,
errors: false,
@@ -76,7 +46,7 @@ describe('searchAfterAndBulkCreate', () => {
},
],
})
- .mockResolvedValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(0, 3)))
+ .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(3, 6)))
.mockResolvedValueOnce({
took: 100,
errors: false,
@@ -91,7 +61,22 @@ describe('searchAfterAndBulkCreate', () => {
},
],
})
- .mockResolvedValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(3, 6)))
+ .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(6, 9)))
+ .mockResolvedValueOnce({
+ took: 100,
+ errors: false,
+ items: [
+ {
+ fakeItemValue: 'fakeItemKey',
+ },
+ {
+ create: {
+ status: 201,
+ },
+ },
+ ],
+ })
+ .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(9, 12)))
.mockResolvedValueOnce({
took: 100,
errors: false,
@@ -107,8 +92,23 @@ describe('searchAfterAndBulkCreate', () => {
],
});
const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({
- someResult: repeatedSearchResultsWithSortId(3, 1, someGuids.slice(6, 9)),
ruleParams: sampleParams,
+ listClient: ({
+ getListItemByValues: async () => [],
+ } as unknown) as ListClient,
+ exceptionsList: [
+ {
+ field: 'source.ip',
+ values_operator: 'included',
+ values_type: 'list',
+ values: [
+ {
+ id: 'ci-badguys.txt',
+ name: 'ip',
+ },
+ ],
+ },
+ ],
services: mockService,
logger: mockLogger,
id: sampleRuleGuid,
@@ -128,63 +128,63 @@ describe('searchAfterAndBulkCreate', () => {
tags: ['some fake tag 1', 'some fake tag 2'],
throttle: 'no_actions',
});
- expect(mockService.callCluster).toHaveBeenCalledTimes(5);
expect(success).toEqual(true);
- expect(createdSignalsCount).toEqual(3);
+ expect(mockService.callCluster).toHaveBeenCalledTimes(8);
+ expect(createdSignalsCount).toEqual(4);
expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000'));
});
- test('if unsuccessful first bulk create', async () => {
- const someGuids = Array.from({ length: 4 }).map((x) => uuid.v4());
- const sampleParams = sampleRuleAlertParams(10);
- mockService.callCluster.mockResolvedValue(sampleBulkCreateDuplicateResult);
+ test('should return success when no search results are in the allowlist', async () => {
+ const sampleParams = sampleRuleAlertParams(30);
+ mockService.callCluster
+ .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3)))
+ .mockResolvedValueOnce({
+ took: 100,
+ errors: false,
+ items: [
+ {
+ fakeItemValue: 'fakeItemKey',
+ },
+ {
+ create: {
+ status: 201,
+ },
+ },
+ {
+ create: {
+ status: 201,
+ },
+ },
+ {
+ create: {
+ status: 201,
+ },
+ },
+ {
+ create: {
+ status: 201,
+ },
+ },
+ ],
+ });
const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({
- someResult: repeatedSearchResultsWithSortId(4, 1, someGuids),
ruleParams: sampleParams,
- services: mockService,
- logger: mockLogger,
- id: sampleRuleGuid,
- inputIndexPattern,
- signalsIndex: DEFAULT_SIGNALS_INDEX,
- name: 'rule-name',
- actions: [],
- createdAt: '2020-01-28T15:58:34.810Z',
- updatedAt: '2020-01-28T15:59:14.004Z',
- createdBy: 'elastic',
- updatedBy: 'elastic',
- interval: '5m',
- enabled: true,
- pageSize: 1,
- filter: undefined,
- refresh: false,
- tags: ['some fake tag 1', 'some fake tag 2'],
- throttle: 'no_actions',
- });
- expect(mockLogger.error).toHaveBeenCalled();
- expect(success).toEqual(false);
- expect(createdSignalsCount).toEqual(1);
- expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000'));
- });
-
- test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => {
- const sampleParams = sampleRuleAlertParams();
- mockService.callCluster.mockResolvedValueOnce({
- took: 100,
- errors: false,
- items: [
+ listClient: ({
+ getListItemByValues: async () => [],
+ } as unknown) as ListClient,
+ exceptionsList: [
{
- fakeItemValue: 'fakeItemKey',
- },
- {
- create: {
- status: 201,
- },
+ field: 'source.ip',
+ values_operator: 'included',
+ values_type: 'list',
+ values: [
+ {
+ id: 'ci-badguys.txt',
+ name: 'ip',
+ },
+ ],
},
],
- });
- const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({
- someResult: sampleDocSearchResultsNoSortId(),
- ruleParams: sampleParams,
services: mockService,
logger: mockLogger,
id: sampleRuleGuid,
@@ -204,31 +204,59 @@ describe('searchAfterAndBulkCreate', () => {
tags: ['some fake tag 1', 'some fake tag 2'],
throttle: 'no_actions',
});
- expect(mockLogger.error).toHaveBeenCalled();
- expect(success).toEqual(false);
- expect(createdSignalsCount).toEqual(1);
+ expect(success).toEqual(true);
+ expect(mockService.callCluster).toHaveBeenCalledTimes(2);
+ expect(createdSignalsCount).toEqual(4); // should not create any signals because all events were in the allowlist
expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000'));
});
- test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => {
- const sampleParams = sampleRuleAlertParams();
- mockService.callCluster.mockResolvedValueOnce({
- took: 100,
- errors: false,
- items: [
- {
- fakeItemValue: 'fakeItemKey',
- },
- {
- create: {
- status: 201,
+ test('should return success when no exceptions list provided', async () => {
+ const sampleParams = sampleRuleAlertParams(30);
+ mockService.callCluster
+ .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3)))
+ .mockResolvedValueOnce({
+ took: 100,
+ errors: false,
+ items: [
+ {
+ fakeItemValue: 'fakeItemKey',
},
- },
- ],
- });
+ {
+ create: {
+ status: 201,
+ },
+ },
+ {
+ create: {
+ status: 201,
+ },
+ },
+ {
+ create: {
+ status: 201,
+ },
+ },
+ {
+ create: {
+ status: 201,
+ },
+ },
+ ],
+ });
const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({
- someResult: sampleDocSearchResultsNoSortIdNoHits(),
ruleParams: sampleParams,
+ listClient: ({
+ getListItemByValues: async ({
+ value,
+ }: {
+ type: string;
+ listId: string;
+ value: string[];
+ }) => {
+ return value.map((item) => ({ value: item }));
+ },
+ } as unknown) as ListClient,
+ exceptionsList: undefined,
services: mockService,
logger: mockLogger,
id: sampleRuleGuid,
@@ -249,31 +277,35 @@ describe('searchAfterAndBulkCreate', () => {
throttle: 'no_actions',
});
expect(success).toEqual(true);
- expect(createdSignalsCount).toEqual(1);
+ expect(mockService.callCluster).toHaveBeenCalledTimes(2);
+ expect(createdSignalsCount).toEqual(4); // should not create any signals because all events were in the allowlist
expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000'));
});
- test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => {
+ test('if unsuccessful first bulk create', async () => {
const sampleParams = sampleRuleAlertParams(10);
- const someGuids = Array.from({ length: 4 }).map((x) => uuid.v4());
mockService.callCluster
- .mockResolvedValueOnce({
- took: 100,
- errors: false,
- items: [
- {
- fakeItemValue: 'fakeItemKey',
- },
- {
- create: {
- status: 201,
- },
- },
- ],
- })
- .mockResolvedValueOnce(sampleDocSearchResultsNoSortId());
+ .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3)))
+ .mockRejectedValue(new Error('bulk failed')); // Added this recently
const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({
- someResult: repeatedSearchResultsWithSortId(4, 1, someGuids),
+ listClient: ({
+ getListItemByValues: async () => {
+ return ([] as unknown) as ListItemArraySchema;
+ },
+ } as unknown) as ListClient,
+ exceptionsList: [
+ {
+ field: 'source.ip',
+ values_operator: 'included',
+ values_type: 'list',
+ values: [
+ {
+ id: 'ci-badguys.txt',
+ name: 'ip',
+ },
+ ],
+ },
+ ],
ruleParams: sampleParams,
services: mockService,
logger: mockLogger,
@@ -294,32 +326,40 @@ describe('searchAfterAndBulkCreate', () => {
tags: ['some fake tag 1', 'some fake tag 2'],
throttle: 'no_actions',
});
- expect(success).toEqual(true);
- expect(createdSignalsCount).toEqual(1);
+ expect(mockLogger.error).toHaveBeenCalled();
+ expect(success).toEqual(false);
+ expect(createdSignalsCount).toEqual(0);
expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000'));
});
- test('if successful iteration of while loop with maxDocs and search after returns empty results with no sort ids', async () => {
- const sampleParams = sampleRuleAlertParams(10);
- const someGuids = Array.from({ length: 4 }).map((x) => uuid.v4());
- mockService.callCluster
- .mockResolvedValueOnce({
- took: 100,
- errors: false,
- items: [
- {
- fakeItemValue: 'fakeItemKey',
- },
- {
- create: {
- status: 201,
- },
- },
- ],
- })
- .mockResolvedValueOnce(sampleEmptyDocSearchResults());
+ test('should return success with 0 total hits', async () => {
+ const sampleParams = sampleRuleAlertParams();
+ mockService.callCluster.mockResolvedValueOnce(sampleEmptyDocSearchResults());
const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({
- someResult: repeatedSearchResultsWithSortId(4, 1, someGuids),
+ listClient: ({
+ getListItemByValues: async ({
+ value,
+ }: {
+ type: string;
+ listId: string;
+ value: string[];
+ }) => {
+ return value.map((item) => ({ value: item }));
+ },
+ } as unknown) as ListClient,
+ exceptionsList: [
+ {
+ field: 'source.ip',
+ values_operator: 'included',
+ values_type: 'list',
+ values: [
+ {
+ id: 'ci-badguys.txt',
+ name: 'ip',
+ },
+ ],
+ },
+ ],
ruleParams: sampleParams,
services: mockService,
logger: mockLogger,
@@ -341,13 +381,12 @@ describe('searchAfterAndBulkCreate', () => {
throttle: 'no_actions',
});
expect(success).toEqual(true);
- expect(createdSignalsCount).toEqual(1);
- expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000'));
+ expect(createdSignalsCount).toEqual(0);
+ expect(lastLookBackDate).toEqual(null);
});
test('if returns false when singleSearchAfter throws an exception', async () => {
const sampleParams = sampleRuleAlertParams(10);
- const someGuids = Array.from({ length: 4 }).map((x) => uuid.v4());
mockService.callCluster
.mockResolvedValueOnce({
took: 100,
@@ -367,7 +406,30 @@ describe('searchAfterAndBulkCreate', () => {
throw Error('Fake Error');
});
const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({
- someResult: repeatedSearchResultsWithSortId(4, 1, someGuids),
+ listClient: ({
+ getListItemByValues: async ({
+ value,
+ }: {
+ type: string;
+ listId: string;
+ value: string[];
+ }) => {
+ return value.map((item) => ({ value: item }));
+ },
+ } as unknown) as ListClient,
+ exceptionsList: [
+ {
+ field: 'source.ip',
+ values_operator: 'included',
+ values_type: 'list',
+ values: [
+ {
+ id: 'ci-badguys.txt',
+ name: 'ip',
+ },
+ ],
+ },
+ ],
ruleParams: sampleParams,
services: mockService,
logger: mockLogger,
@@ -389,7 +451,7 @@ describe('searchAfterAndBulkCreate', () => {
throttle: 'no_actions',
});
expect(success).toEqual(false);
- expect(createdSignalsCount).toEqual(1);
- expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000'));
+ expect(createdSignalsCount).toEqual(0); // should not create signals if search threw error
+ expect(lastLookBackDate).toEqual(null);
});
});
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts
index acf3e9bfb055c..05cdccedbc2c1 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts
@@ -4,18 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { ListClient } from '../../../../../lists/server';
import { AlertServices } from '../../../../../alerting/server';
import { RuleAlertAction } from '../../../../common/detection_engine/types';
-import { RuleTypeParams, RefreshTypes } from '../types';
+import { RuleTypeParams, RefreshTypes, RuleAlertParams } from '../types';
import { Logger } from '../../../../../../../src/core/server';
import { singleSearchAfter } from './single_search_after';
import { singleBulkCreate } from './single_bulk_create';
import { SignalSearchResponse } from './types';
+import { filterEventsAgainstList } from './filter_events_with_list';
interface SearchAfterAndBulkCreateParams {
- someResult: SignalSearchResponse;
ruleParams: RuleTypeParams;
services: AlertServices;
+ listClient: ListClient | undefined; // TODO: undefined is for temporary development, remove before merged
+ exceptionsList: RuleAlertParams['exceptions_list'];
logger: Logger;
id: string;
inputIndexPattern: string[];
@@ -45,9 +48,10 @@ export interface SearchAfterAndBulkCreateReturnType {
// search_after through documents and re-index using bulk endpoint.
export const searchAfterAndBulkCreate = async ({
- someResult,
ruleParams,
+ exceptionsList,
services,
+ listClient,
logger,
id,
inputIndexPattern,
@@ -73,71 +77,31 @@ export const searchAfterAndBulkCreate = async ({
lastLookBackDate: null,
createdSignalsCount: 0,
};
- if (someResult.hits.hits.length === 0) {
- toReturn.success = true;
- return toReturn;
- }
- logger.debug('[+] starting bulk insertion');
- const { bulkCreateDuration, createdItemsCount } = await singleBulkCreate({
- someResult,
- ruleParams,
- services,
- logger,
- id,
- signalsIndex,
- actions,
- name,
- createdAt,
- createdBy,
- updatedAt,
- updatedBy,
- interval,
- enabled,
- refresh,
- tags,
- throttle,
- });
+ let sortId; // tells us where to start our next search_after query
+ let searchResultSize = 0;
- if (createdItemsCount > 0) {
- toReturn.createdSignalsCount = createdItemsCount;
- toReturn.lastLookBackDate =
- someResult.hits.hits.length > 0
- ? new Date(someResult.hits.hits[someResult.hits.hits.length - 1]?._source['@timestamp'])
- : null;
- }
+ /*
+ The purpose of `maxResults` is to ensure we do not perform
+ extra search_after's. This will be reset on each
+ iteration, although it really only matters for the first
+ iteration of the loop.
+ e.g. if maxSignals = 100 but our search result only yields
+ 27 documents, there is no point in performing another search
+ since we know there are no more events that match our rule,
+ and thus, no more signals we could possibly generate.
+ However, if maxSignals = 500 and our search yields a total
+ of 3050 results we don't want to make 3050 signals,
+ we only want 500. So maxResults will help us control how
+ many times we perform a search_after
+ */
+ let maxResults = ruleParams.maxSignals;
- if (bulkCreateDuration) {
- toReturn.bulkCreateTimes.push(bulkCreateDuration);
- }
- const totalHits =
- typeof someResult.hits.total === 'number' ? someResult.hits.total : someResult.hits.total.value;
- // maxTotalHitsSize represents the total number of docs to
- // query for, no matter the size of each individual page of search results.
- // If the total number of hits for the overall search result is greater than
- // maxSignals, default to requesting a total of maxSignals, otherwise use the
- // totalHits in the response from the searchAfter query.
- const maxTotalHitsSize = Math.min(totalHits, ruleParams.maxSignals);
+ // Get
- // number of docs in the current search result
- let hitsSize = someResult.hits.hits.length;
- logger.debug(`first size: ${hitsSize}`);
- let sortIds = someResult.hits.hits[0].sort;
- if (sortIds == null && totalHits > 0) {
- logger.error('sortIds was empty on first search but expected more');
- toReturn.success = false;
- return toReturn;
- } else if (sortIds == null && totalHits === 0) {
- toReturn.success = true;
- return toReturn;
- }
- let sortId;
- if (sortIds != null) {
- sortId = sortIds[0];
- }
- while (hitsSize < maxTotalHitsSize && hitsSize !== 0) {
+ while (searchResultSize < maxResults) {
try {
- logger.debug(`sortIds: ${sortIds}`);
+ logger.debug(`sortIds: ${sortId}`);
const {
searchResult,
searchDuration,
@@ -152,25 +116,60 @@ export const searchAfterAndBulkCreate = async ({
pageSize, // maximum number of docs to receive per search result.
});
toReturn.searchAfterTimes.push(searchDuration);
+ toReturn.lastLookBackDate =
+ searchResult.hits.hits.length > 0
+ ? new Date(
+ searchResult.hits.hits[searchResult.hits.hits.length - 1]?._source['@timestamp']
+ )
+ : null;
+ const totalHits =
+ typeof searchResult.hits.total === 'number'
+ ? searchResult.hits.total
+ : searchResult.hits.total.value;
+ logger.debug(`totalHits: ${totalHits}`);
+
+ // re-calculate maxResults to ensure if our search results
+ // are less than max signals, we are not attempting to
+ // create more signals than there are total search results.
+ maxResults = Math.min(totalHits, ruleParams.maxSignals);
+ searchResultSize += searchResult.hits.hits.length;
if (searchResult.hits.hits.length === 0) {
toReturn.success = true;
return toReturn;
}
- hitsSize += searchResult.hits.hits.length;
- logger.debug(`size adjusted: ${hitsSize}`);
- sortIds = searchResult.hits.hits[0].sort;
- if (sortIds == null) {
- logger.debug('sortIds was empty on search');
+
+ // filter out the search results that match with the values found in the list.
+ // the resulting set are valid signals that are not on the allowlist.
+ const filteredEvents =
+ listClient != null
+ ? await filterEventsAgainstList({
+ listClient,
+ exceptionsList,
+ logger,
+ eventSearchResult: searchResult,
+ })
+ : searchResult;
+
+ if (filteredEvents.hits.hits.length === 0) {
+ // everything in the events were allowed, so no need to generate signals
toReturn.success = true;
- return toReturn; // no more search results
+ return toReturn;
+ }
+
+ // cap max signals created to be no more than maxSignals
+ if (toReturn.createdSignalsCount + filteredEvents.hits.hits.length > ruleParams.maxSignals) {
+ const tempSignalsToIndex = filteredEvents.hits.hits.slice(
+ 0,
+ ruleParams.maxSignals - toReturn.createdSignalsCount
+ );
+ filteredEvents.hits.hits = tempSignalsToIndex;
}
- sortId = sortIds[0];
logger.debug('next bulk index');
const {
bulkCreateDuration: bulkDuration,
createdItemsCount: createdCount,
} = await singleBulkCreate({
- someResult: searchResult,
+ filteredEvents,
ruleParams,
services,
logger,
@@ -189,17 +188,25 @@ export const searchAfterAndBulkCreate = async ({
throttle,
});
logger.debug('finished next bulk index');
+ logger.debug(`created ${createdCount} signals`);
toReturn.createdSignalsCount += createdCount;
if (bulkDuration) {
toReturn.bulkCreateTimes.push(bulkDuration);
}
+
+ if (filteredEvents.hits.hits[0].sort == null) {
+ logger.debug('sortIds was empty on search');
+ toReturn.success = true;
+ return toReturn; // no more search results
+ }
+ sortId = filteredEvents.hits.hits[0].sort[0];
} catch (exc) {
logger.error(`[-] search_after and bulk threw an error ${exc}`);
toReturn.success = false;
return toReturn;
}
}
- logger.debug(`[+] completed bulk index of ${maxTotalHitsSize}`);
+ logger.debug(`[+] completed bulk index of ${toReturn.createdSignalsCount}`);
toReturn.success = true;
return toReturn;
};
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
index 0c7f0839f8daf..ea7255b8a925a 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
@@ -17,6 +17,7 @@ import { scheduleNotificationActions } from '../notifications/schedule_notificat
import { RuleAlertType } from '../rules/types';
import { findMlSignals } from './find_ml_signals';
import { bulkCreateMlSignals } from './bulk_create_ml_signals';
+import { ListPluginSetup } from '../../../../../lists/server/types';
jest.mock('./rule_status_saved_objects_client');
jest.mock('./rule_status_service');
@@ -68,6 +69,11 @@ describe('rules_notification_alert_type', () => {
modulesProvider: jest.fn(),
resultsServiceProvider: jest.fn(),
};
+ const listMock = {
+ getListClient: () => ({
+ getListItemByValues: () => [],
+ }),
+ };
let payload: jest.Mocked;
let alert: ReturnType;
let logger: ReturnType