[RAC][Security Solution] Refactor persistence and security rule generic types (#114022)
* Refactor persistence and security rule generic types * Remove unused import, fix unit tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
6a55f87da0
commit
9e908f6caa
18 changed files with 255 additions and 297 deletions
|
@ -32,7 +32,7 @@ export {
|
|||
LifecycleAlertServices,
|
||||
createLifecycleExecutor,
|
||||
} from './utils/create_lifecycle_executor';
|
||||
export { createPersistenceRuleTypeFactory } from './utils/create_persistence_rule_type_factory';
|
||||
export { createPersistenceRuleTypeWrapper } from './utils/create_persistence_rule_type_wrapper';
|
||||
export * from './utils/persistence_types';
|
||||
export type { AlertsClient } from './alert_data_client/alerts_client';
|
||||
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
|
||||
import { ALERT_INSTANCE_ID, VERSION } from '@kbn/rule-data-utils';
|
||||
import { getCommonAlertFields } from './get_common_alert_fields';
|
||||
import { CreatePersistenceRuleTypeFactory } from './persistence_types';
|
||||
import { CreatePersistenceRuleTypeWrapper } from './persistence_types';
|
||||
|
||||
export const createPersistenceRuleTypeFactory: CreatePersistenceRuleTypeFactory =
|
||||
export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper =
|
||||
({ logger, ruleDataClient }) =>
|
||||
(type) => {
|
||||
return {
|
|
@ -8,43 +8,59 @@
|
|||
import { ApiResponse } from '@elastic/elasticsearch';
|
||||
import { BulkResponse } from '@elastic/elasticsearch/api/types';
|
||||
import { Logger } from '@kbn/logging';
|
||||
import { ESSearchRequest } from 'src/core/types/elasticsearch';
|
||||
import {
|
||||
AlertExecutorOptions,
|
||||
AlertInstanceContext,
|
||||
AlertInstanceState,
|
||||
AlertType,
|
||||
AlertTypeParams,
|
||||
AlertTypeState,
|
||||
} from '../../../alerting/server';
|
||||
import { WithoutReservedActionGroups } from '../../../alerting/common';
|
||||
import { IRuleDataClient } from '../rule_data_client';
|
||||
import { AlertTypeWithExecutor } from '../types';
|
||||
|
||||
export type PersistenceAlertService<
|
||||
TState extends AlertInstanceState = never,
|
||||
TContext extends AlertInstanceContext = never,
|
||||
TActionGroupIds extends string = never
|
||||
> = (
|
||||
export type PersistenceAlertService = (
|
||||
alerts: Array<{
|
||||
id: string;
|
||||
fields: Record<string, unknown>;
|
||||
}>,
|
||||
refresh: boolean | 'wait_for'
|
||||
) => Promise<ApiResponse<BulkResponse, unknown>>;
|
||||
) => Promise<ApiResponse<BulkResponse, unknown> | undefined>;
|
||||
|
||||
export type PersistenceAlertQueryService = (
|
||||
query: ESSearchRequest
|
||||
) => Promise<Array<Record<string, unknown>>>;
|
||||
export interface PersistenceServices<TAlertInstanceContext extends AlertInstanceContext = {}> {
|
||||
alertWithPersistence: PersistenceAlertService<TAlertInstanceContext>;
|
||||
export interface PersistenceServices {
|
||||
alertWithPersistence: PersistenceAlertService;
|
||||
}
|
||||
|
||||
export type CreatePersistenceRuleTypeFactory = (options: {
|
||||
export type PersistenceAlertType<
|
||||
TParams extends AlertTypeParams,
|
||||
TState extends AlertTypeState,
|
||||
TInstanceContext extends AlertInstanceContext = {},
|
||||
TActionGroupIds extends string = never
|
||||
> = Omit<
|
||||
AlertType<TParams, TParams, TState, AlertInstanceState, TInstanceContext, TActionGroupIds>,
|
||||
'executor'
|
||||
> & {
|
||||
executor: (
|
||||
options: AlertExecutorOptions<
|
||||
TParams,
|
||||
TState,
|
||||
AlertInstanceState,
|
||||
TInstanceContext,
|
||||
WithoutReservedActionGroups<TActionGroupIds, never>
|
||||
> & {
|
||||
services: PersistenceServices;
|
||||
}
|
||||
) => Promise<TState | void>;
|
||||
};
|
||||
|
||||
export type CreatePersistenceRuleTypeWrapper = (options: {
|
||||
ruleDataClient: IRuleDataClient;
|
||||
logger: Logger;
|
||||
}) => <
|
||||
TState extends AlertTypeState,
|
||||
TParams extends AlertTypeParams,
|
||||
TServices extends PersistenceServices<TAlertInstanceContext>,
|
||||
TAlertInstanceContext extends AlertInstanceContext = {}
|
||||
TState extends AlertTypeState,
|
||||
TInstanceContext extends AlertInstanceContext = {},
|
||||
TActionGroupIds extends string = never
|
||||
>(
|
||||
type: AlertTypeWithExecutor<TState, TParams, TAlertInstanceContext, TServices>
|
||||
) => AlertTypeWithExecutor<TState, TParams, TAlertInstanceContext, TServices>;
|
||||
type: PersistenceAlertType<TParams, TState, TInstanceContext, TActionGroupIds>
|
||||
) => AlertType<TParams, TParams, TState, AlertInstanceState, TInstanceContext, TActionGroupIds>;
|
||||
|
|
|
@ -14,7 +14,7 @@ import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils';
|
|||
import { ListArray } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { toError } from '@kbn/securitysolution-list-api';
|
||||
|
||||
import { createPersistenceRuleTypeFactory } from '../../../../../rule_registry/server';
|
||||
import { createPersistenceRuleTypeWrapper } from '../../../../../rule_registry/server';
|
||||
import { buildRuleMessageFactory } from './factories/build_rule_message_factory';
|
||||
import {
|
||||
checkPrivilegesFromEsClient,
|
||||
|
@ -22,10 +22,14 @@ import {
|
|||
getRuleRangeTuples,
|
||||
hasReadIndexPrivileges,
|
||||
hasTimestampFields,
|
||||
isMachineLearningParams,
|
||||
isEqlParams,
|
||||
isQueryParams,
|
||||
isSavedQueryParams,
|
||||
isThreatParams,
|
||||
isThresholdParams,
|
||||
} from '../signals/utils';
|
||||
import { DEFAULT_MAX_SIGNALS, DEFAULT_SEARCH_AFTER_PAGE_SIZE } from '../../../../common/constants';
|
||||
import { CreateSecurityRuleTypeFactory } from './types';
|
||||
import { CreateSecurityRuleTypeWrapper } from './types';
|
||||
import { getListClient } from './utils/get_list_client';
|
||||
import {
|
||||
NotificationRuleTypeParams,
|
||||
|
@ -37,13 +41,14 @@ import { bulkCreateFactory, wrapHitsFactory, wrapSequencesFactory } from './fact
|
|||
import { RuleExecutionLogClient } from '../rule_execution_log/rule_execution_log_client';
|
||||
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
|
||||
import { scheduleThrottledNotificationActions } from '../notifications/schedule_throttle_notification_actions';
|
||||
import { AlertAttributes } from '../signals/types';
|
||||
|
||||
/* eslint-disable complexity */
|
||||
export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory =
|
||||
export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
|
||||
({ lists, logger, config, ruleDataClient, eventLogService }) =>
|
||||
(type) => {
|
||||
const { alertIgnoreFields: ignoreFields, alertMergeStrategy: mergeStrategy } = config;
|
||||
const persistenceRuleType = createPersistenceRuleTypeFactory({ ruleDataClient, logger });
|
||||
const persistenceRuleType = createPersistenceRuleTypeWrapper({ ruleDataClient, logger });
|
||||
return persistenceRuleType({
|
||||
...type,
|
||||
async executor(options) {
|
||||
|
@ -69,7 +74,10 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory =
|
|||
eventLogService,
|
||||
underlyingClient: config.ruleExecutionLog.underlyingClient,
|
||||
});
|
||||
const ruleSO = await savedObjectsClient.get('alert', alertId);
|
||||
const ruleSO = await savedObjectsClient.get<AlertAttributes<typeof params>>(
|
||||
'alert',
|
||||
alertId
|
||||
);
|
||||
|
||||
const {
|
||||
actions,
|
||||
|
@ -107,7 +115,16 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory =
|
|||
// move this collection of lines into a function in utils
|
||||
// so that we can use it in create rules route, bulk, etc.
|
||||
try {
|
||||
if (!isMachineLearningParams(params)) {
|
||||
// Typescript 4.1.3 can't figure out that `!isMachineLearningParams(params)` also excludes the only rule type
|
||||
// of rule params that doesn't include `params.index`, but Typescript 4.3.5 does compute the stricter type correctly.
|
||||
// When we update Typescript to >= 4.3.5, we can replace this logic with `!isMachineLearningParams(params)` again.
|
||||
if (
|
||||
isEqlParams(params) ||
|
||||
isThresholdParams(params) ||
|
||||
isQueryParams(params) ||
|
||||
isSavedQueryParams(params) ||
|
||||
isThreatParams(params)
|
||||
) {
|
||||
const index = params.index;
|
||||
const hasTimestampOverride = !!timestampOverride;
|
||||
|
||||
|
@ -254,7 +271,7 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory =
|
|||
createdSignals,
|
||||
createdSignalsCount: createdSignals.length,
|
||||
errors: result.errors.concat(runResult.errors),
|
||||
lastLookbackDate: runResult.lastLookbackDate,
|
||||
lastLookbackDate: runResult.lastLookBackDate,
|
||||
searchAfterTimes: result.searchAfterTimes.concat(runResult.searchAfterTimes),
|
||||
state: runState,
|
||||
success: result.success && runResult.success,
|
|
@ -12,6 +12,7 @@ import { allowedExperimentalValues } from '../../../../../common/experimental_fe
|
|||
import { createEqlAlertType } from './create_eql_alert_type';
|
||||
import { createRuleTypeMocks } from '../__mocks__/rule_type';
|
||||
import { getEqlRuleParams } from '../../schemas/rule_schemas.mock';
|
||||
import { createSecurityRuleTypeWrapper } from '../create_security_rule_type_wrapper';
|
||||
import { createMockConfig } from '../../routes/__mocks__';
|
||||
|
||||
jest.mock('../../rule_execution_log/rule_execution_log_client');
|
||||
|
@ -23,15 +24,20 @@ describe('Event correlation alerts', () => {
|
|||
query: 'any where false',
|
||||
};
|
||||
const { services, dependencies, executor } = createRuleTypeMocks('eql', params);
|
||||
const eqlAlertType = createEqlAlertType({
|
||||
experimentalFeatures: allowedExperimentalValues,
|
||||
const securityRuleTypeWrapper = createSecurityRuleTypeWrapper({
|
||||
lists: dependencies.lists,
|
||||
logger: dependencies.logger,
|
||||
config: createMockConfig(),
|
||||
ruleDataClient: dependencies.ruleDataClient,
|
||||
eventLogService: dependencies.eventLogService,
|
||||
version: '1.0.0',
|
||||
});
|
||||
const eqlAlertType = securityRuleTypeWrapper(
|
||||
createEqlAlertType({
|
||||
experimentalFeatures: allowedExperimentalValues,
|
||||
logger: dependencies.logger,
|
||||
version: '1.0.0',
|
||||
})
|
||||
);
|
||||
dependencies.alerting.registerType(eqlAlertType);
|
||||
services.scopedClusterClient.asCurrentUser.search.mockReturnValue(
|
||||
elasticsearchClientMock.createSuccessTransportRequestPromise({
|
||||
|
|
|
@ -6,24 +6,16 @@
|
|||
*/
|
||||
|
||||
import { validateNonExact } from '@kbn/securitysolution-io-ts-utils';
|
||||
import { PersistenceServices } from '../../../../../../rule_registry/server';
|
||||
import { EQL_RULE_TYPE_ID } from '../../../../../common/constants';
|
||||
import { eqlRuleParams, EqlRuleParams } from '../../schemas/rule_schemas';
|
||||
import { EqlRuleParams, eqlRuleParams } from '../../schemas/rule_schemas';
|
||||
import { eqlExecutor } from '../../signals/executors/eql';
|
||||
import { createSecurityRuleTypeFactory } from '../create_security_rule_type_factory';
|
||||
import { CreateRuleOptions } from '../types';
|
||||
import { CreateRuleOptions, SecurityAlertType } from '../types';
|
||||
|
||||
export const createEqlAlertType = (createOptions: CreateRuleOptions) => {
|
||||
const { experimentalFeatures, lists, logger, config, ruleDataClient, version, eventLogService } =
|
||||
createOptions;
|
||||
const createSecurityRuleType = createSecurityRuleTypeFactory({
|
||||
lists,
|
||||
logger,
|
||||
config,
|
||||
ruleDataClient,
|
||||
eventLogService,
|
||||
});
|
||||
return createSecurityRuleType<EqlRuleParams, {}, PersistenceServices, {}>({
|
||||
export const createEqlAlertType = (
|
||||
createOptions: CreateRuleOptions
|
||||
): SecurityAlertType<EqlRuleParams, {}, {}, 'default'> => {
|
||||
const { experimentalFeatures, logger, version } = createOptions;
|
||||
return {
|
||||
id: EQL_RULE_TYPE_ID,
|
||||
name: 'Event Correlation Rule',
|
||||
validate: {
|
||||
|
@ -83,5 +75,5 @@ export const createEqlAlertType = (createOptions: CreateRuleOptions) => {
|
|||
});
|
||||
return { ...result, state };
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
@ -16,7 +16,6 @@ import { BuildRuleMessage } from '../../signals/rule_messages';
|
|||
import { errorAggregator, makeFloatString } from '../../signals/utils';
|
||||
import { RefreshTypes } from '../../types';
|
||||
import { PersistenceAlertService } from '../../../../../../rule_registry/server';
|
||||
import { AlertInstanceContext } from '../../../../../../alerting/common';
|
||||
|
||||
export interface GenericBulkCreateResponse<T> {
|
||||
success: boolean;
|
||||
|
@ -27,9 +26,9 @@ export interface GenericBulkCreateResponse<T> {
|
|||
}
|
||||
|
||||
export const bulkCreateFactory =
|
||||
<TContext extends AlertInstanceContext>(
|
||||
(
|
||||
logger: Logger,
|
||||
alertWithPersistence: PersistenceAlertService<TContext>,
|
||||
alertWithPersistence: PersistenceAlertService,
|
||||
buildRuleMessage: BuildRuleMessage,
|
||||
refreshForBulkCreate: RefreshTypes
|
||||
) =>
|
||||
|
@ -61,6 +60,19 @@ export const bulkCreateFactory =
|
|||
`individual bulk process time took: ${makeFloatString(end - start)} milliseconds`
|
||||
)
|
||||
);
|
||||
|
||||
if (response == null) {
|
||||
return {
|
||||
errors: [
|
||||
'alertWithPersistence returned undefined response. Alerts as Data write flag may be disabled.',
|
||||
],
|
||||
success: false,
|
||||
bulkCreateDuration: makeFloatString(end - start),
|
||||
createdItemsCount: 0,
|
||||
createdItems: [],
|
||||
};
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
buildRuleMessage(`took property says bulk took: ${response.body.took} milliseconds`)
|
||||
);
|
||||
|
|
|
@ -16,6 +16,7 @@ import { createIndicatorMatchAlertType } from './create_indicator_match_alert_ty
|
|||
import { sampleDocNoSortId } from '../../signals/__mocks__/es_results';
|
||||
import { CountResponse } from 'kibana/server';
|
||||
import { RuleParams } from '../../schemas/rule_schemas';
|
||||
import { createSecurityRuleTypeWrapper } from '../create_security_rule_type_wrapper';
|
||||
import { createMockConfig } from '../../routes/__mocks__';
|
||||
|
||||
jest.mock('../utils/get_list_client', () => ({
|
||||
|
@ -50,18 +51,23 @@ describe('Indicator Match Alerts', () => {
|
|||
to: 'now',
|
||||
type: 'threat_match',
|
||||
};
|
||||
const { services, dependencies, executor } = createRuleTypeMocks('threat_match', params);
|
||||
const securityRuleTypeWrapper = createSecurityRuleTypeWrapper({
|
||||
lists: dependencies.lists,
|
||||
logger: dependencies.logger,
|
||||
config: createMockConfig(),
|
||||
ruleDataClient: dependencies.ruleDataClient,
|
||||
eventLogService: dependencies.eventLogService,
|
||||
});
|
||||
|
||||
it('does not send an alert when no events found', async () => {
|
||||
const { services, dependencies, executor } = createRuleTypeMocks('threat_match', params);
|
||||
const indicatorMatchAlertType = createIndicatorMatchAlertType({
|
||||
experimentalFeatures: allowedExperimentalValues,
|
||||
lists: dependencies.lists,
|
||||
logger: dependencies.logger,
|
||||
config: createMockConfig(),
|
||||
ruleDataClient: dependencies.ruleDataClient,
|
||||
eventLogService: dependencies.eventLogService,
|
||||
version: '1.0.0',
|
||||
});
|
||||
const indicatorMatchAlertType = securityRuleTypeWrapper(
|
||||
createIndicatorMatchAlertType({
|
||||
experimentalFeatures: allowedExperimentalValues,
|
||||
logger: dependencies.logger,
|
||||
version: '1.0.0',
|
||||
})
|
||||
);
|
||||
|
||||
dependencies.alerting.registerType(indicatorMatchAlertType);
|
||||
|
||||
|
@ -92,16 +98,13 @@ describe('Indicator Match Alerts', () => {
|
|||
});
|
||||
|
||||
it('does not send an alert when no enrichments are found', async () => {
|
||||
const { services, dependencies, executor } = createRuleTypeMocks('threat_match', params);
|
||||
const indicatorMatchAlertType = createIndicatorMatchAlertType({
|
||||
experimentalFeatures: allowedExperimentalValues,
|
||||
lists: dependencies.lists,
|
||||
logger: dependencies.logger,
|
||||
config: createMockConfig(),
|
||||
ruleDataClient: dependencies.ruleDataClient,
|
||||
eventLogService: dependencies.eventLogService,
|
||||
version: '1.0.0',
|
||||
});
|
||||
const indicatorMatchAlertType = securityRuleTypeWrapper(
|
||||
createIndicatorMatchAlertType({
|
||||
experimentalFeatures: allowedExperimentalValues,
|
||||
logger: dependencies.logger,
|
||||
version: '1.0.0',
|
||||
})
|
||||
);
|
||||
|
||||
dependencies.alerting.registerType(indicatorMatchAlertType);
|
||||
|
||||
|
@ -130,16 +133,13 @@ describe('Indicator Match Alerts', () => {
|
|||
});
|
||||
|
||||
it('sends an alert when enrichments are found', async () => {
|
||||
const { services, dependencies, executor } = createRuleTypeMocks('threat_match', params);
|
||||
const indicatorMatchAlertType = createIndicatorMatchAlertType({
|
||||
experimentalFeatures: allowedExperimentalValues,
|
||||
lists: dependencies.lists,
|
||||
logger: dependencies.logger,
|
||||
config: createMockConfig(),
|
||||
ruleDataClient: dependencies.ruleDataClient,
|
||||
eventLogService: dependencies.eventLogService,
|
||||
version: '1.0.0',
|
||||
});
|
||||
const indicatorMatchAlertType = securityRuleTypeWrapper(
|
||||
createIndicatorMatchAlertType({
|
||||
experimentalFeatures: allowedExperimentalValues,
|
||||
logger: dependencies.logger,
|
||||
version: '1.0.0',
|
||||
})
|
||||
);
|
||||
|
||||
dependencies.alerting.registerType(indicatorMatchAlertType);
|
||||
|
||||
|
|
|
@ -6,24 +6,16 @@
|
|||
*/
|
||||
|
||||
import { validateNonExact } from '@kbn/securitysolution-io-ts-utils';
|
||||
import { PersistenceServices } from '../../../../../../rule_registry/server';
|
||||
import { INDICATOR_RULE_TYPE_ID } from '../../../../../common/constants';
|
||||
import { threatRuleParams, ThreatRuleParams } from '../../schemas/rule_schemas';
|
||||
import { ThreatRuleParams, threatRuleParams } from '../../schemas/rule_schemas';
|
||||
import { threatMatchExecutor } from '../../signals/executors/threat_match';
|
||||
import { createSecurityRuleTypeFactory } from '../create_security_rule_type_factory';
|
||||
import { CreateRuleOptions } from '../types';
|
||||
import { CreateRuleOptions, SecurityAlertType } from '../types';
|
||||
|
||||
export const createIndicatorMatchAlertType = (createOptions: CreateRuleOptions) => {
|
||||
const { experimentalFeatures, lists, logger, config, ruleDataClient, version, eventLogService } =
|
||||
createOptions;
|
||||
const createSecurityRuleType = createSecurityRuleTypeFactory({
|
||||
lists,
|
||||
logger,
|
||||
config,
|
||||
ruleDataClient,
|
||||
eventLogService,
|
||||
});
|
||||
return createSecurityRuleType<ThreatRuleParams, {}, PersistenceServices, {}>({
|
||||
export const createIndicatorMatchAlertType = (
|
||||
createOptions: CreateRuleOptions
|
||||
): SecurityAlertType<ThreatRuleParams, {}, {}, 'default'> => {
|
||||
const { experimentalFeatures, logger, version } = createOptions;
|
||||
return {
|
||||
id: INDICATOR_RULE_TYPE_ID,
|
||||
name: 'Indicator Match Rule',
|
||||
validate: {
|
||||
|
@ -86,5 +78,5 @@ export const createIndicatorMatchAlertType = (createOptions: CreateRuleOptions)
|
|||
});
|
||||
return { ...result, state };
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { KibanaRequest, Logger } from 'src/core/server';
|
||||
import { SavedObject } from 'src/core/types';
|
||||
|
||||
import { buildEsQuery, IIndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
|
||||
import { createPersistenceRuleTypeFactory } from '../../../../../rule_registry/server';
|
||||
import { ML_RULE_TYPE_ID } from '../../../../common/constants';
|
||||
import { SecurityRuleRegistry } from '../../../plugin';
|
||||
|
||||
const createSecurityMlRuleType = createPersistenceRuleTypeFactory<SecurityRuleRegistry>();
|
||||
|
||||
import {
|
||||
AlertInstanceContext,
|
||||
AlertInstanceState,
|
||||
AlertServices,
|
||||
} from '../../../../../alerting/server';
|
||||
import { ListClient } from '../../../../../lists/server';
|
||||
import { isJobStarted } from '../../../../common/machine_learning/helpers';
|
||||
import { ExceptionListItemSchema } from '../../../../common/shared_imports';
|
||||
import { SetupPlugins } from '../../../plugin';
|
||||
import { RefreshTypes } from '../types';
|
||||
import { bulkCreateMlSignals } from '../signals/bulk_create_ml_signals';
|
||||
import { filterEventsAgainstList } from '../signals/filters/filter_events_against_list';
|
||||
import { findMlSignals } from '../signals/find_ml_signals';
|
||||
import { BuildRuleMessage } from '../signals/rule_messages';
|
||||
import { RuleStatusService } from '../signals/rule_status_service';
|
||||
import { MachineLearningRuleAttributes } from '../signals/types';
|
||||
import { createErrorsFromShard, createSearchAfterReturnType, mergeReturns } from '../signals/utils';
|
||||
|
||||
export const mlAlertType = createSecurityMlRuleType({
|
||||
id: ML_RULE_TYPE_ID,
|
||||
name: 'Machine Learning Rule',
|
||||
validate: {
|
||||
params: schema.object({
|
||||
indexPatterns: schema.arrayOf(schema.string()),
|
||||
customQuery: schema.string(),
|
||||
}),
|
||||
},
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
},
|
||||
],
|
||||
defaultActionGroupId: 'default',
|
||||
actionVariables: {
|
||||
context: [{ name: 'server', description: 'the server' }],
|
||||
},
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: false,
|
||||
producer: 'security-solution',
|
||||
async executor({
|
||||
services: { alertWithPersistence, findAlerts },
|
||||
params: { indexPatterns, customQuery },
|
||||
}) {
|
||||
return {
|
||||
lastChecked: new Date(),
|
||||
};
|
||||
},
|
||||
});
|
||||
*/
|
|
@ -14,6 +14,7 @@ import { createRuleTypeMocks } from '../__mocks__/rule_type';
|
|||
import { createMlAlertType } from './create_ml_alert_type';
|
||||
|
||||
import { RuleParams } from '../../schemas/rule_schemas';
|
||||
import { createSecurityRuleTypeWrapper } from '../create_security_rule_type_wrapper';
|
||||
import { createMockConfig } from '../../routes/__mocks__';
|
||||
|
||||
jest.mock('../../signals/bulk_create_ml_signals');
|
||||
|
@ -94,16 +95,21 @@ describe('Machine Learning Alerts', () => {
|
|||
},
|
||||
]);
|
||||
const { dependencies, executor } = createRuleTypeMocks('machine_learning', params);
|
||||
const mlAlertType = createMlAlertType({
|
||||
experimentalFeatures: allowedExperimentalValues,
|
||||
const securityRuleTypeWrapper = createSecurityRuleTypeWrapper({
|
||||
lists: dependencies.lists,
|
||||
logger: dependencies.logger,
|
||||
config: createMockConfig(),
|
||||
ml: mlMock,
|
||||
ruleDataClient: dependencies.ruleDataClient,
|
||||
eventLogService: dependencies.eventLogService,
|
||||
version: '1.0.0',
|
||||
});
|
||||
const mlAlertType = securityRuleTypeWrapper(
|
||||
createMlAlertType({
|
||||
experimentalFeatures: allowedExperimentalValues,
|
||||
logger: dependencies.logger,
|
||||
ml: mlMock,
|
||||
version: '1.0.0',
|
||||
})
|
||||
);
|
||||
|
||||
dependencies.alerting.registerType(mlAlertType);
|
||||
|
||||
|
|
|
@ -6,23 +6,16 @@
|
|||
*/
|
||||
|
||||
import { validateNonExact } from '@kbn/securitysolution-io-ts-utils';
|
||||
import { PersistenceServices } from '../../../../../../rule_registry/server';
|
||||
import { ML_RULE_TYPE_ID } from '../../../../../common/constants';
|
||||
import { machineLearningRuleParams, MachineLearningRuleParams } from '../../schemas/rule_schemas';
|
||||
import { MachineLearningRuleParams, machineLearningRuleParams } from '../../schemas/rule_schemas';
|
||||
import { mlExecutor } from '../../signals/executors/ml';
|
||||
import { createSecurityRuleTypeFactory } from '../create_security_rule_type_factory';
|
||||
import { CreateRuleOptions } from '../types';
|
||||
import { CreateRuleOptions, SecurityAlertType } from '../types';
|
||||
|
||||
export const createMlAlertType = (createOptions: CreateRuleOptions) => {
|
||||
const { lists, logger, config, ml, ruleDataClient, eventLogService } = createOptions;
|
||||
const createSecurityRuleType = createSecurityRuleTypeFactory({
|
||||
lists,
|
||||
logger,
|
||||
config,
|
||||
ruleDataClient,
|
||||
eventLogService,
|
||||
});
|
||||
return createSecurityRuleType<MachineLearningRuleParams, {}, PersistenceServices, {}>({
|
||||
export const createMlAlertType = (
|
||||
createOptions: CreateRuleOptions
|
||||
): SecurityAlertType<MachineLearningRuleParams, {}, {}, 'default'> => {
|
||||
const { logger, ml } = createOptions;
|
||||
return {
|
||||
id: ML_RULE_TYPE_ID,
|
||||
name: 'Machine Learning Rule',
|
||||
validate: {
|
||||
|
@ -81,5 +74,5 @@ export const createMlAlertType = (createOptions: CreateRuleOptions) => {
|
|||
});
|
||||
return { ...result, state };
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@ import { allowedExperimentalValues } from '../../../../../common/experimental_fe
|
|||
import { sampleDocNoSortId } from '../../signals/__mocks__/es_results';
|
||||
import { createQueryAlertType } from './create_query_alert_type';
|
||||
import { createRuleTypeMocks } from '../__mocks__/rule_type';
|
||||
import { createSecurityRuleTypeWrapper } from '../create_security_rule_type_wrapper';
|
||||
import { createMockConfig } from '../../routes/__mocks__';
|
||||
|
||||
jest.mock('../utils/get_list_client', () => ({
|
||||
|
@ -26,17 +27,22 @@ jest.mock('../utils/get_list_client', () => ({
|
|||
jest.mock('../../rule_execution_log/rule_execution_log_client');
|
||||
|
||||
describe('Custom Query Alerts', () => {
|
||||
const { services, dependencies, executor } = createRuleTypeMocks();
|
||||
const securityRuleTypeWrapper = createSecurityRuleTypeWrapper({
|
||||
lists: dependencies.lists,
|
||||
logger: dependencies.logger,
|
||||
config: createMockConfig(),
|
||||
ruleDataClient: dependencies.ruleDataClient,
|
||||
eventLogService: dependencies.eventLogService,
|
||||
});
|
||||
it('does not send an alert when no events found', async () => {
|
||||
const { services, dependencies, executor } = createRuleTypeMocks();
|
||||
const queryAlertType = createQueryAlertType({
|
||||
experimentalFeatures: allowedExperimentalValues,
|
||||
lists: dependencies.lists,
|
||||
logger: dependencies.logger,
|
||||
config: createMockConfig(),
|
||||
ruleDataClient: dependencies.ruleDataClient,
|
||||
eventLogService: dependencies.eventLogService,
|
||||
version: '1.0.0',
|
||||
});
|
||||
const queryAlertType = securityRuleTypeWrapper(
|
||||
createQueryAlertType({
|
||||
experimentalFeatures: allowedExperimentalValues,
|
||||
logger: dependencies.logger,
|
||||
version: '1.0.0',
|
||||
})
|
||||
);
|
||||
|
||||
dependencies.alerting.registerType(queryAlertType);
|
||||
|
||||
|
@ -74,16 +80,13 @@ describe('Custom Query Alerts', () => {
|
|||
});
|
||||
|
||||
it('sends a properly formatted alert when events are found', async () => {
|
||||
const { services, dependencies, executor } = createRuleTypeMocks();
|
||||
const queryAlertType = createQueryAlertType({
|
||||
experimentalFeatures: allowedExperimentalValues,
|
||||
lists: dependencies.lists,
|
||||
logger: dependencies.logger,
|
||||
config: createMockConfig(),
|
||||
ruleDataClient: dependencies.ruleDataClient,
|
||||
eventLogService: dependencies.eventLogService,
|
||||
version: '1.0.0',
|
||||
});
|
||||
const queryAlertType = securityRuleTypeWrapper(
|
||||
createQueryAlertType({
|
||||
experimentalFeatures: allowedExperimentalValues,
|
||||
logger: dependencies.logger,
|
||||
version: '1.0.0',
|
||||
})
|
||||
);
|
||||
|
||||
dependencies.alerting.registerType(queryAlertType);
|
||||
|
||||
|
|
|
@ -6,24 +6,16 @@
|
|||
*/
|
||||
|
||||
import { validateNonExact } from '@kbn/securitysolution-io-ts-utils';
|
||||
import { PersistenceServices } from '../../../../../../rule_registry/server';
|
||||
import { QUERY_RULE_TYPE_ID } from '../../../../../common/constants';
|
||||
import { queryRuleParams, QueryRuleParams } from '../../schemas/rule_schemas';
|
||||
import { QueryRuleParams, queryRuleParams } from '../../schemas/rule_schemas';
|
||||
import { queryExecutor } from '../../signals/executors/query';
|
||||
import { createSecurityRuleTypeFactory } from '../create_security_rule_type_factory';
|
||||
import { CreateRuleOptions } from '../types';
|
||||
import { CreateRuleOptions, SecurityAlertType } from '../types';
|
||||
|
||||
export const createQueryAlertType = (createOptions: CreateRuleOptions) => {
|
||||
const { experimentalFeatures, lists, logger, config, ruleDataClient, version, eventLogService } =
|
||||
createOptions;
|
||||
const createSecurityRuleType = createSecurityRuleTypeFactory({
|
||||
lists,
|
||||
logger,
|
||||
config,
|
||||
ruleDataClient,
|
||||
eventLogService,
|
||||
});
|
||||
return createSecurityRuleType<QueryRuleParams, {}, PersistenceServices, {}>({
|
||||
export const createQueryAlertType = (
|
||||
createOptions: CreateRuleOptions
|
||||
): SecurityAlertType<QueryRuleParams, {}, {}, 'default'> => {
|
||||
const { experimentalFeatures, logger, version } = createOptions;
|
||||
return {
|
||||
id: QUERY_RULE_TYPE_ID,
|
||||
name: 'Custom Query Rule',
|
||||
validate: {
|
||||
|
@ -86,5 +78,5 @@ export const createQueryAlertType = (createOptions: CreateRuleOptions) => {
|
|||
});
|
||||
return { ...result, state };
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ import { allowedExperimentalValues } from '../../../../../common/experimental_fe
|
|||
import { createThresholdAlertType } from './create_threshold_alert_type';
|
||||
import { createRuleTypeMocks } from '../__mocks__/rule_type';
|
||||
import { getThresholdRuleParams } from '../../schemas/rule_schemas.mock';
|
||||
import { createSecurityRuleTypeWrapper } from '../create_security_rule_type_wrapper';
|
||||
import { createMockConfig } from '../../routes/__mocks__';
|
||||
|
||||
jest.mock('../../rule_execution_log/rule_execution_log_client');
|
||||
|
@ -17,16 +18,21 @@ describe('Threshold Alerts', () => {
|
|||
it('does not send an alert when no events found', async () => {
|
||||
const params = getThresholdRuleParams();
|
||||
const { dependencies, executor } = createRuleTypeMocks('threshold', params);
|
||||
const thresholdAlertTpe = createThresholdAlertType({
|
||||
experimentalFeatures: allowedExperimentalValues,
|
||||
const securityRuleTypeWrapper = createSecurityRuleTypeWrapper({
|
||||
lists: dependencies.lists,
|
||||
logger: dependencies.logger,
|
||||
config: createMockConfig(),
|
||||
ruleDataClient: dependencies.ruleDataClient,
|
||||
eventLogService: dependencies.eventLogService,
|
||||
version: '1.0.0',
|
||||
});
|
||||
dependencies.alerting.registerType(thresholdAlertTpe);
|
||||
const thresholdAlertType = securityRuleTypeWrapper(
|
||||
createThresholdAlertType({
|
||||
experimentalFeatures: allowedExperimentalValues,
|
||||
logger: dependencies.logger,
|
||||
version: '1.0.0',
|
||||
})
|
||||
);
|
||||
dependencies.alerting.registerType(thresholdAlertType);
|
||||
|
||||
await executor({ params });
|
||||
expect(dependencies.ruleDataClient.getWriter).not.toBeCalled();
|
||||
|
|
|
@ -6,26 +6,17 @@
|
|||
*/
|
||||
|
||||
import { validateNonExact } from '@kbn/securitysolution-io-ts-utils';
|
||||
|
||||
import { PersistenceServices } from '../../../../../../rule_registry/server';
|
||||
import { THRESHOLD_RULE_TYPE_ID } from '../../../../../common/constants';
|
||||
import { thresholdRuleParams, ThresholdRuleParams } from '../../schemas/rule_schemas';
|
||||
import { thresholdExecutor } from '../../signals/executors/threshold';
|
||||
import { ThresholdAlertState } from '../../signals/types';
|
||||
import { createSecurityRuleTypeFactory } from '../create_security_rule_type_factory';
|
||||
import { CreateRuleOptions } from '../types';
|
||||
import { CreateRuleOptions, SecurityAlertType } from '../types';
|
||||
|
||||
export const createThresholdAlertType = (createOptions: CreateRuleOptions) => {
|
||||
const { experimentalFeatures, lists, logger, config, ruleDataClient, version, eventLogService } =
|
||||
createOptions;
|
||||
const createSecurityRuleType = createSecurityRuleTypeFactory({
|
||||
lists,
|
||||
logger,
|
||||
config,
|
||||
ruleDataClient,
|
||||
eventLogService,
|
||||
});
|
||||
return createSecurityRuleType<ThresholdRuleParams, {}, PersistenceServices, ThresholdAlertState>({
|
||||
export const createThresholdAlertType = (
|
||||
createOptions: CreateRuleOptions
|
||||
): SecurityAlertType<ThresholdRuleParams, ThresholdAlertState, {}, 'default'> => {
|
||||
const { experimentalFeatures, logger, version } = createOptions;
|
||||
return {
|
||||
id: THRESHOLD_RULE_TYPE_ID,
|
||||
name: 'Threshold Rule',
|
||||
validate: {
|
||||
|
@ -63,8 +54,6 @@ export const createThresholdAlertType = (createOptions: CreateRuleOptions) => {
|
|||
state,
|
||||
} = execOptions;
|
||||
|
||||
// console.log(JSON.stringify(state));
|
||||
|
||||
const result = await thresholdExecutor({
|
||||
buildRuleMessage,
|
||||
bulkCreate,
|
||||
|
@ -82,5 +71,5 @@ export const createThresholdAlertType = (createOptions: CreateRuleOptions) => {
|
|||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
@ -11,28 +11,30 @@ import { SearchHit } from '@elastic/elasticsearch/api/types';
|
|||
import { Logger } from '@kbn/logging';
|
||||
import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
|
||||
|
||||
import { AlertExecutorOptions, AlertType } from '../../../../../alerting/server';
|
||||
import { SavedObject } from '../../../../../../../src/core/server';
|
||||
import {
|
||||
AlertInstanceContext,
|
||||
AlertInstanceState,
|
||||
AlertTypeParams,
|
||||
AlertTypeState,
|
||||
WithoutReservedActionGroups,
|
||||
} from '../../../../../alerting/common';
|
||||
import { AlertType } from '../../../../../alerting/server';
|
||||
import { ListClient } from '../../../../../lists/server';
|
||||
import { TechnicalRuleFieldMap } from '../../../../../rule_registry/common/assets/field_maps/technical_rule_field_map';
|
||||
import { TypeOfFieldMap } from '../../../../../rule_registry/common/field_map';
|
||||
import {
|
||||
AlertTypeWithExecutor,
|
||||
PersistenceServices,
|
||||
IRuleDataClient,
|
||||
} from '../../../../../rule_registry/server';
|
||||
import { PersistenceServices, IRuleDataClient } from '../../../../../rule_registry/server';
|
||||
import { BaseHit } from '../../../../common/detection_engine/types';
|
||||
import { ConfigType } from '../../../config';
|
||||
import { SetupPlugins } from '../../../plugin';
|
||||
import { RuleParams } from '../schemas/rule_schemas';
|
||||
import { BuildRuleMessage } from '../signals/rule_messages';
|
||||
import { AlertAttributes, BulkCreate, WrapHits, WrapSequences } from '../signals/types';
|
||||
import {
|
||||
AlertAttributes,
|
||||
BulkCreate,
|
||||
SearchAfterAndBulkCreateReturnType,
|
||||
WrapHits,
|
||||
WrapSequences,
|
||||
} from '../signals/types';
|
||||
import { AlertsFieldMap, RulesFieldMap } from './field_maps';
|
||||
import { ExperimentalFeatures } from '../../../../common/experimental_features';
|
||||
import { IEventLogService } from '../../../../../event_log/server';
|
||||
|
@ -50,12 +52,6 @@ export interface SecurityAlertTypeReturnValue<TState extends AlertTypeState> {
|
|||
warningMessages: string[];
|
||||
}
|
||||
|
||||
type SimpleAlertType<
|
||||
TState extends AlertTypeState,
|
||||
TParams extends AlertTypeParams = {},
|
||||
TAlertInstanceContext extends AlertInstanceContext = {}
|
||||
> = AlertType<TParams, TParams, TState, AlertInstanceState, TAlertInstanceContext, string, string>;
|
||||
|
||||
export interface RunOpts<TParams extends RuleParams> {
|
||||
buildRuleMessage: BuildRuleMessage;
|
||||
bulkCreate: BulkCreate;
|
||||
|
@ -72,44 +68,42 @@ export interface RunOpts<TParams extends RuleParams> {
|
|||
wrapSequences: WrapSequences;
|
||||
}
|
||||
|
||||
export type SecurityAlertTypeExecutor<
|
||||
TState extends AlertTypeState,
|
||||
TServices extends PersistenceServices<TAlertInstanceContext>,
|
||||
export type SecurityAlertType<
|
||||
TParams extends RuleParams,
|
||||
TAlertInstanceContext extends AlertInstanceContext = {}
|
||||
> = (
|
||||
options: Parameters<SimpleAlertType<TState, TParams, TAlertInstanceContext>['executor']>[0] & {
|
||||
runOpts: RunOpts<TParams>;
|
||||
} & { services: TServices }
|
||||
) => Promise<SecurityAlertTypeReturnValue<TState>>;
|
||||
|
||||
type SecurityAlertTypeWithExecutor<
|
||||
TState extends AlertTypeState,
|
||||
TServices extends PersistenceServices<TAlertInstanceContext>,
|
||||
TParams extends RuleParams,
|
||||
TAlertInstanceContext extends AlertInstanceContext = {}
|
||||
TInstanceContext extends AlertInstanceContext = {},
|
||||
TActionGroupIds extends string = never
|
||||
> = Omit<
|
||||
AlertType<TParams, TParams, TState, AlertInstanceState, TAlertInstanceContext, string, string>,
|
||||
AlertType<TParams, TParams, TState, AlertInstanceState, TInstanceContext, TActionGroupIds>,
|
||||
'executor'
|
||||
> & {
|
||||
executor: SecurityAlertTypeExecutor<TState, TServices, TParams, TAlertInstanceContext>;
|
||||
executor: (
|
||||
options: AlertExecutorOptions<
|
||||
TParams,
|
||||
TState,
|
||||
AlertInstanceState,
|
||||
TInstanceContext,
|
||||
WithoutReservedActionGroups<TActionGroupIds, never>
|
||||
> & {
|
||||
services: PersistenceServices;
|
||||
runOpts: RunOpts<TParams>;
|
||||
}
|
||||
) => Promise<SearchAfterAndBulkCreateReturnType & { state: TState }>;
|
||||
};
|
||||
|
||||
export type CreateSecurityRuleTypeFactory = (options: {
|
||||
export type CreateSecurityRuleTypeWrapper = (options: {
|
||||
lists: SetupPlugins['lists'];
|
||||
logger: Logger;
|
||||
config: ConfigType;
|
||||
ruleDataClient: IRuleDataClient;
|
||||
eventLogService: IEventLogService;
|
||||
}) => <
|
||||
TParams extends RuleParams & { index?: string[] | undefined },
|
||||
TAlertInstanceContext extends AlertInstanceContext,
|
||||
TServices extends PersistenceServices<TAlertInstanceContext>,
|
||||
TState extends AlertTypeState
|
||||
TParams extends RuleParams,
|
||||
TState extends AlertTypeState,
|
||||
TInstanceContext extends AlertInstanceContext = {}
|
||||
>(
|
||||
type: SecurityAlertTypeWithExecutor<TState, TServices, TParams, TAlertInstanceContext>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) => AlertTypeWithExecutor<TState, TParams, TAlertInstanceContext, any>;
|
||||
type: SecurityAlertType<TParams, TState, TInstanceContext, 'default'>
|
||||
) => AlertType<TParams, TParams, TState, AlertInstanceState, TInstanceContext, 'default'>;
|
||||
|
||||
export type RACAlertSignal = TypeOfFieldMap<AlertsFieldMap> & TypeOfFieldMap<RulesFieldMap>;
|
||||
export type RACAlert = Exclude<
|
||||
|
@ -124,11 +118,7 @@ export type WrappedRACAlert = BaseHit<RACAlert>;
|
|||
|
||||
export interface CreateRuleOptions {
|
||||
experimentalFeatures: ExperimentalFeatures;
|
||||
lists: SetupPlugins['lists'];
|
||||
logger: Logger;
|
||||
config: ConfigType;
|
||||
ml?: SetupPlugins['ml'];
|
||||
ruleDataClient: IRuleDataClient;
|
||||
version: string;
|
||||
eventLogService: IEventLogService;
|
||||
}
|
||||
|
|
|
@ -109,6 +109,7 @@ import { ctiFieldMap } from './lib/detection_engine/rule_types/field_maps/cti';
|
|||
import { legacyRulesNotificationAlertType } from './lib/detection_engine/notifications/legacy_rules_notification_alert_type';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { legacyIsNotificationAlertExecutor } from './lib/detection_engine/notifications/legacy_types';
|
||||
import { createSecurityRuleTypeWrapper } from './lib/detection_engine/rule_types/create_security_rule_type_wrapper';
|
||||
import { IEventLogClientService, IEventLogService } from '../../event_log/server';
|
||||
import { registerEventLogProvider } from './lib/detection_engine/rule_execution_log/event_log_adapter/register_event_log_provider';
|
||||
|
||||
|
@ -268,20 +269,34 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
// Register rule types via rule-registry
|
||||
const createRuleOptions: CreateRuleOptions = {
|
||||
experimentalFeatures,
|
||||
lists: plugins.lists,
|
||||
logger: this.logger,
|
||||
config: this.config,
|
||||
ml: plugins.ml,
|
||||
ruleDataClient,
|
||||
eventLogService,
|
||||
version: this.context.env.packageInfo.version,
|
||||
};
|
||||
|
||||
this.setupPlugins.alerting.registerType(createEqlAlertType(createRuleOptions));
|
||||
this.setupPlugins.alerting.registerType(createIndicatorMatchAlertType(createRuleOptions));
|
||||
this.setupPlugins.alerting.registerType(createMlAlertType(createRuleOptions));
|
||||
this.setupPlugins.alerting.registerType(createQueryAlertType(createRuleOptions));
|
||||
this.setupPlugins.alerting.registerType(createThresholdAlertType(createRuleOptions));
|
||||
const securityRuleTypeWrapper = createSecurityRuleTypeWrapper({
|
||||
lists: plugins.lists,
|
||||
logger: this.logger,
|
||||
config: this.config,
|
||||
ruleDataClient,
|
||||
eventLogService,
|
||||
});
|
||||
|
||||
this.setupPlugins.alerting.registerType(
|
||||
securityRuleTypeWrapper(createEqlAlertType(createRuleOptions))
|
||||
);
|
||||
this.setupPlugins.alerting.registerType(
|
||||
securityRuleTypeWrapper(createIndicatorMatchAlertType(createRuleOptions))
|
||||
);
|
||||
this.setupPlugins.alerting.registerType(
|
||||
securityRuleTypeWrapper(createMlAlertType(createRuleOptions))
|
||||
);
|
||||
this.setupPlugins.alerting.registerType(
|
||||
securityRuleTypeWrapper(createQueryAlertType(createRuleOptions))
|
||||
);
|
||||
this.setupPlugins.alerting.registerType(
|
||||
securityRuleTypeWrapper(createThresholdAlertType(createRuleOptions))
|
||||
);
|
||||
}
|
||||
|
||||
// TODO We need to get the endpoint routes inside of initRoutes
|
||||
|
|
Loading…
Reference in a new issue