[AI4DSOC] Add possibility to disable Stack Rules, Rules Settings and Maintenance window based on Serverless Tier (#214586)

This commit is contained in:
Tomasz Ciecierski 2025-04-22 10:50:48 +02:00 committed by GitHub
parent caa9b3ff45
commit 6356f2cdf1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 636 additions and 193 deletions

View file

@ -136,6 +136,7 @@ enabled:
- x-pack/test/alerting_api_integration/security_and_spaces/group3/config.ts
- x-pack/test/alerting_api_integration/security_and_spaces/group4/config.ts
- x-pack/test/alerting_api_integration/security_and_spaces/group5/config.ts
- x-pack/test/alerting_api_integration/security_and_spaces/group6/config.ts
- x-pack/test/alerting_api_integration/security_and_spaces/group3/config_with_schedule_circuit_breaker.ts
- x-pack/test/alerting_api_integration/security_and_spaces/group2/config_non_dedicated_task_runner.ts
- x-pack/test/alerting_api_integration/security_and_spaces/group4/config_non_dedicated_task_runner.ts

View file

@ -5,6 +5,11 @@ xpack.osquery.enabled: false
xpack.ml.ad.enabled: false
xpack.ml.dfa.enabled: false
## Disable plugin features
xpack.alerting.maintenanceWindow.enabled: false
xpack.alerting.rulesSettings.enabled: false
xpack.trigger_actions_ui.rules.enabled: false
xpack.features.overrides:
### The following features are Security features hidden in Role management UI for this specific tier.
securitySolutionTimeline.hidden: true

View file

@ -52,8 +52,8 @@ export function MaintenanceWindowCallout({
} = kibanaServices;
const isMaintenanceWindowDisabled =
!capabilities[MAINTENANCE_WINDOW_FEATURE_ID].show &&
!capabilities[MAINTENANCE_WINDOW_FEATURE_ID].save;
!capabilities[MAINTENANCE_WINDOW_FEATURE_ID]?.show &&
!capabilities[MAINTENANCE_WINDOW_FEATURE_ID]?.save;
const { data: activeMaintenanceWindows = [] } = useFetchActiveMaintenanceWindows(kibanaServices, {
enabled: !isMaintenanceWindowDisabled,
});

View file

@ -349,8 +349,11 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.stack_connectors.enableExperimental (array?)',
'xpack.trigger_actions_ui.enableExperimental (array?)',
'xpack.trigger_actions_ui.enableGeoTrackingThresholdAlert (boolean?)',
'xpack.trigger_actions_ui.rules.enabled (boolean?)',
'xpack.timelines.enableExperimental (array?)',
'xpack.alerting.rules.run.alerts.max (number?)',
'xpack.alerting.maintenanceWindow.enabled (boolean?)',
'xpack.alerting.rulesSettings.enabled (boolean?)',
'xpack.upgrade_assistant.featureSet.migrateSystemIndices (boolean?)',
'xpack.upgrade_assistant.featureSet.mlSnapshots (boolean?)',
'xpack.upgrade_assistant.featureSet.reindexCorrectiveActions (boolean?)',

View file

@ -255,4 +255,3 @@ export const DELETE_MAINTENANCE_WINDOW_API_PATH = `${BASE_MAINTENANCE_WINDOW_API
export const ALERTING_FEATURE_ID = 'alerts';
export const MONITORING_HISTORY_LIMIT = 200;
export const ENABLE_MAINTENANCE_WINDOWS = true;

View file

@ -68,8 +68,8 @@ export const MaintenanceWindowsPage = React.memo(() => {
}, [navigateToCreateMaintenanceWindow]);
const refreshData = useCallback(() => refetch(), [refetch]);
const showWindowMaintenance = capabilities[MAINTENANCE_WINDOW_FEATURE_ID].show;
const writeWindowMaintenance = capabilities[MAINTENANCE_WINDOW_FEATURE_ID].save;
const showWindowMaintenance = capabilities[MAINTENANCE_WINDOW_FEATURE_ID]?.show;
const writeWindowMaintenance = capabilities[MAINTENANCE_WINDOW_FEATURE_ID]?.save;
const isNotFiltered = search === '' && selectedStatus.length === 0;
const showEmptyPrompt =

View file

@ -26,6 +26,9 @@ const mockAlertingUIConfig: AlertingUIConfig = {
},
},
},
maintenanceWindow: {
enabled: true,
},
};
const mockInitializerContext = coreMock.createPluginInitializerContext(mockAlertingUIConfig);

View file

@ -17,8 +17,8 @@ import type { ServerlessPluginStart } from '@kbn/serverless/public';
import type { AlertNavigationHandler } from './alert_navigation_registry';
import { AlertNavigationRegistry } from './alert_navigation_registry';
import { loadRule, loadRuleType } from './services/rule_api';
import { MAINTENANCE_WINDOWS_APP_ID } from '../common';
import type { Rule } from '../common';
import { ENABLE_MAINTENANCE_WINDOWS, MAINTENANCE_WINDOWS_APP_ID } from '../common';
export interface PluginSetupContract {
/**
@ -82,6 +82,7 @@ export interface AlertingUIConfig {
};
};
};
maintenanceWindow: { enabled: boolean };
}
export class AlertingPublicPlugin
@ -115,7 +116,7 @@ export class AlertingPublicPlugin
handler: AlertNavigationHandler
) => this.alertNavigationRegistry!.registerDefault(applicationId, handler);
if (ENABLE_MAINTENANCE_WINDOWS) {
if (this.config.maintenanceWindow?.enabled) {
plugins.management.sections.section.insightsAndAlerting.registerApp({
id: MAINTENANCE_WINDOWS_APP_ID,
title: i18n.translate('xpack.alerting.management.section.title', {

View file

@ -21,6 +21,9 @@ describe('config validation', () => {
"interval": "5m",
"removalDelay": "1h",
},
"maintenanceWindow": Object {
"enabled": true,
},
"rules": Object {
"maxScheduledPerMinute": 32000,
"minimumScheduleInterval": Object {
@ -38,6 +41,7 @@ describe('config validation', () => {
},
"rulesSettings": Object {
"cacheInterval": 60000,
"enabled": true,
},
}
`);

View file

@ -74,8 +74,12 @@ export const configSchema = schema.object({
cancelAlertsOnRuleTimeout: schema.boolean({ defaultValue: true }),
rules: rulesSchema,
rulesSettings: schema.object({
enabled: schema.boolean({ defaultValue: true }),
cacheInterval: schema.number({ defaultValue: DEFAULT_CACHE_INTERVAL_MS }),
}),
maintenanceWindow: schema.object({
enabled: schema.boolean({ defaultValue: true }),
}),
});
export type AlertingConfig = TypeOf<typeof configSchema>;

View file

@ -83,6 +83,8 @@ export const config: PluginConfigDescriptor<AlertingConfig> = {
schema: configSchema,
exposeToBrowser: {
rules: { run: { alerts: { max: true } } },
rulesSettings: { enabled: true },
maintenanceWindow: { enabled: true },
},
deprecations: ({ renameFromRoot, deprecate }) => [
deprecate('maxEphemeralActionsPerAlert', '9.0.0', {

View file

@ -274,9 +274,12 @@ export class AlertingPlugin {
};
});
plugins.features.registerKibanaFeature(getRulesSettingsFeature(this.isServerless));
plugins.features.registerKibanaFeature(maintenanceWindowFeature);
if (this.config.rulesSettings.enabled) {
plugins.features.registerKibanaFeature(getRulesSettingsFeature(this.isServerless));
}
if (this.config.maintenanceWindow.enabled) {
plugins.features.registerKibanaFeature(maintenanceWindowFeature);
}
this.isESOCanEncrypt = plugins.encryptedSavedObjects.canEncrypt;
@ -405,6 +408,7 @@ export class AlertingPlugin {
config$: plugins.unifiedSearch.autocomplete.getInitializerContextConfig().create(),
isServerless: this.isServerless,
docLinks: core.docLinks,
alertingConfig: this.config,
});
return {

View file

@ -10,6 +10,7 @@ import type { UsageCounter } from '@kbn/usage-collection-plugin/server';
import type { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
import type { ConfigSchema } from '@kbn/unified-search-plugin/server/config';
import type { Observable } from 'rxjs';
import type { AlertingConfig } from '../config';
import type { GetAlertIndicesAlias, ILicenseState } from '../lib';
import type { AlertingRequestHandlerContext } from '../types';
import { createRuleRoute } from './rule/apis/create';
@ -96,6 +97,7 @@ export interface RouteOptions {
config$?: Observable<ConfigSchema>;
isServerless?: boolean;
docLinks: DocLinksServiceSetup;
alertingConfig: AlertingConfig;
}
export function defineRoutes(opts: RouteOptions) {
@ -106,6 +108,7 @@ export function defineRoutes(opts: RouteOptions) {
usageCounter,
config$,
getAlertIndicesAlias,
alertingConfig,
} = opts;
createRuleRoute(opts);
@ -147,25 +150,26 @@ export function defineRoutes(opts: RouteOptions) {
muteAlertRoute(router, licenseState);
unmuteAlertRoute(router, licenseState);
// Maintenance Window - Internal APIs
createMaintenanceWindowRouteInternal(router, licenseState);
getMaintenanceWindowRouteInternal(router, licenseState);
updateMaintenanceWindowRouteInternal(router, licenseState);
deleteMaintenanceWindowRouteInternal(router, licenseState);
findMaintenanceWindowsRouteInternal(router, licenseState);
archiveMaintenanceWindowRouteInternal(router, licenseState);
finishMaintenanceWindowRouteInternal(router, licenseState);
getActiveMaintenanceWindowsRouteInternal(router, licenseState);
bulkGetMaintenanceWindowRouteInternal(router, licenseState);
// Maintenance Window - External APIs
getMaintenanceWindowRoute(router, licenseState);
createMaintenanceWindowRoute(router, licenseState);
deleteMaintenanceWindowRoute(router, licenseState);
archiveMaintenanceWindowRoute(router, licenseState);
unarchiveMaintenanceWindowRoute(router, licenseState);
updateMaintenanceWindowRoute(router, licenseState);
if (alertingConfig.maintenanceWindow.enabled) {
// Maintenance Window - Internal APIs
createMaintenanceWindowRouteInternal(router, licenseState);
getMaintenanceWindowRouteInternal(router, licenseState);
updateMaintenanceWindowRouteInternal(router, licenseState);
deleteMaintenanceWindowRouteInternal(router, licenseState);
findMaintenanceWindowsRouteInternal(router, licenseState);
archiveMaintenanceWindowRouteInternal(router, licenseState);
finishMaintenanceWindowRouteInternal(router, licenseState);
getActiveMaintenanceWindowsRouteInternal(router, licenseState);
bulkGetMaintenanceWindowRouteInternal(router, licenseState);
// Maintenance Window - External APIs
getMaintenanceWindowRoute(router, licenseState);
createMaintenanceWindowRoute(router, licenseState);
deleteMaintenanceWindowRoute(router, licenseState);
archiveMaintenanceWindowRoute(router, licenseState);
unarchiveMaintenanceWindowRoute(router, licenseState);
updateMaintenanceWindowRoute(router, licenseState);
}
// backfill APIs
scheduleBackfillRoute(router, licenseState);
getBackfillRoute(router, licenseState);
@ -178,15 +182,18 @@ export function defineRoutes(opts: RouteOptions) {
getRuleIdsWithGapsRoute(router, licenseState);
getGapsSummaryByRuleIdsRoute(router, licenseState);
// Rules Settings APIs
if (alertingConfig.rulesSettings.enabled) {
getQueryDelaySettingsRoute(router, licenseState);
updateQueryDelaySettingsRoute(router, licenseState);
getFlappingSettingsRoute(router, licenseState);
updateFlappingSettingsRoute(router, licenseState);
}
// Other APIs
registerFieldsRoute(router, licenseState);
getScheduleFrequencyRoute(router, licenseState);
getQueryDelaySettingsRoute(router, licenseState);
updateQueryDelaySettingsRoute(router, licenseState);
getGlobalExecutionLogRoute(router, licenseState);
getActionErrorLogRoute(router, licenseState);
getFlappingSettingsRoute(router, licenseState);
updateFlappingSettingsRoute(router, licenseState);
runSoonRoute(router, licenseState);
healthRoute(router, licenseState, encryptedSavedObjects);
getGlobalExecutionKPIRoute(router, licenseState);

View file

@ -34,6 +34,39 @@ describe('createRuleRoute', () => {
const docLinks = docLinksServiceMock.createSetupContract();
const createdAt = new Date();
const updatedAt = new Date();
const alertingConfigMock = {
healthCheck: {
interval: '60m',
},
invalidateApiKeysTask: {
interval: '5m',
removalDelay: '1h',
},
enableFrameworkAlerts: true,
cancelAlertsOnRuleTimeout: true,
rules: {
minimumScheduleInterval: {
value: '1m',
enforce: false,
},
maxScheduledPerMinute: 32000,
run: {
actions: {
max: 100000,
},
alerts: {
max: 1000,
},
},
},
rulesSettings: {
enabled: true,
cacheInterval: 60000,
},
maintenanceWindow: {
enabled: true,
},
};
const action: RuleAction = {
actionTypeId: 'test',
group: 'default',
@ -154,6 +187,7 @@ describe('createRuleRoute', () => {
encryptedSavedObjects,
usageCounter: mockUsageCounter,
docLinks,
alertingConfig: alertingConfigMock,
});
const [config, handler] = router.post.mock.calls[0];
@ -268,6 +302,7 @@ describe('createRuleRoute', () => {
encryptedSavedObjects,
usageCounter: mockUsageCounter,
docLinks,
alertingConfig: alertingConfigMock,
});
const [config, handler] = router.post.mock.calls[0];
@ -386,6 +421,7 @@ describe('createRuleRoute', () => {
encryptedSavedObjects,
usageCounter: mockUsageCounter,
docLinks,
alertingConfig: alertingConfigMock,
});
const [config, handler] = router.post.mock.calls[0];
@ -505,6 +541,7 @@ describe('createRuleRoute', () => {
encryptedSavedObjects,
usageCounter: mockUsageCounter,
docLinks,
alertingConfig: alertingConfigMock,
});
const [config, handler] = router.post.mock.calls[0];
@ -612,7 +649,13 @@ describe('createRuleRoute', () => {
const router = httpServiceMock.createRouter();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true });
createRuleRoute({ router, licenseState, encryptedSavedObjects, docLinks });
createRuleRoute({
router,
licenseState,
encryptedSavedObjects,
docLinks,
alertingConfig: alertingConfigMock,
});
const [, handler] = router.post.mock.calls[0];
@ -634,7 +677,13 @@ describe('createRuleRoute', () => {
throw new Error('OMG');
});
createRuleRoute({ router, licenseState, encryptedSavedObjects, docLinks });
createRuleRoute({
router,
licenseState,
encryptedSavedObjects,
docLinks,
alertingConfig: alertingConfigMock,
});
const [, handler] = router.post.mock.calls[0];
@ -652,7 +701,13 @@ describe('createRuleRoute', () => {
const router = httpServiceMock.createRouter();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true });
createRuleRoute({ router, licenseState, encryptedSavedObjects, docLinks });
createRuleRoute({
router,
licenseState,
encryptedSavedObjects,
docLinks,
alertingConfig: alertingConfigMock,
});
const [, handler] = router.post.mock.calls[0];
@ -684,6 +739,7 @@ describe('createRuleRoute', () => {
encryptedSavedObjects,
usageCounter: mockUsageCounter,
docLinks,
alertingConfig: alertingConfigMock,
});
const [_, handler] = router.post.mock.calls[0];
@ -767,6 +823,7 @@ describe('createRuleRoute', () => {
encryptedSavedObjects,
usageCounter: mockUsageCounter,
docLinks,
alertingConfig: alertingConfigMock,
});
const [_, handler] = router.post.mock.calls[0];
@ -824,6 +881,7 @@ describe('createRuleRoute', () => {
encryptedSavedObjects,
usageCounter: mockUsageCounter,
docLinks,
alertingConfig: alertingConfigMock,
});
const [_, handler] = router.post.mock.calls[0];

View file

@ -71,6 +71,7 @@ export function generateAlertingConfig(): AlertingConfig {
},
},
},
rulesSettings: { cacheInterval: 60000 },
rulesSettings: { enabled: true, cacheInterval: 60000 },
maintenanceWindow: { enabled: true },
};
}

View file

@ -7,4 +7,7 @@
export interface TriggersActionsUiConfigType {
enableExperimental: string[];
rules: {
enabled: boolean;
};
}

View file

@ -14,10 +14,12 @@ import { useKibana } from '../../../common/lib/kibana';
export const RulesSettingsLink = () => {
const [isVisible, setIsVisible] = useState<boolean>(false);
const {
application: { capabilities },
application: {
capabilities: { rulesSettings = {} },
},
} = useKibana().services;
const { show, readFlappingSettingsUI, readQueryDelaySettingsUI } = capabilities.rulesSettings;
const { show, readFlappingSettingsUI, readQueryDelaySettingsUI } = rulesSettings;
if (!show || (!readFlappingSettingsUI && !readQueryDelaySettingsUI)) {
return null;

View file

@ -240,59 +240,61 @@ export class Plugin
});
}
plugins.management.sections.section.insightsAndAlerting.registerApp({
id: PLUGIN_ID,
title: featureTitle,
order: 1,
async mount(params: ManagementAppMountParams) {
const [coreStart, pluginsStart] = (await core.getStartServices()) as [
CoreStart,
PluginsStart,
unknown
];
if (this.config.rules.enabled) {
plugins.management.sections.section.insightsAndAlerting.registerApp({
id: PLUGIN_ID,
title: featureTitle,
order: 1,
async mount(params: ManagementAppMountParams) {
const [coreStart, pluginsStart] = (await core.getStartServices()) as [
CoreStart,
PluginsStart,
unknown
];
const { renderApp } = await import('./application/rules_app');
const { renderApp } = await import('./application/rules_app');
// The `/api/features` endpoint requires the "Global All" Kibana privilege. Users with a
// subset of this privilege are not authorized to access this endpoint and will receive a 404
// error that causes the Alerting view to fail to load.
let kibanaFeatures: KibanaFeature[];
try {
kibanaFeatures = await pluginsStart.features.getFeatures();
} catch (err) {
kibanaFeatures = [];
}
// The `/api/features` endpoint requires the "Global All" Kibana privilege. Users with a
// subset of this privilege are not authorized to access this endpoint and will receive a 404
// error that causes the Alerting view to fail to load.
let kibanaFeatures: KibanaFeature[];
try {
kibanaFeatures = await pluginsStart.features.getFeatures();
} catch (err) {
kibanaFeatures = [];
}
return renderApp({
...coreStart,
actions: plugins.actions,
dashboard: pluginsStart.dashboard,
cloud: plugins.cloud,
data: pluginsStart.data,
dataViews: pluginsStart.dataViews,
dataViewEditor: pluginsStart.dataViewEditor,
charts: pluginsStart.charts,
alerting: pluginsStart.alerting,
spaces: pluginsStart.spaces,
unifiedSearch: pluginsStart.unifiedSearch,
isCloud: Boolean(plugins.cloud?.isCloudEnabled),
element: params.element,
theme: params.theme,
storage: new Storage(window.localStorage),
setBreadcrumbs: params.setBreadcrumbs,
history: params.history,
actionTypeRegistry,
ruleTypeRegistry,
kibanaFeatures,
licensing: pluginsStart.licensing,
expressions: pluginsStart.expressions,
isServerless: !!pluginsStart.serverless,
fieldFormats: pluginsStart.fieldFormats,
lens: pluginsStart.lens,
fieldsMetadata: pluginsStart.fieldsMetadata,
});
},
});
return renderApp({
...coreStart,
actions: plugins.actions,
dashboard: pluginsStart.dashboard,
cloud: plugins.cloud,
data: pluginsStart.data,
dataViews: pluginsStart.dataViews,
dataViewEditor: pluginsStart.dataViewEditor,
charts: pluginsStart.charts,
alerting: pluginsStart.alerting,
spaces: pluginsStart.spaces,
unifiedSearch: pluginsStart.unifiedSearch,
isCloud: Boolean(plugins.cloud?.isCloudEnabled),
element: params.element,
theme: params.theme,
storage: new Storage(window.localStorage),
setBreadcrumbs: params.setBreadcrumbs,
history: params.history,
actionTypeRegistry,
ruleTypeRegistry,
kibanaFeatures,
licensing: pluginsStart.licensing,
expressions: pluginsStart.expressions,
isServerless: !!pluginsStart.serverless,
fieldFormats: pluginsStart.fieldFormats,
lens: pluginsStart.lens,
fieldsMetadata: pluginsStart.fieldsMetadata,
});
},
});
}
plugins.management.sections.section.insightsAndAlerting.registerApp({
id: CONNECTORS_PLUGIN_ID,

View file

@ -19,6 +19,9 @@ import {
const allowedExperimentalValues = getExperimentalAllowedValues();
export const configSchema = schema.object({
rules: schema.object({
enabled: schema.boolean({ defaultValue: true }),
}),
enableGeoTrackingThresholdAlert: schema.maybe(schema.boolean({ defaultValue: false })),
enableExperimental: schema.arrayOf(schema.string(), {
defaultValue: () => [],

View file

@ -14,6 +14,9 @@ export { DEFAULT_GROUPS, TIME_SERIES_BUCKET_SELECTOR_FIELD } from './data';
export const config: PluginConfigDescriptor<ConfigSchema> = {
exposeToBrowser: {
rules: {
enabled: true,
},
enableGeoTrackingThresholdAlert: true,
enableExperimental: true,
},

View file

@ -0,0 +1,19 @@
/*
* 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 { createTestConfig } from '../../common/config';
// eslint-disable-next-line import/no-default-export
export default createTestConfig('security_and_spaces', {
disabledPlugins: ['alerting.maintenanceWindow', 'alerting.rulesSettings'],
license: 'trial',
ssl: true,
enableActionsProxy: true,
publicBaseUrl: true,
testFiles: [require.resolve('./tests')],
useDedicatedTaskRunner: true,
});

View file

@ -0,0 +1,16 @@
/*
* 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 type { FtrProviderContext } from '../../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) {
describe('alerting api integration security and spaces disabled - Group 6', function () {
loadTestFile(require.resolve('./maintenance_window_disabled'));
loadTestFile(require.resolve('./rules_settings_disabled'));
});
}

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { setupSpacesAndUsers, tearDown } from '../../../setup';
// eslint-disable-next-line import/no-default-export
export default function maintenanceWindowDisabledTests({
loadTestFile,
getService,
}: FtrProviderContext) {
describe('Maintenance Window Disabled - Group 3', () => {
before(async () => {
await setupSpacesAndUsers(getService);
});
after(async () => {
await tearDown(getService);
});
loadTestFile(require.resolve('./maintenance_window_api_disabled'));
});
}

View file

@ -0,0 +1,152 @@
/*
* 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 expect from '@kbn/expect';
import { UserAtSpaceScenarios } from '../../../scenarios';
import { getUrlPrefix } from '../../../../common/lib';
import type { FtrProviderContext } from '../../../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function maintenanceWindowApiDisabledTests({ getService }: FtrProviderContext) {
const supertestWithoutAuth = getService('supertestWithoutAuth');
describe('maintenance window API disabled', () => {
const createParams = {
title: 'test-maintenance-window',
duration: 60 * 60 * 1000, // 1 hr
r_rule: {
dtstart: new Date().toISOString(),
tzid: 'UTC',
freq: 2, // weekly
},
};
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
describe(scenario.id, () => {
it('should return 404 when trying to create a maintenance window', async () => {
const response = await supertestWithoutAuth
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.send(createParams);
expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
});
});
it('should return 404 when trying to get a maintenance window', async () => {
const response = await supertestWithoutAuth
.get(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window/some-id`)
.auth(user.username, user.password);
expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
});
});
it('should return 404 when trying to update a maintenance window', async () => {
const response = await supertestWithoutAuth
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window/some-id`)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.send(createParams);
expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
});
});
it('should return 404 when trying to delete a maintenance window', async () => {
const response = await supertestWithoutAuth
.delete(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window/some-id`)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password);
expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
});
});
it('should return 404 when trying to archive a maintenance window', async () => {
const response = await supertestWithoutAuth
.post(
`${getUrlPrefix(
space.id
)}/internal/alerting/rules/maintenance_window/some-id/_archive`
)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password);
expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
});
});
it('should return 404 when trying to finish a maintenance window', async () => {
const response = await supertestWithoutAuth
.post(
`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window/some-id/_finish`
)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password);
expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
});
});
it('should return 404 when trying to find maintenance windows', async () => {
const response = await supertestWithoutAuth
.get(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window/_find`)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password);
expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
});
});
it('should return 404 when trying to get active maintenance windows', async () => {
const response = await supertestWithoutAuth
.get(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window/_active`)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password);
expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
});
});
});
}
});
}

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { FtrProviderContext } from '../../../../../common/ftr_provider_context';
import { setupSpacesAndUsers, tearDown } from '../../../setup';
// eslint-disable-next-line import/no-default-export
export default function rulesSettingsDisabledTests({
loadTestFile,
getService,
}: FtrProviderContext) {
describe('Rules Settings Disabled - Group 6', () => {
before(async () => {
await setupSpacesAndUsers(getService);
});
after(async () => {
await tearDown(getService);
});
loadTestFile(require.resolve('./rules_settings_api_disabled'));
});
}

View file

@ -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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { UserAtSpaceScenarios } from '../../../scenarios';
import { getUrlPrefix } from '../../../../common/lib';
import type { FtrProviderContext } from '../../../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function rulesSettingsApiDisabledTests({ getService }: FtrProviderContext) {
const supertestWithoutAuth = getService('supertestWithoutAuth');
describe('rules settings API disabled', () => {
const flappingSettingsParams = {
enabled: true,
look_back_window: 10,
status_change_threshold: 5,
};
const queryDelaySettingsParams = {
delay: 30,
};
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
describe(scenario.id, () => {
it('should return 404 when trying to get flapping settings', async () => {
const response = await supertestWithoutAuth
.get(`${getUrlPrefix(space.id)}/internal/alerting/rules/settings/_flapping`)
.auth(user.username, user.password);
expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
});
});
it('should return 404 when trying to update flapping settings', async () => {
const response = await supertestWithoutAuth
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/settings/_flapping`)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.send(flappingSettingsParams);
expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
});
});
it('should return 404 when trying to get query delay settings', async () => {
const response = await supertestWithoutAuth
.get(`${getUrlPrefix(space.id)}/internal/alerting/rules/settings/_query_delay`)
.auth(user.username, user.password);
expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
});
});
it('should return 404 when trying to update query delay settings', async () => {
const response = await supertestWithoutAuth
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/settings/_query_delay`)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.send(queryDelaySettingsParams);
expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
});
});
});
}
});
}

View file

@ -7,108 +7,110 @@
import { login } from '../../../tasks/login';
import { visit } from '../../../tasks/navigation';
import { ALERTS_SUMMARY_PROMPT, GET_STARTED_PAGE } from '../constants';
import { ALERT_SUMMARY_URL, ALERTS_URL, RULES_LANDING_URL } from '../../../urls/navigation';
import { ALERTS_SUMMARY_PROMPT, GET_STARTED_PAGE } from '../../../screens/ai_soc';
import {
ALERT_SUMMARY_URL,
ALERTS_URL,
MAINTENANCE_WINDOW_URL,
RULES_LANDING_URL,
STACK_RULES_URL,
} from '../../../urls/navigation';
const testPageAccess = () => {
it('should show page or redirect depending on capabilities', () => {
visit(ALERT_SUMMARY_URL);
cy.get(ALERTS_SUMMARY_PROMPT).should('exist');
// should redirect out from alerts to get started page
visit(ALERTS_URL);
cy.get(GET_STARTED_PAGE).should('exist');
// should redirect out from rules to get started page
visit(RULES_LANDING_URL);
cy.get(GET_STARTED_PAGE).should('exist');
});
};
const MANAGEMENT_PAGE_DESCRIPTION =
'Manage data and indices, oversee rules and connectors, organize saved objects and files, and create API keys in a central location.';
describe('Capabilities', { tags: '@serverless' }, () => {
describe('Admin user capabilities', () => {
beforeEach(() => {
login('admin');
});
testPageAccess();
});
describe('User with siem v1 role', () => {
const roleDescriptor = {
elasticsearch: {
indices: [
{
names: ['*'],
privileges: ['all'],
const userRoles = [
{
name: 'Admin user',
loginAs: 'admin',
setup: () => {}, // No additional setup needed
teardown: () => {}, // No teardown needed
},
{
name: 'User with siem v1 role',
loginAs: 'siemv1',
setup: () => {
cy.task('createServerlessCustomRole', {
roleDescriptor: {
elasticsearch: {
indices: [{ names: ['*'], privileges: ['all'] }],
},
kibana: [
{
feature: { siem: ['all'], fleet: ['all'] },
spaces: ['*'],
},
],
},
],
roleName: 'siemv1',
});
},
kibana: [
{
feature: {
siem: ['all'],
fleet: ['all'],
teardown: () => {
cy.task('deleteServerlessCustomRole', 'siemv1');
},
},
{
name: 'User with siem v2 role',
loginAs: 'siemV2',
setup: () => {
cy.task('createServerlessCustomRole', {
roleDescriptor: {
elasticsearch: {
indices: [{ names: ['*'], privileges: ['all'] }],
},
kibana: [
{
feature: { siemV2: ['all'], fleet: ['all'] },
spaces: ['*'],
},
],
},
spaces: ['*'],
},
],
};
roleName: 'siemV2',
});
},
teardown: () => {
cy.task('deleteServerlessCustomRole', 'siemV2');
},
},
];
before(() => {
cy.task('createServerlessCustomRole', {
roleDescriptor,
roleName: 'siemv1',
// Iterate through each user role
userRoles.forEach((role) => {
describe(`${role.name} capabilities`, () => {
before(() => role.setup());
beforeEach(() => {
login(role.loginAs);
});
after(() => role.teardown());
// Individual test cases with clear descriptions
it('should show alert summary prompt when visiting alert summary page', () => {
visit(ALERT_SUMMARY_URL);
cy.get(ALERTS_SUMMARY_PROMPT).should('exist');
});
it('should redirect from alerts to get started page', () => {
visit(ALERTS_URL);
cy.get(GET_STARTED_PAGE).should('exist');
});
it('should redirect from rules to get started page', () => {
visit(RULES_LANDING_URL);
cy.get(GET_STARTED_PAGE).should('exist');
});
it('should redirect from stack rules to main management page', () => {
visit(STACK_RULES_URL);
cy.contains(MANAGEMENT_PAGE_DESCRIPTION).should('exist');
});
it('should redirect from maintenance window to main management page', () => {
visit(MAINTENANCE_WINDOW_URL);
cy.contains(MANAGEMENT_PAGE_DESCRIPTION).should('exist');
});
});
beforeEach(() => {
login('siemv1');
});
after(() => {
cy.task('deleteServerlessCustomRole', 'siemv1');
});
testPageAccess();
});
describe('User with siem v2 role', () => {
const roleDescriptor = {
elasticsearch: {
indices: [
{
names: ['*'],
privileges: ['all'],
},
],
},
kibana: [
{
feature: {
siemV2: ['all'],
fleet: ['all'],
},
spaces: ['*'],
},
],
};
before(() => {
cy.task('createServerlessCustomRole', {
roleDescriptor,
roleName: 'siemV2',
});
});
beforeEach(() => {
login('siemV2');
});
after(() => {
cy.task('deleteServerlessCustomRole', 'siemV2');
});
testPageAccess();
});
});

View file

@ -4,8 +4,3 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const ALERTS_SUMMARY_PROMPT = '[data-test-subj="alert-summary-landing-page-prompt"]';
export const GET_STARTED_PAGE = '[data-test-subj="onboarding-hub-page"]';
export const AI_SOC_NAVIGATION =
'[data-test-subj="nav-item nav-item-security_solution_ai_nav nav-item-id-security_solution_ai_nav"]';

View file

@ -8,15 +8,11 @@
import { login } from '../../../tasks/login';
import { visit } from '../../../tasks/navigation';
import { GET_STARTED_URL } from '../../../urls/navigation';
import { AI_SOC_NAVIGATION } from '../constants';
import { AI_SOC_NAVIGATION } from '../../../screens/ai_soc';
const visibleLinks = ['discover', 'attack_discovery', 'case', 'alert_summary', 'configurations'];
const notVisibleLinks = [
// 'machine_learning-landing', -- TODO comment out when ML is turned off
'alerts',
'rules',
];
const notVisibleLinks = ['machine_learning-landing', 'alerts', 'rules'];
describe('AI$DSOC Navigation', { tags: '@serverless' }, () => {
beforeEach(() => {

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const ALERTS_SUMMARY_PROMPT = '[data-test-subj="alert-summary-landing-page-prompt"]';
export const GET_STARTED_PAGE = '[data-test-subj="onboarding-hub-page"]';
export const AI_SOC_NAVIGATION =
'[data-test-subj="nav-item nav-item-security_solution_ai_nav nav-item-id-security_solution_ai_nav"]';

View file

@ -88,3 +88,7 @@ export const ASSET_INVENTORY_URL = '/app/security/asset_inventory';
// Custom Role Creation
export const CUSTOM_ROLES_URL = 'app/management/security/roles/edit';
// Alerting
export const STACK_RULES_URL = 'app/management/insightsAndAlerting/triggersActions/rules';
export const MAINTENANCE_WINDOW_URL = 'app/management/insightsAndAlerting/maintenanceWindows';