[Security Solution] Nav unified (#131157)

This commit is contained in:
Steph Milovic 2022-05-10 08:30:24 -06:00 committed by GitHub
parent 9044d1f76f
commit 4ef0f1e0b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1251 additions and 23 deletions

View file

@ -121,7 +121,6 @@ export enum SecurityPageName {
usersExternalAlerts = 'users-external_alerts',
threatHuntingLanding = 'threat-hunting',
dashboardsLanding = 'dashboards',
manageLanding = 'manage',
}
export const THREAT_HUNTING_PATH = '/threat_hunting' as const;

View file

@ -0,0 +1,36 @@
/*
* 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 { getCasesDeepLinks } from '@kbn/cases-plugin/public';
import { CASES_PATH, SecurityPageName } from '../../common/constants';
import { FEATURE, LinkItem } from '../common/links/types';
export const getCasesLinkItems = (): LinkItem => {
const casesLinks = getCasesDeepLinks<LinkItem>({
basePath: CASES_PATH,
extend: {
[SecurityPageName.case]: {
globalNavEnabled: true,
globalNavOrder: 9006,
features: [FEATURE.casesRead],
},
[SecurityPageName.caseConfigure]: {
features: [FEATURE.casesCrud],
licenseType: 'gold',
},
[SecurityPageName.caseCreate]: {
features: [FEATURE.casesCrud],
},
},
});
const { id, deepLinks, ...rest } = casesLinks;
return {
...rest,
id: SecurityPageName.case,
links: deepLinks as LinkItem[],
};
};

View file

@ -6,7 +6,7 @@
*/
import { UrlStateType } from '../url_state/constants';
import type { SecurityPageName } from '../../../app/types';
import { SecurityPageName } from '../../../app/types';
import { UrlState } from '../url_state/types';
import { SiemRouteType } from '../../utils/route/types';
@ -40,26 +40,27 @@ export interface NavTab {
pageId?: SecurityPageName;
isBeta?: boolean;
}
export type SecurityNavKey =
| SecurityPageName.administration
| SecurityPageName.alerts
| SecurityPageName.blocklist
| SecurityPageName.detectionAndResponse
| SecurityPageName.case
| SecurityPageName.endpoints
| SecurityPageName.landing
| SecurityPageName.policies
| SecurityPageName.eventFilters
| SecurityPageName.exceptions
| SecurityPageName.hostIsolationExceptions
| SecurityPageName.hosts
| SecurityPageName.network
| SecurityPageName.overview
| SecurityPageName.rules
| SecurityPageName.timelines
| SecurityPageName.trustedApps
| SecurityPageName.users;
export const securityNavKeys = [
SecurityPageName.administration,
SecurityPageName.alerts,
SecurityPageName.blocklist,
SecurityPageName.detectionAndResponse,
SecurityPageName.case,
SecurityPageName.endpoints,
SecurityPageName.landing,
SecurityPageName.policies,
SecurityPageName.eventFilters,
SecurityPageName.exceptions,
SecurityPageName.hostIsolationExceptions,
SecurityPageName.hosts,
SecurityPageName.network,
SecurityPageName.overview,
SecurityPageName.rules,
SecurityPageName.timelines,
SecurityPageName.trustedApps,
SecurityPageName.users,
] as const;
export type SecurityNavKey = typeof securityNavKeys[number];
export type SecurityNav = Record<SecurityNavKey, NavTab>;

View file

@ -0,0 +1,49 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { SecurityPageName, THREAT_HUNTING_PATH } from '../../../common/constants';
import { THREAT_HUNTING } from '../../app/translations';
import { FEATURE, LinkItem, UserPermissions } from './types';
import { links as hostsLinks } from '../../hosts/links';
import { links as detectionLinks } from '../../detections/links';
import { links as networkLinks } from '../../network/links';
import { links as usersLinks } from '../../users/links';
import { links as timelinesLinks } from '../../timelines/links';
import { getCasesLinkItems } from '../../cases/links';
import { links as managementLinks } from '../../management/links';
import { gettingStartedLinks, dashboardsLandingLinks } from '../../overview/links';
export const appLinks: Readonly<LinkItem[]> = Object.freeze([
gettingStartedLinks,
dashboardsLandingLinks,
detectionLinks,
{
id: SecurityPageName.threatHuntingLanding,
title: THREAT_HUNTING,
path: THREAT_HUNTING_PATH,
globalNavEnabled: false,
features: [FEATURE.general],
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.threatHunting', {
defaultMessage: 'Threat hunting',
}),
],
links: [hostsLinks, networkLinks, usersLinks],
},
timelinesLinks,
getCasesLinkItems(),
managementLinks,
]);
export const getAppLinks = async ({
enableExperimental,
license,
capabilities,
}: UserPermissions) => {
// OLM team, implement async behavior here
return appLinks;
};

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './links';

View file

@ -0,0 +1,421 @@
/*
* 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 {
getAncestorLinksInfo,
getDeepLinks,
getInitialDeepLinks,
getLinkInfo,
getNavLinkItems,
needsUrlState,
} from './links';
import { CASES_FEATURE_ID, SecurityPageName, SERVER_APP_ID } from '../../../common/constants';
import { Capabilities } from '@kbn/core/types';
import { AppDeepLink } from '@kbn/core/public';
import { mockGlobalState } from '../mock';
import { NavLinkItem } from './types';
import { LicenseType } from '@kbn/licensing-plugin/common/types';
import { LicenseService } from '../../../common/license';
const mockExperimentalDefaults = mockGlobalState.app.enableExperimental;
const mockCapabilities = {
[CASES_FEATURE_ID]: { read_cases: true, crud_cases: true },
[SERVER_APP_ID]: { show: true },
} as unknown as Capabilities;
const findDeepLink = (id: string, deepLinks: AppDeepLink[]): AppDeepLink | null =>
deepLinks.reduce((deepLinkFound: AppDeepLink | null, deepLink) => {
if (deepLinkFound !== null) {
return deepLinkFound;
}
if (deepLink.id === id) {
return deepLink;
}
if (deepLink.deepLinks) {
return findDeepLink(id, deepLink.deepLinks);
}
return null;
}, null);
const findNavLink = (id: SecurityPageName, navLinks: NavLinkItem[]): NavLinkItem | null =>
navLinks.reduce((deepLinkFound: NavLinkItem | null, deepLink) => {
if (deepLinkFound !== null) {
return deepLinkFound;
}
if (deepLink.id === id) {
return deepLink;
}
if (deepLink.links) {
return findNavLink(id, deepLink.links);
}
return null;
}, null);
// remove filter once new nav is live
const allPages = Object.values(SecurityPageName).filter(
(pageName) =>
pageName !== SecurityPageName.explore &&
pageName !== SecurityPageName.detections &&
pageName !== SecurityPageName.investigate
);
const casesPages = [
SecurityPageName.case,
SecurityPageName.caseConfigure,
SecurityPageName.caseCreate,
];
const featureFlagPages = [
SecurityPageName.detectionAndResponse,
SecurityPageName.hostsAuthentications,
SecurityPageName.hostsRisk,
SecurityPageName.usersRisk,
];
const premiumPages = [
SecurityPageName.caseConfigure,
SecurityPageName.hostsAnomalies,
SecurityPageName.networkAnomalies,
SecurityPageName.usersAnomalies,
SecurityPageName.detectionAndResponse,
SecurityPageName.hostsRisk,
SecurityPageName.usersRisk,
];
const nonCasesPages = allPages.reduce(
(acc: SecurityPageName[], p) =>
casesPages.includes(p) || featureFlagPages.includes(p) ? acc : [p, ...acc],
[]
);
const licenseBasicMock = jest.fn().mockImplementation((arg: LicenseType) => arg === 'basic');
const licensePremiumMock = jest.fn().mockReturnValue(true);
const mockLicense = {
isAtLeast: licensePremiumMock,
} as unknown as LicenseService;
describe('security app link helpers', () => {
beforeEach(() => {
mockLicense.isAtLeast = licensePremiumMock;
});
describe('getInitialDeepLinks', () => {
it('should return all pages in the app', () => {
const links = getInitialDeepLinks();
allPages.forEach((page) => expect(findDeepLink(page, links)).toBeTruthy());
});
});
describe('getDeepLinks', () => {
it('basicLicense should return only basic links', async () => {
mockLicense.isAtLeast = licenseBasicMock;
const links = await getDeepLinks({
enableExperimental: mockExperimentalDefaults,
license: mockLicense,
capabilities: mockCapabilities,
});
expect(findDeepLink(SecurityPageName.hostsAnomalies, links)).toBeFalsy();
allPages.forEach((page) => {
if (premiumPages.includes(page)) {
return expect(findDeepLink(page, links)).toBeFalsy();
}
if (featureFlagPages.includes(page)) {
// ignore feature flag pages
return;
}
expect(findDeepLink(page, links)).toBeTruthy();
});
});
it('platinumLicense should return all links', async () => {
const links = await getDeepLinks({
enableExperimental: mockExperimentalDefaults,
license: mockLicense,
capabilities: mockCapabilities,
});
allPages.forEach((page) => {
if (premiumPages.includes(page) && !featureFlagPages.includes(page)) {
return expect(findDeepLink(page, links)).toBeTruthy();
}
if (featureFlagPages.includes(page)) {
// ignore feature flag pages
return;
}
expect(findDeepLink(page, links)).toBeTruthy();
});
});
it('hideWhenExperimentalKey hides entry when key = true', async () => {
const links = await getDeepLinks({
enableExperimental: { ...mockExperimentalDefaults, usersEnabled: true },
license: mockLicense,
capabilities: mockCapabilities,
});
expect(findDeepLink(SecurityPageName.hostsAuthentications, links)).toBeFalsy();
});
it('hideWhenExperimentalKey shows entry when key = false', async () => {
const links = await getDeepLinks({
enableExperimental: { ...mockExperimentalDefaults, usersEnabled: false },
license: mockLicense,
capabilities: mockCapabilities,
});
expect(findDeepLink(SecurityPageName.hostsAuthentications, links)).toBeTruthy();
});
it('experimentalKey shows entry when key = false', async () => {
const links = await getDeepLinks({
enableExperimental: {
...mockExperimentalDefaults,
riskyHostsEnabled: false,
riskyUsersEnabled: false,
detectionResponseEnabled: false,
},
license: mockLicense,
capabilities: mockCapabilities,
});
expect(findDeepLink(SecurityPageName.hostsRisk, links)).toBeFalsy();
expect(findDeepLink(SecurityPageName.usersRisk, links)).toBeFalsy();
expect(findDeepLink(SecurityPageName.detectionAndResponse, links)).toBeFalsy();
});
it('experimentalKey shows entry when key = true', async () => {
const links = await getDeepLinks({
enableExperimental: {
...mockExperimentalDefaults,
riskyHostsEnabled: true,
riskyUsersEnabled: true,
detectionResponseEnabled: true,
},
license: mockLicense,
capabilities: mockCapabilities,
});
expect(findDeepLink(SecurityPageName.hostsRisk, links)).toBeTruthy();
expect(findDeepLink(SecurityPageName.usersRisk, links)).toBeTruthy();
expect(findDeepLink(SecurityPageName.detectionAndResponse, links)).toBeTruthy();
});
it('Removes siem features when siem capabilities are false', async () => {
const capabilities = {
...mockCapabilities,
[SERVER_APP_ID]: { show: false },
} as unknown as Capabilities;
const links = await getDeepLinks({
enableExperimental: mockExperimentalDefaults,
license: mockLicense,
capabilities,
});
nonCasesPages.forEach((page) => {
// investigate is active for both Cases and Timelines pages
if (page === SecurityPageName.investigate) {
return expect(findDeepLink(page, links)).toBeTruthy();
}
return expect(findDeepLink(page, links)).toBeFalsy();
});
casesPages.forEach((page) => expect(findDeepLink(page, links)).toBeTruthy());
});
it('Removes cases features when cases capabilities are false', async () => {
const capabilities = {
...mockCapabilities,
[CASES_FEATURE_ID]: { read_cases: false, crud_cases: false },
} as unknown as Capabilities;
const links = await getDeepLinks({
enableExperimental: mockExperimentalDefaults,
license: mockLicense,
capabilities,
});
nonCasesPages.forEach((page) => expect(findDeepLink(page, links)).toBeTruthy());
casesPages.forEach((page) => expect(findDeepLink(page, links)).toBeFalsy());
});
});
describe('getNavLinkItems', () => {
it('basicLicense should return only basic links', () => {
mockLicense.isAtLeast = licenseBasicMock;
const links = getNavLinkItems({
enableExperimental: mockExperimentalDefaults,
license: mockLicense,
capabilities: mockCapabilities,
});
expect(findNavLink(SecurityPageName.hostsAnomalies, links)).toBeFalsy();
allPages.forEach((page) => {
if (premiumPages.includes(page)) {
return expect(findNavLink(page, links)).toBeFalsy();
}
if (featureFlagPages.includes(page)) {
// ignore feature flag pages
return;
}
expect(findNavLink(page, links)).toBeTruthy();
});
});
it('platinumLicense should return all links', () => {
const links = getNavLinkItems({
enableExperimental: mockExperimentalDefaults,
license: mockLicense,
capabilities: mockCapabilities,
});
allPages.forEach((page) => {
if (premiumPages.includes(page) && !featureFlagPages.includes(page)) {
return expect(findNavLink(page, links)).toBeTruthy();
}
if (featureFlagPages.includes(page)) {
// ignore feature flag pages
return;
}
expect(findNavLink(page, links)).toBeTruthy();
});
});
it('hideWhenExperimentalKey hides entry when key = true', () => {
const links = getNavLinkItems({
enableExperimental: { ...mockExperimentalDefaults, usersEnabled: true },
license: mockLicense,
capabilities: mockCapabilities,
});
expect(findNavLink(SecurityPageName.hostsAuthentications, links)).toBeFalsy();
});
it('hideWhenExperimentalKey shows entry when key = false', () => {
const links = getNavLinkItems({
enableExperimental: { ...mockExperimentalDefaults, usersEnabled: false },
license: mockLicense,
capabilities: mockCapabilities,
});
expect(findNavLink(SecurityPageName.hostsAuthentications, links)).toBeTruthy();
});
it('experimentalKey shows entry when key = false', () => {
const links = getNavLinkItems({
enableExperimental: {
...mockExperimentalDefaults,
riskyHostsEnabled: false,
riskyUsersEnabled: false,
detectionResponseEnabled: false,
},
license: mockLicense,
capabilities: mockCapabilities,
});
expect(findNavLink(SecurityPageName.hostsRisk, links)).toBeFalsy();
expect(findNavLink(SecurityPageName.usersRisk, links)).toBeFalsy();
expect(findNavLink(SecurityPageName.detectionAndResponse, links)).toBeFalsy();
});
it('experimentalKey shows entry when key = true', () => {
const links = getNavLinkItems({
enableExperimental: {
...mockExperimentalDefaults,
riskyHostsEnabled: true,
riskyUsersEnabled: true,
detectionResponseEnabled: true,
},
license: mockLicense,
capabilities: mockCapabilities,
});
expect(findNavLink(SecurityPageName.hostsRisk, links)).toBeTruthy();
expect(findNavLink(SecurityPageName.usersRisk, links)).toBeTruthy();
expect(findNavLink(SecurityPageName.detectionAndResponse, links)).toBeTruthy();
});
it('Removes siem features when siem capabilities are false', () => {
const capabilities = {
...mockCapabilities,
[SERVER_APP_ID]: { show: false },
} as unknown as Capabilities;
const links = getNavLinkItems({
enableExperimental: mockExperimentalDefaults,
license: mockLicense,
capabilities,
});
nonCasesPages.forEach((page) => {
// investigate is active for both Cases and Timelines pages
if (page === SecurityPageName.investigate) {
return expect(findNavLink(page, links)).toBeTruthy();
}
return expect(findNavLink(page, links)).toBeFalsy();
});
casesPages.forEach((page) => expect(findNavLink(page, links)).toBeTruthy());
});
it('Removes cases features when cases capabilities are false', () => {
const capabilities = {
...mockCapabilities,
[CASES_FEATURE_ID]: { read_cases: false, crud_cases: false },
} as unknown as Capabilities;
const links = getNavLinkItems({
enableExperimental: mockExperimentalDefaults,
license: mockLicense,
capabilities,
});
nonCasesPages.forEach((page) => expect(findNavLink(page, links)).toBeTruthy());
casesPages.forEach((page) => expect(findNavLink(page, links)).toBeFalsy());
});
});
describe('getAncestorLinksInfo', () => {
it('finds flattened links for hosts', () => {
const hierarchy = getAncestorLinksInfo(SecurityPageName.hosts);
expect(hierarchy).toEqual([
{
features: ['siem.show'],
globalNavEnabled: false,
globalSearchKeywords: ['Threat hunting'],
id: 'threat-hunting',
path: '/threat_hunting',
title: 'Threat Hunting',
},
{
globalNavEnabled: true,
globalNavOrder: 9002,
globalSearchEnabled: true,
globalSearchKeywords: ['Hosts'],
id: 'hosts',
path: '/hosts',
title: 'Hosts',
},
]);
});
it('finds flattened links for uncommonProcesses', () => {
const hierarchy = getAncestorLinksInfo(SecurityPageName.uncommonProcesses);
expect(hierarchy).toEqual([
{
features: ['siem.show'],
globalNavEnabled: false,
globalSearchKeywords: ['Threat hunting'],
id: 'threat-hunting',
path: '/threat_hunting',
title: 'Threat Hunting',
},
{
globalNavEnabled: true,
globalNavOrder: 9002,
globalSearchEnabled: true,
globalSearchKeywords: ['Hosts'],
id: 'hosts',
path: '/hosts',
title: 'Hosts',
},
{
id: 'uncommon_processes',
path: '/hosts/uncommonProcesses',
title: 'Uncommon Processes',
},
]);
});
});
describe('needsUrlState', () => {
it('returns true when url state exists for page', () => {
const needsUrl = needsUrlState(SecurityPageName.hosts);
expect(needsUrl).toEqual(true);
});
it('returns false when url state does not exist for page', () => {
const needsUrl = needsUrlState(SecurityPageName.landing);
expect(needsUrl).toEqual(false);
});
});
describe('getLinkInfo', () => {
it('gets information for an individual link', () => {
const linkInfo = getLinkInfo(SecurityPageName.hosts);
expect(linkInfo).toEqual({
globalNavEnabled: true,
globalNavOrder: 9002,
globalSearchEnabled: true,
globalSearchKeywords: ['Hosts'],
id: 'hosts',
path: '/hosts',
title: 'Hosts',
});
});
});
});

View file

@ -0,0 +1,197 @@
/*
* 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 { AppDeepLink, AppNavLinkStatus, Capabilities } from '@kbn/core/public';
import { get } from 'lodash';
import { SecurityPageName } from '../../../common/constants';
import { appLinks, getAppLinks } from './app_links';
import {
Feature,
LinkInfo,
LinkItem,
NavLinkItem,
NormalizedLink,
NormalizedLinks,
UserPermissions,
} from './types';
const createDeepLink = (link: LinkItem, linkProps?: UserPermissions): AppDeepLink => ({
id: link.id,
path: link.path,
title: link.title,
...(link.links && link.links.length
? {
deepLinks: reduceLinks<AppDeepLink>({
links: link.links,
linkProps,
formatFunction: createDeepLink,
}),
}
: {}),
...(link.icon != null ? { euiIconType: link.icon } : {}),
...(link.image != null ? { icon: link.image } : {}),
...(link.globalSearchKeywords != null ? { keywords: link.globalSearchKeywords } : {}),
...(link.globalNavEnabled != null
? { navLinkStatus: link.globalNavEnabled ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden }
: {}),
...(link.globalNavOrder != null ? { order: link.globalNavOrder } : {}),
...(link.globalSearchEnabled != null ? { searchable: link.globalSearchEnabled } : {}),
});
const createNavLinkItem = (link: LinkItem, linkProps?: UserPermissions): NavLinkItem => ({
id: link.id,
path: link.path,
title: link.title,
...(link.description != null ? { description: link.description } : {}),
...(link.icon != null ? { icon: link.icon } : {}),
...(link.image != null ? { image: link.image } : {}),
...(link.links && link.links.length
? {
links: reduceLinks<NavLinkItem>({
links: link.links,
linkProps,
formatFunction: createNavLinkItem,
}),
}
: {}),
...(link.skipUrlState != null ? { skipUrlState: link.skipUrlState } : {}),
});
const hasFeaturesCapability = (
features: Feature[] | undefined,
capabilities: Capabilities
): boolean => {
if (!features) {
return true;
}
return features.some((featureKey) => get(capabilities, featureKey, false));
};
const isLinkAllowed = (link: LinkItem, linkProps?: UserPermissions) =>
!(
linkProps != null &&
// exclude link when license is basic and link is premium
((linkProps.license && !linkProps.license.isAtLeast(link.licenseType ?? 'basic')) ||
// exclude link when enableExperimental[hideWhenExperimentalKey] is enabled and link has hideWhenExperimentalKey
(link.hideWhenExperimentalKey != null &&
linkProps.enableExperimental[link.hideWhenExperimentalKey]) ||
// exclude link when enableExperimental[experimentalKey] is disabled and link has experimentalKey
(link.experimentalKey != null && !linkProps.enableExperimental[link.experimentalKey]) ||
// exclude link when link is not part of enabled feature capabilities
(linkProps.capabilities != null &&
!hasFeaturesCapability(link.features, linkProps.capabilities)))
);
export function reduceLinks<T>({
links,
linkProps,
formatFunction,
}: {
links: Readonly<LinkItem[]>;
linkProps?: UserPermissions;
formatFunction: (link: LinkItem, linkProps?: UserPermissions) => T;
}): T[] {
return links.reduce(
(deepLinks: T[], link: LinkItem) =>
isLinkAllowed(link, linkProps) ? [...deepLinks, formatFunction(link, linkProps)] : deepLinks,
[]
);
}
export const getInitialDeepLinks = (): AppDeepLink[] => {
return appLinks.map((link) => createDeepLink(link));
};
export const getDeepLinks = async ({
enableExperimental,
license,
capabilities,
}: UserPermissions): Promise<AppDeepLink[]> => {
const links = await getAppLinks({ enableExperimental, license, capabilities });
return reduceLinks<AppDeepLink>({
links,
linkProps: { enableExperimental, license, capabilities },
formatFunction: createDeepLink,
});
};
export const getNavLinkItems = ({
enableExperimental,
license,
capabilities,
}: UserPermissions): NavLinkItem[] =>
reduceLinks<NavLinkItem>({
links: appLinks,
linkProps: { enableExperimental, license, capabilities },
formatFunction: createNavLinkItem,
});
/**
* Recursive function to create the `NormalizedLinks` structure from a `LinkItem` array parameter
*/
const getNormalizedLinks = (
currentLinks: Readonly<LinkItem[]>,
parentId?: SecurityPageName
): NormalizedLinks => {
const result = currentLinks.reduce<Partial<NormalizedLinks>>(
(normalized, { links, ...currentLink }) => {
normalized[currentLink.id] = {
...currentLink,
parentId,
};
if (links && links.length > 0) {
Object.assign(normalized, getNormalizedLinks(links, currentLink.id));
}
return normalized;
},
{}
);
return result as NormalizedLinks;
};
/**
* Normalized indexed version of the global `links` array, referencing the parent by id, instead of having nested links children
*/
const normalizedLinks: Readonly<NormalizedLinks> = Object.freeze(getNormalizedLinks(appLinks));
/**
* Returns the `NormalizedLink` from a link id parameter.
* The object reference is frozen to make sure it is not mutated by the caller.
*/
const getNormalizedLink = (id: SecurityPageName): Readonly<NormalizedLink> =>
Object.freeze(normalizedLinks[id]);
/**
* Returns the `LinkInfo` from a link id parameter
*/
export const getLinkInfo = (id: SecurityPageName): LinkInfo => {
// discards the parentId and creates the linkInfo copy.
const { parentId, ...linkInfo } = getNormalizedLink(id);
return linkInfo;
};
/**
* Returns the `LinkInfo` of all the ancestors to the parameter id link, also included.
*/
export const getAncestorLinksInfo = (id: SecurityPageName): LinkInfo[] => {
const ancestors: LinkInfo[] = [];
let currentId: SecurityPageName | undefined = id;
while (currentId) {
const { parentId, ...linkInfo } = getNormalizedLink(currentId);
ancestors.push(linkInfo);
currentId = parentId;
}
return ancestors.reverse();
};
/**
* Returns `true` if the links needs to carry the application state in the url.
* Defaults to `true` if the `skipUrlState` property of the `LinkItem` is `undefined`.
*/
export const needsUrlState = (id: SecurityPageName): boolean => {
return !getNormalizedLink(id).skipUrlState;
};

View file

@ -0,0 +1,68 @@
/*
* 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 { Capabilities } from '@kbn/core/types';
import { LicenseType } from '@kbn/licensing-plugin/common/types';
import { LicenseService } from '../../../common/license';
import { ExperimentalFeatures } from '../../../common/experimental_features';
import { CASES_FEATURE_ID, SecurityPageName, SERVER_APP_ID } from '../../../common/constants';
export const FEATURE = {
general: `${SERVER_APP_ID}.show`,
casesRead: `${CASES_FEATURE_ID}.read_cases`,
casesCrud: `${CASES_FEATURE_ID}.crud_cases`,
};
export type Feature = Readonly<typeof FEATURE[keyof typeof FEATURE]>;
export interface UserPermissions {
enableExperimental: ExperimentalFeatures;
license?: LicenseService;
capabilities?: Capabilities;
}
export interface LinkItem {
description?: string;
disabled?: boolean; // default false
/**
* Displays deep link when feature flag is enabled.
*/
experimentalKey?: keyof ExperimentalFeatures;
features?: Feature[];
/**
* Hides deep link when feature flag is enabled.
*/
globalNavEnabled?: boolean; // default false
globalNavOrder?: number;
globalSearchEnabled?: boolean;
globalSearchKeywords?: string[];
hideWhenExperimentalKey?: keyof ExperimentalFeatures;
icon?: string;
id: SecurityPageName;
image?: string;
isBeta?: boolean;
licenseType?: LicenseType;
links?: LinkItem[];
path: string;
skipUrlState?: boolean; // defaults to false
title: string;
}
export interface NavLinkItem {
description?: string;
icon?: string;
id: SecurityPageName;
links?: NavLinkItem[];
image?: string;
path: string;
title: string;
skipUrlState?: boolean; // default to false
}
export type LinkInfo = Omit<LinkItem, 'links'>;
export type NormalizedLink = LinkInfo & { parentId?: SecurityPageName };
export type NormalizedLinks = Record<SecurityPageName, NormalizedLink>;

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { ALERTS_PATH, SecurityPageName } from '../../common/constants';
import { ALERTS } from '../app/translations';
import { LinkItem, FEATURE } from '../common/links/types';
export const links: LinkItem = {
id: SecurityPageName.alerts,
title: ALERTS,
path: ALERTS_PATH,
features: [FEATURE.general],
globalNavEnabled: true,
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.alerts', {
defaultMessage: 'Alerts',
}),
],
globalSearchEnabled: true,
globalNavOrder: 9001,
};

View file

@ -0,0 +1,79 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { HOSTS_PATH, SecurityPageName } from '../../common/constants';
import { HOSTS } from '../app/translations';
import { LinkItem } from '../common/links/types';
export const links: LinkItem = {
id: SecurityPageName.hosts,
title: HOSTS,
path: HOSTS_PATH,
globalNavEnabled: true,
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.hosts', {
defaultMessage: 'Hosts',
}),
],
globalSearchEnabled: true,
globalNavOrder: 9002,
links: [
{
id: SecurityPageName.hostsAuthentications,
title: i18n.translate('xpack.securitySolution.appLinks.hosts.authentications', {
defaultMessage: 'Authentications',
}),
path: `${HOSTS_PATH}/authentications`,
hideWhenExperimentalKey: 'usersEnabled',
},
{
id: SecurityPageName.uncommonProcesses,
title: i18n.translate('xpack.securitySolution.appLinks.hosts.uncommonProcesses', {
defaultMessage: 'Uncommon Processes',
}),
path: `${HOSTS_PATH}/uncommonProcesses`,
},
{
id: SecurityPageName.hostsAnomalies,
title: i18n.translate('xpack.securitySolution.appLinks.hosts.anomalies', {
defaultMessage: 'Anomalies',
}),
path: `${HOSTS_PATH}/anomalies`,
licenseType: 'gold',
},
{
id: SecurityPageName.hostsEvents,
title: i18n.translate('xpack.securitySolution.appLinks.hosts.events', {
defaultMessage: 'Events',
}),
path: `${HOSTS_PATH}/events`,
},
{
id: SecurityPageName.hostsExternalAlerts,
title: i18n.translate('xpack.securitySolution.appLinks.hosts.externalAlerts', {
defaultMessage: 'External Alerts',
}),
path: `${HOSTS_PATH}/externalAlerts`,
},
{
id: SecurityPageName.hostsRisk,
title: i18n.translate('xpack.securitySolution.appLinks.hosts.risk', {
defaultMessage: 'Hosts by risk',
}),
path: `${HOSTS_PATH}/hostRisk`,
experimentalKey: 'riskyHostsEnabled',
},
{
id: SecurityPageName.sessions,
title: i18n.translate('xpack.securitySolution.appLinks.hosts.sessions', {
defaultMessage: 'Sessions',
}),
path: `${HOSTS_PATH}/sessions`,
isBeta: true,
},
],
};

View file

@ -27,7 +27,7 @@ export const DashboardRoutes = () => (
);
export const ManageRoutes = () => (
<TrackApplicationView viewId={SecurityPageName.manageLanding}>
<TrackApplicationView viewId={SecurityPageName.administration}>
<ManageLandingPage />
</TrackApplicationView>
);

View file

@ -0,0 +1,111 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import {
BLOCKLIST_PATH,
ENDPOINTS_PATH,
EVENT_FILTERS_PATH,
EXCEPTIONS_PATH,
HOST_ISOLATION_EXCEPTIONS_PATH,
MANAGEMENT_PATH,
POLICIES_PATH,
RULES_PATH,
SecurityPageName,
TRUSTED_APPS_PATH,
} from '../../common/constants';
import {
BLOCKLIST,
ENDPOINTS,
EVENT_FILTERS,
EXCEPTIONS,
HOST_ISOLATION_EXCEPTIONS,
MANAGE,
POLICIES,
RULES,
TRUSTED_APPLICATIONS,
} from '../app/translations';
import { FEATURE, LinkItem } from '../common/links/types';
export const links: LinkItem = {
id: SecurityPageName.administration,
title: MANAGE,
path: MANAGEMENT_PATH,
skipUrlState: true,
globalNavEnabled: false,
features: [FEATURE.general],
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.manage', {
defaultMessage: 'Manage',
}),
],
links: [
{
id: SecurityPageName.rules,
title: RULES,
path: RULES_PATH,
globalNavEnabled: false,
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.rules', {
defaultMessage: 'Rules',
}),
],
globalSearchEnabled: true,
},
{
id: SecurityPageName.exceptions,
title: EXCEPTIONS,
path: EXCEPTIONS_PATH,
globalNavEnabled: false,
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.exceptions', {
defaultMessage: 'Exception lists',
}),
],
globalSearchEnabled: true,
},
{
id: SecurityPageName.endpoints,
globalNavEnabled: true,
title: ENDPOINTS,
globalNavOrder: 9006,
path: ENDPOINTS_PATH,
skipUrlState: true,
},
{
id: SecurityPageName.policies,
title: POLICIES,
path: POLICIES_PATH,
skipUrlState: true,
experimentalKey: 'policyListEnabled',
},
{
id: SecurityPageName.trustedApps,
title: TRUSTED_APPLICATIONS,
path: TRUSTED_APPS_PATH,
skipUrlState: true,
},
{
id: SecurityPageName.eventFilters,
title: EVENT_FILTERS,
path: EVENT_FILTERS_PATH,
skipUrlState: true,
},
{
id: SecurityPageName.hostIsolationExceptions,
title: HOST_ISOLATION_EXCEPTIONS,
path: HOST_ISOLATION_EXCEPTIONS_PATH,
skipUrlState: true,
},
{
id: SecurityPageName.blocklist,
title: BLOCKLIST,
path: BLOCKLIST_PATH,
skipUrlState: true,
},
],
};

View file

@ -0,0 +1,62 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { NETWORK_PATH, SecurityPageName } from '../../common/constants';
import { NETWORK } from '../app/translations';
import { LinkItem } from '../common/links/types';
export const links: LinkItem = {
id: SecurityPageName.network,
title: NETWORK,
path: NETWORK_PATH,
globalNavEnabled: true,
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.network', {
defaultMessage: 'Network',
}),
],
globalNavOrder: 9003,
links: [
{
id: SecurityPageName.networkDns,
title: i18n.translate('xpack.securitySolution.appLinks.network.dns', {
defaultMessage: 'DNS',
}),
path: `${NETWORK_PATH}/dns`,
},
{
id: SecurityPageName.networkHttp,
title: i18n.translate('xpack.securitySolution.appLinks.network.http', {
defaultMessage: 'HTTP',
}),
path: `${NETWORK_PATH}/http`,
},
{
id: SecurityPageName.networkTls,
title: i18n.translate('xpack.securitySolution.appLinks.network.tls', {
defaultMessage: 'TLS',
}),
path: `${NETWORK_PATH}/tls`,
},
{
id: SecurityPageName.networkExternalAlerts,
title: i18n.translate('xpack.securitySolution.appLinks.network.externalAlerts', {
defaultMessage: 'External Alerts',
}),
path: `${NETWORK_PATH}/external-alerts`,
},
{
id: SecurityPageName.networkAnomalies,
title: i18n.translate('xpack.securitySolution.appLinks.hosts.anomalies', {
defaultMessage: 'Anomalies',
}),
path: `${NETWORK_PATH}/anomalies`,
licenseType: 'gold',
},
],
};

View file

@ -0,0 +1,73 @@
/*
* 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 { i18n } from '@kbn/i18n';
import {
DASHBOARDS_PATH,
DETECTION_RESPONSE_PATH,
LANDING_PATH,
OVERVIEW_PATH,
SecurityPageName,
} from '../../common/constants';
import { DASHBOARDS, DETECTION_RESPONSE, GETTING_STARTED, OVERVIEW } from '../app/translations';
import { FEATURE, LinkItem } from '../common/links/types';
export const overviewLinks: LinkItem = {
id: SecurityPageName.overview,
title: OVERVIEW,
path: OVERVIEW_PATH,
globalNavEnabled: true,
features: [FEATURE.general],
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.overview', {
defaultMessage: 'Overview',
}),
],
globalNavOrder: 9000,
};
export const gettingStartedLinks: LinkItem = {
id: SecurityPageName.landing,
title: GETTING_STARTED,
path: LANDING_PATH,
globalNavEnabled: false,
features: [FEATURE.general],
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.getStarted', {
defaultMessage: 'Getting started',
}),
],
skipUrlState: true,
};
export const detectionResponseLinks: LinkItem = {
id: SecurityPageName.detectionAndResponse,
title: DETECTION_RESPONSE,
path: DETECTION_RESPONSE_PATH,
globalNavEnabled: false,
experimentalKey: 'detectionResponseEnabled',
features: [FEATURE.general],
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.detectionAndResponse', {
defaultMessage: 'Detection & Response',
}),
],
};
export const dashboardsLandingLinks: LinkItem = {
id: SecurityPageName.dashboardsLanding,
title: DASHBOARDS,
path: DASHBOARDS_PATH,
globalNavEnabled: false,
features: [FEATURE.general],
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.dashboards', {
defaultMessage: 'Dashboards',
}),
],
links: [overviewLinks, detectionResponseLinks],
};

View file

@ -222,6 +222,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
}
licenseService.start(plugins.licensing.license$);
const licensing = licenseService.getLicenseInformation$();
/**
* Register deepLinks and pass an appUpdater for each subPlugin, to change deepLinks as needed when licensing changes.
*/

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { SecurityPageName, TIMELINES_PATH } from '../../common/constants';
import { TIMELINES } from '../app/translations';
import { FEATURE, LinkItem } from '../common/links/types';
export const links: LinkItem = {
id: SecurityPageName.timelines,
title: TIMELINES,
path: TIMELINES_PATH,
globalNavEnabled: true,
features: [FEATURE.general],
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.timelines', {
defaultMessage: 'Timelines',
}),
],
globalNavOrder: 9005,
links: [
{
id: SecurityPageName.timelinesTemplates,
title: i18n.translate('xpack.securitySolution.appLinks.timeline.templates', {
defaultMessage: 'Templates',
}),
path: `${TIMELINES_PATH}/template`,
},
],
};

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { SecurityPageName, USERS_PATH } from '../../common/constants';
import { USERS } from '../app/translations';
import { LinkItem } from '../common/links/types';
export const links: LinkItem = {
id: SecurityPageName.users,
title: USERS,
path: USERS_PATH,
globalNavEnabled: true,
experimentalKey: 'usersEnabled',
globalSearchKeywords: [
i18n.translate('xpack.securitySolution.appLinks.users', {
defaultMessage: 'Users',
}),
],
globalNavOrder: 9004,
links: [
{
id: SecurityPageName.usersAuthentications,
title: i18n.translate('xpack.securitySolution.appLinks.users.authentications', {
defaultMessage: 'Authentications',
}),
path: `${USERS_PATH}/authentications`,
},
{
id: SecurityPageName.usersAnomalies,
title: i18n.translate('xpack.securitySolution.appLinks.users.anomalies', {
defaultMessage: 'Anomalies',
}),
path: `${USERS_PATH}/anomalies`,
licenseType: 'gold',
},
{
id: SecurityPageName.usersRisk,
title: i18n.translate('xpack.securitySolution.appLinks.users.risk', {
defaultMessage: 'Users by risk',
}),
path: `${USERS_PATH}/userRisk`,
experimentalKey: 'riskyUsersEnabled',
},
{
id: SecurityPageName.usersEvents,
title: i18n.translate('xpack.securitySolution.appLinks.users.events', {
defaultMessage: 'Events',
}),
path: `${USERS_PATH}/events`,
},
{
id: SecurityPageName.usersExternalAlerts,
title: i18n.translate('xpack.securitySolution.appLinks.users.externalAlerts', {
defaultMessage: 'External Alerts',
}),
path: `${USERS_PATH}/externalAlerts`,
},
],
};