Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][PoC] AI 4 SOC navigation approach 2 #212652

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export class FeatureRegistry {
for (const [featureId, featureOverride] of Object.entries(overrides)) {
const feature = this.kibanaFeatures[featureId];
if (!feature) {
continue;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be discussed with @elastic/kibana-security

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a few questions to better understand what you’re trying to achieve:

  1. What is the process for changing the product tier/line?
  2. Is it something a user can do themselves?
  3. Is the current tier stored somewhere in ES, or is it a config value?
  4. Does the change require a Kibana restart?
  5. In which offerings is it possible to dynamically update tiers/lines (ECH, ECS, on-prem)?

I vaguely remember that dynamic configuration was only possible in Elastic Cloud (or even only in Serverless?), where the configuration flag for the current tier lived in the Kibana config and was managed by infra (controller?). Is that still the case? If so, is there any reason the code that changes the tier config in the Kibana config couldn’t also alter overrides in the config?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To the best of my knowledge:

  1. the configuration is done through the Cloud UI.
  2. Good question: @angorayc do you know?
  3. 99% just the config value
  4. Yes
  5. Only serverless.

I think the overrides changes the capabilities, right? We're here trying to not even register the feature which makes the corresponding RBAC privileges not even visible for the user.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @azasypkin, thanks for taking a look. Sorry that this line is outdated, I should have removed this continue;, it's no longer needed. In the previous approach that I implemented I was skipping the registration of the siem/siemV2 feature when the new tier was enabled, in the current approach this feature is always registered, just with a different configuration for this new tier.

I vaguely remember that dynamic configuration was only possible in Elastic Cloud (or even only in Serverless?), where the configuration flag for the current tier lived in the Kibana config and was managed by infra (controller?). Is that still the case? If so, is there any reason the code that changes the tier config in the Kibana config couldn’t also alter overrides in the config?

You have a good memory. That's right. In serverless, we use the project controller to create the tiers and roles configuration that we read from Kibana. This approach worked well, and I think we should follow a similar approach. However, I think we should be careful and try to minimize the cross-repo dependencies we introduce, they have become problematic in different ways for the predefined role definitions: we have to keep duplicates in Kibana which are confusing and hard to maintain in sync with the production configs.

I think we should be able to keep the single source of truth inside Kibana, and use the project-controller as the decision maker. I was thinking about having a yaml file with different definitions of the features overrides, one per tier, or maybe having one file per tier would also make sense (e.g. security.essentials.override.yml, security.complete.overrides.yml...), inside Kibana. And the project-controller somehow applies one of them depending on the current tier. Or maybe Kibana itself should pick the correct override config, using the tiers config that is already provided by the project-controller.

wdyt?

cc @YulNaumenko

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the current approach this feature is always registered, just with a different configuration for this new tier.

Aha, good to know. That should definitely simplify things.

However, I think we should be careful and try to minimize the cross-repo dependencies we introduce, they have become problematic in different ways for the predefined role definitions: we have to keep duplicates in Kibana which are confusing and hard to maintain in sync with the production configs.

Agree. The only reason I mentioned controller is that I thought you were skipping the feature registration completely, and hence it would make sense for controller to remove overrides for the feature that doesn’t exist from the config as well. But since you changed your approach, we don’t need to do that.

I think we should be able to keep the single source of truth inside Kibana, and use the project-controller as the decision maker. I was thinking about having a yaml file with different definitions of the features overrides, one per tier, or maybe having one file per tier would also make sense (e.g. security.essentials.override.yml, security.complete.overrides.yml...), inside Kibana. And the project-controller somehow applies one of them depending on the current tier. Or maybe Kibana itself should pick the correct override config, using the tiers config that is already provided by the project-controller.
Here’s the proofread version of your text:

If I understand your current approach correctly, you’re just changing the feature definition at runtime depending on the tier. If so, it sounds much easier than doing this via the configuration like we did with feature overrides. If it fully works for you and doesn't require any nasty hacks (?), I think it’s better to keep what you have, but let’s discuss this during our sync later today to make sure I’m not missing anything.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@azasypkin

If I understand your current approach correctly, you’re just changing the feature definition at runtime depending on the tier. If so, it sounds much easier than doing this via the configuration like we did with feature overrides. If it fully works for you and doesn't require any nasty hacks (?), I think it’s better to keep what you have, but let’s discuss this during our sync later today to make sure I’m not missing anything.

Great! glad that you like the idea, but that's only for Security features.
The missing part is with other features outside the Security plugin, that we also need we need to disable/hide for this tier, such as Fleet, Osquery...
This is what we want to discuss with you, the proposal of having a different override configuration per tier is meant to solve this.

throw new Error(
`Cannot override feature "${featureId}" since feature with such ID is not registered.`
);
Expand Down
10 changes: 8 additions & 2 deletions x-pack/solutions/security/packages/features/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ export { securityDefaultProductFeaturesConfig } from './src/security/product_fea
export { getCasesDefaultProductFeaturesConfig } from './src/cases/product_feature_config';
export { assistantDefaultProductFeaturesConfig } from './src/assistant/product_feature_config';
export { attackDiscoveryDefaultProductFeaturesConfig } from './src/attack_discovery/product_feature_config';
export { timelineDefaultProductFeaturesConfig } from './src/timeline/product_feature_config';
export { notesDefaultProductFeaturesConfig } from './src/notes/product_feature_config';
export {
timelineDefaultProductFeaturesConfig,
nonTimelineDefaultProductFeaturesConfig,
} from './src/timeline/product_feature_config';
export {
notesDefaultProductFeaturesConfig,
nonNotesDefaultProductFeaturesConfig,
} from './src/notes/product_feature_config';
export { siemMigrationsDefaultProductFeaturesConfig } from './src/siem_migrations/product_feature_config';

export { createEnabledProductFeaturesConfigMap } from './src/helpers';
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const createEnabledProductFeaturesConfigMap = <
>(
productFeatures: Record<K, ProductFeatureKibanaConfig<T>>,
enabledProductFeaturesKeys: ProductFeatureKeys
) =>
): Map<K, ProductFeatureKibanaConfig<T>> =>
new Map(
Object.entries<ProductFeatureKibanaConfig<T>>(productFeatures).reduce<
Array<[K, ProductFeatureKibanaConfig<T>]>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,10 @@ export const notesDefaultProductFeaturesConfig: Record<
},
},
};

export const nonNotesDefaultProductFeaturesConfig: Record<
ProductFeatureNotesFeatureKey,
ProductFeatureKibanaConfig
> = {
[ProductFeatureNotesFeatureKey.notes]: {},
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
*/

export enum ProductFeatureSecurityKey {
/** Elastic endpoint detections, includes alerts, rules, investigations */
detections = 'detections',
/** External detections that don't use Elastic endpoint */
externalDetections = 'external_detections',
/** Enables Advanced Insights (Entity Risk, GenAI) */
advancedInsights = 'advanced_insights',
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,40 @@ import type { DefaultSecurityProductFeaturesConfig } from './types';
* - `subFeaturesPrivileges`: the privileges that will be added into the existing Security subFeature with the privilege `id` specified.
*/
export const securityDefaultProductFeaturesConfig: DefaultSecurityProductFeaturesConfig = {
[ProductFeatureSecurityKey.detections]: {
privileges: {
all: {
ui: ['show', 'crud'],
api: [
APP_ID,
'lists-all',
'lists-read',
'lists-summary',
'rac',
'cloud-security-posture-all',
'cloud-security-posture-read',
'cloud-defend-all',
'cloud-defend-read',
],
},
read: {
ui: ['show'],
api: [APP_ID, 'lists-read', 'rac', 'cloud-security-posture-read', 'cloud-defend-read'],
},
},
},
[ProductFeatureSecurityKey.externalDetections]: {
privileges: {
all: {
ui: ['alerts_summary', 'basic_rule_management'],
api: [`${APP_ID}-external`],
},
read: {
ui: ['alerts_summary_read', 'basic_rule_management_read'],
api: [`${APP_ID}-external`],
},
},
},
[ProductFeatureSecurityKey.advancedInsights]: {
privileges: {
all: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,21 +104,6 @@ export const getSecurityBaseKibanaFeature = ({
},
app: [APP_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, 'kibana'],
catalogue: [APP_ID],
api: [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: I’m not familiar enough with the mechanics here or whether these API privileges are needed, but we’re not making a breaking change for users who have roles with this privielge, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To best of my knowledge, this is safe. We are just moving these privileges to product_feature_config which also add's the possibility to be run against PLI configuration.
CC: @semd

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, this is safe. We are just moving things around to make them tier-dependent.

  • All existing tiers will have these privileges configured with the same API/UI actions as currently.
  • The new tier will have these privileges configured with a new set of API/UI actions specific to this tier.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Roger that, thanks for clarifying!

APP_ID,
'lists-all',
'lists-read',
'lists-summary',
'rac',
'cloud-security-posture-all',
'cloud-security-posture-read',
'cloud-defend-all',
'cloud-defend-read',
'timeline_write',
'timeline_read',
'notes_write',
'notes_read',
],
savedObject: {
all: ['alert', ...savedObjects],
read: [],
Expand All @@ -134,7 +119,9 @@ export const getSecurityBaseKibanaFeature = ({
management: {
insightsAndAlerting: ['triggersActions'],
},
ui: ['show', 'crud'],

api: [],
ui: [],
},
read: {
replacedBy: {
Expand All @@ -151,15 +138,6 @@ export const getSecurityBaseKibanaFeature = ({
},
app: [APP_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, 'kibana'],
catalogue: [APP_ID],
api: [
APP_ID,
'lists-read',
'rac',
'cloud-security-posture-read',
'cloud-defend-read',
'timeline_read',
'notes_read',
],
savedObject: {
all: [],
read: [...savedObjects],
Expand All @@ -175,7 +153,8 @@ export const getSecurityBaseKibanaFeature = ({
management: {
insightsAndAlerting: ['triggersActions'],
},
ui: ['show'],
api: [],
ui: [],
},
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,6 @@ export const getSecurityV2BaseKibanaFeature = ({
all: {
app: [APP_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, 'kibana'],
catalogue: [APP_ID],
api: [
APP_ID,
'lists-all',
'lists-read',
'lists-summary',
'rac',
'cloud-security-posture-all',
'cloud-security-posture-read',
'cloud-defend-all',
'cloud-defend-read',
],
savedObject: {
all: ['alert', ...savedObjects],
read: [],
Expand All @@ -99,12 +88,12 @@ export const getSecurityV2BaseKibanaFeature = ({
management: {
insightsAndAlerting: ['triggersActions'],
},
ui: ['show', 'crud'],
api: [],
ui: [],
},
read: {
app: [APP_ID, CLOUD_POSTURE_APP_ID, CLOUD_DEFEND_APP_ID, 'kibana'],
catalogue: [APP_ID],
api: [APP_ID, 'lists-read', 'rac', 'cloud-security-posture-read', 'cloud-defend-read'],
savedObject: {
all: [],
read: [...savedObjects],
Expand All @@ -120,7 +109,8 @@ export const getSecurityV2BaseKibanaFeature = ({
management: {
insightsAndAlerting: ['triggersActions'],
},
ui: ['show'],
api: [],
ui: [],
},
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,10 @@ export const timelineDefaultProductFeaturesConfig: Record<
},
},
};

export const nonTimelineDefaultProductFeaturesConfig: Record<
ProductFeatureTimelineFeatureKey,
ProductFeatureKibanaConfig
> = {
[ProductFeatureTimelineFeatureKey.timeline]: {},
};
16 changes: 9 additions & 7 deletions x-pack/solutions/security/packages/features/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,13 @@ export interface ProductFeatureParams<T extends string = string> {
}

export interface ProductFeaturesConfigurator {
security: () => ProductFeaturesConfig<SecuritySubFeatureId>;
cases: () => ProductFeaturesConfig<CasesSubFeatureId>;
securityAssistant: () => ProductFeaturesConfig<AssistantSubFeatureId>;
attackDiscovery: () => ProductFeaturesConfig;
timeline: () => ProductFeaturesConfig;
notes: () => ProductFeaturesConfig;
siemMigrations: () => ProductFeaturesConfig;
security?: () => ProductFeaturesConfig<SecuritySubFeatureId>;
cases?: () => ProductFeaturesConfig<CasesSubFeatureId>;
securityAssistant?: () => ProductFeaturesConfig<AssistantSubFeatureId>;
attackDiscovery?: () => ProductFeaturesConfig;
timeline?: () => ProductFeaturesConfig;
notes?: () => ProductFeaturesConfig;
siemMigrations?: () => ProductFeaturesConfig;
}

export type ProductFeatureConfiguratorName = keyof ProductFeaturesConfigurator;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
*/

import { SecurityPageName, ExternalPageName } from '@kbn/security-solution-navigation';
import { ASSETS_PATH, CLOUD_DEFEND_PATH } from '../../../../../common/constants';
import {
ASSETS_PATH,
ATTACK_DISCOVERY_FEATURE_ID,
CLOUD_DEFEND_PATH,
} from '../../../../../common/constants';
import { SECURITY_FEATURE_ID } from '../../../../../common';
import type { LinkItem } from '../../../../common/links/types';
import type { SolutionNavLink } from '../../../../common/links';
Expand All @@ -18,7 +22,7 @@ const assetsAppLink: LinkItem = {
id: SecurityPageName.assets,
title: i18n.ASSETS_TITLE,
path: ASSETS_PATH,
capabilities: [`${SECURITY_FEATURE_ID}.show`],
capabilities: [`${SECURITY_FEATURE_ID}.show`, `${ATTACK_DISCOVERY_FEATURE_ID}.attack-discovery`],
hideTimeline: true,
skipUrlState: true,
links: [], // endpoints and cloudDefend links are added in createAssetsLinkFromManage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,11 @@ import {
ATTACK_DISCOVERY_FEATURE_ID,
ATTACK_DISCOVERY_PATH,
SecurityPageName,
SECURITY_FEATURE_ID,
} from '../../common/constants';
import type { LinkItem } from '../common/links/types';

export const links: LinkItem = {
capabilities: [
[`${SECURITY_FEATURE_ID}.show`, `${ATTACK_DISCOVERY_FEATURE_ID}.attack-discovery`],
], // This is an AND condition via the nested array
capabilities: `${ATTACK_DISCOVERY_FEATURE_ID}.attack-discovery`,
globalNavPosition: 4,
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.attackDiscovery', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ export const UserPrivilegesProvider = ({
kibanaCapabilities,
children,
}: UserPrivilegesProviderProps) => {
const crud: boolean = kibanaCapabilities[SECURITY_FEATURE_ID].crud === true;
const read: boolean = kibanaCapabilities[SECURITY_FEATURE_ID].show === true;
const crud: boolean = kibanaCapabilities[SECURITY_FEATURE_ID]?.crud === true;
const read: boolean = kibanaCapabilities[SECURITY_FEATURE_ID]?.show === true;
const [kibanaSecuritySolutionsPrivileges, setKibanaSecuritySolutionsPrivileges] = useState({
crud,
read,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ export const getSubPluginRoutesByCapabilities = (

export const isSubPluginAvailable = (pluginKey: string, capabilities: Capabilities): boolean => {
if (CASES_SUB_PLUGIN_KEY === pluginKey) {
return capabilities[CASES_FEATURE_ID].read_cases === true;
return capabilities[CASES_FEATURE_ID]?.read_cases === true;
}
return hasAccessToSecuritySolution(capabilities);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
import type { Capabilities } from '@kbn/core/public';
import { SECURITY_FEATURE_ID } from '../common/constants';

export function hasAccessToSecuritySolution(capabilities: Capabilities) {
return (
export function hasAccessToSecuritySolution(capabilities: Capabilities): boolean {
return Boolean(
// Using `siemV2`
capabilities[SECURITY_FEATURE_ID]?.show === true
capabilities[SECURITY_FEATURE_ID]?.show ||
capabilities.securitySolutionCasesV2?.read_cases ||
capabilities.securitySolutionAttackDiscovery?.['attack-discovery']
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import React from 'react';
import { SECURITY_FEATURE_ID } from '../../../../../../common';
import type { OnboardingCardConfig } from '../../../../types';
import { OnboardingCardId } from '../../../../constants';
import { ALERTS_CARD_TITLE } from './translations';
Expand All @@ -22,4 +23,5 @@ export const alertsCardConfig: OnboardingCardConfig = {
'./alerts_card'
)
),
capabilitiesRequired: [`${SECURITY_FEATURE_ID}.show`],
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { checkIntegrationsCardComplete } from './integrations_check_complete';
import { OnboardingCardId } from '../../../../constants';
import type { IntegrationCardMetadata } from './types';
import { getCardIcon } from '../common/card_icon';
import { SECURITY_FEATURE_ID } from '../../../../../../common';

export const integrationsCardConfig: OnboardingCardConfig<IntegrationCardMetadata> = {
id: OnboardingCardId.integrations,
Expand All @@ -27,5 +28,5 @@ export const integrationsCardConfig: OnboardingCardConfig<IntegrationCardMetadat
)
),
checkComplete: checkIntegrationsCardComplete,
capabilitiesRequired: 'fleet.read',
capabilitiesRequired: [[`${SECURITY_FEATURE_ID}.show`, 'fleet.read']],
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { OnboardingCardConfig } from '../../../../types';
import { OnboardingCardId } from '../../../../constants';
import { RULES_CARD_TITLE } from './translations';
import { checkRulesComplete } from './rules_check_complete';
import { SECURITY_FEATURE_ID } from '../../../../../../common';
import { getCardIcon } from '../common/card_icon';

export const rulesCardConfig: OnboardingCardConfig = {
Expand All @@ -24,4 +25,5 @@ export const rulesCardConfig: OnboardingCardConfig = {
)
),
checkComplete: checkRulesComplete,
capabilitiesRequired: [`${SECURITY_FEATURE_ID}.show`],
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@
*/

import { i18n } from '@kbn/i18n';
import { ONBOARDING_PATH, SecurityPageName, SECURITY_FEATURE_ID } from '../../common/constants';
import { ONBOARDING_PATH, SecurityPageName } from '../../common/constants';
import { GETTING_STARTED } from '../app/translations';
import type { LinkItem } from '../common/links/types';

export const onboardingLinks: LinkItem = {
id: SecurityPageName.landing,
title: GETTING_STARTED,
path: ONBOARDING_PATH,
capabilities: [`${SECURITY_FEATURE_ID}.show`],
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.getStarted', {
defaultMessage: 'Getting started',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -416,16 +416,11 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
const { capabilities } = core.application;
const { upsellingService, isSolutionNavigationEnabled$ } = this.contract;

// When the user does not have access to SIEM (main Security feature) nor Security Cases feature, the plugin must be inaccessible.
if (
!hasAccessToSecuritySolution(capabilities) &&
!capabilities.securitySolutionCasesV2?.read_cases
) {
this.appUpdater$.next(() => ({
status: AppStatus.inaccessible,
visibleIn: [],
}));
// no need to register the links updater when the plugin is inaccessible
// When the user does not have any of the capabilities required to access security solution, the plugin should be inaccessible
// This is necessary to hide security solution from the selectable solutions in the spaces UI
if (!hasAccessToSecuritySolution(capabilities)) {
this.appUpdater$.next(() => ({ status: AppStatus.inaccessible, visibleIn: [] }));
// no need to register the links updater when the plugin is inaccessible. return early
return;
}

Expand Down
Loading