diff --git a/packages/kbn-utils/src/path/index.test.ts b/packages/kbn-utils/src/path/index.test.ts index 307d47af9ac5..e4c80a0783b5 100644 --- a/packages/kbn-utils/src/path/index.test.ts +++ b/packages/kbn-utils/src/path/index.test.ts @@ -7,10 +7,17 @@ */ import { accessSync, constants } from 'fs'; -import { createAbsolutePathSerializer } from '@kbn/dev-utils'; import { getConfigPath, getDataPath, getLogsPath, getConfigDirectory } from './'; - -expect.addSnapshotSerializer(createAbsolutePathSerializer()); +import { REPO_ROOT } from '../repo_root'; + +expect.addSnapshotSerializer( + ((rootPath: string = REPO_ROOT, replacement = '') => { + return { + test: (value: any) => typeof value === 'string' && value.startsWith(rootPath), + serialize: (value: string) => value.replace(rootPath, replacement).replace(/\\/g, '/'), + }; + })() +); describe('Default path finder', () => { it('should expose a path to the config directory', () => { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_failed_transaction_rate.ts similarity index 94% rename from x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts rename to x-pack/plugins/apm/server/lib/transaction_groups/get_failed_transaction_rate.ts index e1dde61bfc3f..b4f2c4b4bee1 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_failed_transaction_rate.ts @@ -32,7 +32,7 @@ import { getFailedTransactionRateTimeSeries, } from '../helpers/transaction_error_rate'; -export async function getErrorRate({ +export async function getFailedTransactionRate({ environment, kuery, serviceName, @@ -122,7 +122,7 @@ export async function getErrorRate({ return { timeseries, average }; } -export async function getErrorRatePeriods({ +export async function getFailedTransactionRatePeriods({ environment, kuery, serviceName, @@ -157,11 +157,15 @@ export async function getErrorRatePeriods({ searchAggregatedTransactions, }; - const currentPeriodPromise = getErrorRate({ ...commonProps, start, end }); + const currentPeriodPromise = getFailedTransactionRate({ + ...commonProps, + start, + end, + }); const previousPeriodPromise = comparisonStart && comparisonEnd - ? getErrorRate({ + ? getFailedTransactionRate({ ...commonProps, start: comparisonStart, end: comparisonEnd, diff --git a/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts index ad2ab74098c2..545fb4dbc460 100644 --- a/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts @@ -29,7 +29,7 @@ import { getDurationFieldForTransactions, getProcessorEventForTransactions, } from '../../lib/helpers/transactions'; -import { getErrorRate } from '../../lib/transaction_groups/get_error_rate'; +import { getFailedTransactionRate } from '../../lib/transaction_groups/get_failed_transaction_rate'; import { withApmSpan } from '../../utils/with_apm_span'; import { percentCgroupMemoryUsedScript, @@ -123,7 +123,7 @@ async function getFailedTransactionsRateStats({ numBuckets, }: TaskParameters): Promise { return withApmSpan('get_error_rate_for_service_map_node', async () => { - const { average, timeseries } = await getErrorRate({ + const { average, timeseries } = await getFailedTransactionRate({ environment, setup, serviceName, diff --git a/x-pack/plugins/apm/server/routes/transactions/route.ts b/x-pack/plugins/apm/server/routes/transactions/route.ts index fb73fe155596..b9db2762bce9 100644 --- a/x-pack/plugins/apm/server/routes/transactions/route.ts +++ b/x-pack/plugins/apm/server/routes/transactions/route.ts @@ -19,7 +19,7 @@ import { getServiceTransactionGroupDetailedStatisticsPeriods } from '../services import { getTransactionBreakdown } from './breakdown'; import { getTransactionTraceSamples } from './trace_samples'; import { getLatencyPeriods } from './get_latency_charts'; -import { getErrorRatePeriods } from '../../lib/transaction_groups/get_error_rate'; +import { getFailedTransactionRatePeriods } from '../../lib/transaction_groups/get_failed_transaction_rate'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { createApmServerRouteRepository } from '../apm_routes/create_apm_server_route_repository'; import { @@ -349,7 +349,7 @@ const transactionChartsErrorRateRoute = createApmServerRoute({ end, }); - return getErrorRatePeriods({ + return getFailedTransactionRatePeriods({ environment, kuery, serviceName, diff --git a/x-pack/plugins/fleet/common/index.ts b/x-pack/plugins/fleet/common/index.ts index 611e15032385..46a8e2d01fc9 100644 --- a/x-pack/plugins/fleet/common/index.ts +++ b/x-pack/plugins/fleet/common/index.ts @@ -13,3 +13,4 @@ export * from './services'; export * from './types'; export type { FleetAuthz } from './authz'; export { calculateAuthz } from './authz'; +export { createFleetAuthzMock } from './mocks'; diff --git a/x-pack/plugins/fleet/common/mocks.ts b/x-pack/plugins/fleet/common/mocks.ts index eb81ea2d6a0a..5b71e9b15860 100644 --- a/x-pack/plugins/fleet/common/mocks.ts +++ b/x-pack/plugins/fleet/common/mocks.ts @@ -5,7 +5,8 @@ * 2.0. */ -import type { NewPackagePolicy, PackagePolicy, DeletePackagePoliciesResponse } from './types'; +import type { DeletePackagePoliciesResponse, NewPackagePolicy, PackagePolicy } from './types'; +import type { FleetAuthz } from './authz'; export const createNewPackagePolicyMock = (): NewPackagePolicy => { return { @@ -56,3 +57,27 @@ export const deletePackagePolicyMock = (): DeletePackagePoliciesResponse => { }, ]; }; + +/** + * Creates mock `authz` object + */ +export const createFleetAuthzMock = (): FleetAuthz => { + return { + fleet: { + all: true, + setup: true, + readEnrollmentTokens: true, + }, + integrations: { + readPackageInfo: true, + readInstalledPackages: true, + installPackages: true, + upgradePackages: true, + removePackages: true, + readPackageSettings: true, + writePackageSettings: true, + readIntegrationPolicies: true, + writeIntegrationPolicies: true, + }, + }; +}; diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index 90a0addfae49..90c9181b5007 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -7,11 +7,11 @@ import { of } from 'rxjs'; import { + coreMock, elasticsearchServiceMock, loggingSystemMock, - savedObjectsServiceMock, - coreMock, savedObjectsClientMock, + savedObjectsServiceMock, } from '../../../../../src/core/server/mocks'; import { dataPluginMock } from '../../../../../src/plugins/data/server/mocks'; import { licensingMock } from '../../../../plugins/licensing/server/mocks'; @@ -21,7 +21,7 @@ import type { PackagePolicyServiceInterface } from '../services/package_policy'; import type { AgentPolicyServiceInterface, PackageService } from '../services'; import type { FleetAppContext } from '../plugin'; import { createMockTelemetryEventsSender } from '../telemetry/__mocks__'; -import type { FleetAuthz } from '../../common'; +import { createFleetAuthzMock } from '../../common'; import { agentServiceMock } from '../services/agents/agent_service.mock'; import type { FleetRequestHandlerContext } from '../types'; @@ -145,27 +145,3 @@ export const createMockPackageService = (): PackageService => { ensureInstalledPackage: jest.fn(), }; }; - -/** - * Creates mock `authz` object - */ -export const createFleetAuthzMock = (): FleetAuthz => { - return { - fleet: { - all: true, - setup: true, - readEnrollmentTokens: true, - }, - integrations: { - readPackageInfo: true, - readInstalledPackages: true, - installPackages: true, - upgradePackages: true, - removePackages: true, - readPackageSettings: true, - writePackageSettings: true, - readIntegrationPolicies: true, - writeIntegrationPolicies: true, - }, - }; -}; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 4b45cf645201..1e421fefce83 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -6,6 +6,7 @@ */ import type { Observable } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; import type { CoreSetup, CoreStart, @@ -16,13 +17,18 @@ import type { SavedObjectsServiceStart, HttpServiceSetup, KibanaRequest, + ServiceStatus, ElasticsearchClient, } from 'kibana/server'; import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import type { TelemetryPluginSetup, TelemetryPluginStart } from 'src/plugins/telemetry/server'; -import { DEFAULT_APP_CATEGORIES, SavedObjectsClient } from '../../../../src/core/server'; +import { + DEFAULT_APP_CATEGORIES, + SavedObjectsClient, + ServiceStatusLevels, +} from '../../../../src/core/server'; import type { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; import type { LicensingPluginSetup, ILicense } from '../../licensing/server'; import type { @@ -182,6 +188,7 @@ export class FleetPlugin private securitySetup?: SecurityPluginSetup; private encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup; private readonly telemetryEventsSender: TelemetryEventsSender; + private readonly fleetStatus$: BehaviorSubject; private agentService?: AgentService; @@ -193,6 +200,11 @@ export class FleetPlugin this.logger = this.initializerContext.logger.get(); this.configInitialValue = this.initializerContext.config.get(); this.telemetryEventsSender = new TelemetryEventsSender(this.logger.get('telemetry_events')); + + this.fleetStatus$ = new BehaviorSubject({ + level: ServiceStatusLevels.unavailable, + summary: 'Fleet is unavailable', + }); } public setup(core: CoreSetup, deps: FleetSetupDeps) { @@ -203,6 +215,8 @@ export class FleetPlugin this.securitySetup = deps.security; const config = this.configInitialValue; + core.status.set(this.fleetStatus$.asObservable()); + registerSavedObjects(core.savedObjects, deps.encryptedSavedObjects); registerEncryptedSavedObjects(deps.encryptedSavedObjects); @@ -357,13 +371,33 @@ export class FleetPlugin const fleetSetupPromise = (async () => { try { + this.fleetStatus$.next({ + level: ServiceStatusLevels.degraded, + summary: 'Fleet is setting up', + }); + await setupFleet( new SavedObjectsClient(core.savedObjects.createInternalRepository()), core.elasticsearch.client.asInternalUser ); + + this.fleetStatus$.next({ + level: ServiceStatusLevels.available, + summary: 'Fleet is available', + }); } catch (error) { logger.warn('Fleet setup failed'); logger.warn(error); + + this.fleetStatus$.next({ + // As long as Fleet has a dependency on EPR, we can't reliably set Kibana status to `unavailable` here. + // See https://github.com/elastic/kibana/issues/120237 + level: ServiceStatusLevels.available, + summary: 'Fleet setup failed', + meta: { + error: error.message, + }, + }); } })(); @@ -400,6 +434,7 @@ export class FleetPlugin appContextService.stop(); licenseService.stop(); this.telemetryEventsSender.stop(); + this.fleetStatus$.complete(); } private setupAgentService(internalEsClient: ElasticsearchClient): AgentService { diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts index d48d80add243..035659185955 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts @@ -9,12 +9,14 @@ import { httpServerMock, savedObjectsClientMock } from 'src/core/server/mocks'; import type { PostFleetSetupResponse } from '../../../common'; import { RegistryError } from '../../errors'; -import { createAppContextStartContractMock, xpackMocks, createFleetAuthzMock } from '../../mocks'; +import { createAppContextStartContractMock, xpackMocks } from '../../mocks'; import { agentServiceMock } from '../../services/agents/agent_service.mock'; import { appContextService } from '../../services/app_context'; import { setupFleet } from '../../services/setup'; import type { FleetRequestHandlerContext } from '../../types'; +import { createFleetAuthzMock } from '../../../common'; + import { fleetSetupHandler } from './handlers'; jest.mock('../../services/setup', () => { diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx index f67a9df4a4a8..4b0d8cdc5509 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx @@ -10,6 +10,8 @@ import React, { useState, useEffect, useCallback } from 'react'; import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup, EuiFlyout } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import './_index.scss'; + import { Dictionary } from '../../../../common/types/common'; import { useUrlState } from '../../util/url_state'; // @ts-ignore diff --git a/x-pack/plugins/security_solution/common/cti/constants.ts b/x-pack/plugins/security_solution/common/cti/constants.ts index 7a88b065d870..b33541c5057d 100644 --- a/x-pack/plugins/security_solution/common/cti/constants.ts +++ b/x-pack/plugins/security_solution/common/cti/constants.ts @@ -58,5 +58,14 @@ export const EVENT_ENRICHMENT_INDICATOR_FIELD_MAP = { export const DEFAULT_EVENT_ENRICHMENT_FROM = 'now-30d'; export const DEFAULT_EVENT_ENRICHMENT_TO = 'now'; -export const TI_INTEGRATION_PREFIX = 'ti'; -export const OTHER_TI_DATASET_KEY = '_others_ti_'; +export const CTI_DATASET_KEY_MAP: { [key: string]: string } = { + 'AbuseCH URL': 'ti_abusech.url', + 'AbuseCH Malware': 'ti_abusech.malware', + 'AbuseCH MalwareBazaar': 'ti_abusech.malwarebazaar', + 'AlienVault OTX': 'ti_otx.threat', + 'Anomali Limo': 'ti_anomali.limo', + 'Anomali Threatstream': 'ti_anomali.threatstream', + MISP: 'ti_misp.threat', + ThreatQuotient: 'ti_threatq.threat', + Cybersixgill: 'ti_cybersixgill.threat', +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts new file mode 100644 index 000000000000..588366036932 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts @@ -0,0 +1,75 @@ +/* + * 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 { calculateEndpointAuthz, getEndpointAuthzInitialState } from './authz'; +import { createFleetAuthzMock, FleetAuthz } from '../../../../../fleet/common'; +import { createLicenseServiceMock } from '../../../license/mocks'; +import type { EndpointAuthz } from '../../types/authz'; + +describe('Endpoint Authz service', () => { + let licenseService: ReturnType; + let fleetAuthz: FleetAuthz; + + beforeEach(() => { + licenseService = createLicenseServiceMock(); + fleetAuthz = createFleetAuthzMock(); + }); + + describe('calculateEndpointAuthz()', () => { + describe('and `fleet.all` access is true', () => { + it.each>([ + ['canAccessFleet'], + ['canAccessEndpointManagement'], + ['canIsolateHost'], + ])('should set `%s` to `true`', (authProperty) => { + expect(calculateEndpointAuthz(licenseService, fleetAuthz)[authProperty]).toBe(true); + }); + + it('should set `canIsolateHost` to false if not proper license', () => { + licenseService.isPlatinumPlus.mockReturnValue(false); + + expect(calculateEndpointAuthz(licenseService, fleetAuthz).canIsolateHost).toBe(false); + }); + + it('should set `canUnIsolateHost` to true even if not proper license', () => { + licenseService.isPlatinumPlus.mockReturnValue(false); + + expect(calculateEndpointAuthz(licenseService, fleetAuthz).canUnIsolateHost).toBe(true); + }); + }); + + describe('and `fleet.all` access is false', () => { + beforeEach(() => (fleetAuthz.fleet.all = false)); + + it.each>([ + ['canAccessFleet'], + ['canAccessEndpointManagement'], + ['canIsolateHost'], + ])('should set `%s` to `false`', (authProperty) => { + expect(calculateEndpointAuthz(licenseService, fleetAuthz)[authProperty]).toBe(false); + }); + + it('should set `canUnIsolateHost` to true even if not proper license', () => { + licenseService.isPlatinumPlus.mockReturnValue(false); + + expect(calculateEndpointAuthz(licenseService, fleetAuthz).canUnIsolateHost).toBe(true); + }); + }); + }); + + describe('getEndpointAuthzInitialState()', () => { + it('returns expected initial state', () => { + expect(getEndpointAuthzInitialState()).toEqual({ + canAccessFleet: false, + canAccessEndpointManagement: false, + canIsolateHost: false, + canUnIsolateHost: true, + canCreateArtifactsByPolicy: false, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts new file mode 100644 index 000000000000..766843311cfd --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts @@ -0,0 +1,43 @@ +/* + * 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 { LicenseService } from '../../../license'; +import { FleetAuthz } from '../../../../../fleet/common'; +import { EndpointAuthz } from '../../types/authz'; + +/** + * Used by both the server and the UI to generate the Authorization for access to Endpoint related + * functionality + * + * @param licenseService + * @param fleetAuthz + */ +export const calculateEndpointAuthz = ( + licenseService: LicenseService, + fleetAuthz: FleetAuthz +): EndpointAuthz => { + const isPlatinumPlusLicense = licenseService.isPlatinumPlus(); + const hasAllAccessToFleet = fleetAuthz.fleet.all; + + return { + canAccessFleet: hasAllAccessToFleet, + canAccessEndpointManagement: hasAllAccessToFleet, + canCreateArtifactsByPolicy: isPlatinumPlusLicense, + canIsolateHost: isPlatinumPlusLicense && hasAllAccessToFleet, + canUnIsolateHost: true, + }; +}; + +export const getEndpointAuthzInitialState = (): EndpointAuthz => { + return { + canAccessFleet: false, + canAccessEndpointManagement: false, + canCreateArtifactsByPolicy: false, + canIsolateHost: false, + canUnIsolateHost: true, + }; +}; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/use_integrations_page_link.tsx b/x-pack/plugins/security_solution/common/endpoint/service/authz/index.ts similarity index 59% rename from x-pack/plugins/security_solution/public/overview/components/overview_cti_links/use_integrations_page_link.tsx rename to x-pack/plugins/security_solution/common/endpoint/service/authz/index.ts index de710c2f1b17..975d28eb9dcb 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/use_integrations_page_link.tsx +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/index.ts @@ -5,7 +5,5 @@ * 2.0. */ -import { useBasePath } from '../../../common/lib/kibana'; - -export const useIntegrationsPageLink = () => - `${useBasePath()}/app/integrations/browse?q=threat%20intelligence`; +export { getEndpointAuthzInitialState, calculateEndpointAuthz } from './authz'; +export { getEndpointAuthzInitialStateMock } from './mocks'; diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/mocks.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/mocks.ts new file mode 100644 index 000000000000..7f1a6f969272 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/mocks.ts @@ -0,0 +1,29 @@ +/* + * 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 { EndpointAuthz } from '../../types/authz'; +import { getEndpointAuthzInitialState } from './authz'; + +export const getEndpointAuthzInitialStateMock = ( + overrides: Partial = {} +): EndpointAuthz => { + const authz: EndpointAuthz = { + ...( + Object.entries(getEndpointAuthzInitialState()) as Array<[keyof EndpointAuthz, boolean]> + ).reduce((mockPrivileges, [key, value]) => { + // Invert the initial values (from `false` to `true`) so that everything is authorized + mockPrivileges[key] = !value; + + return mockPrivileges; + }, {} as EndpointAuthz), + // this one is currently treated special in that everyone can un-isolate + canUnIsolateHost: true, + ...overrides, + }; + + return authz; +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/authz.ts b/x-pack/plugins/security_solution/common/endpoint/types/authz.ts new file mode 100644 index 000000000000..da0a372db8aa --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/types/authz.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Set of Endpoint Specific privileges that control application authorization. This interface is + * used both on the client and server for consistency + */ +export interface EndpointAuthz { + /** If user has permissions to access Fleet */ + canAccessFleet: boolean; + /** If user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */ + canAccessEndpointManagement: boolean; + /** if user has permissions to create Artifacts by Policy */ + canCreateArtifactsByPolicy: boolean; + /** If user has permissions to isolate hosts */ + canIsolateHost: boolean; + /** If user has permissions to un-isolate (release) hosts */ + canUnIsolateHost: boolean; +} + +export interface EndpointPrivileges extends EndpointAuthz { + loading: boolean; +} diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index c869c9c780bd..1fce6f17bdea 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -1246,3 +1246,5 @@ interface BaseListResponse { * Returned by the server via GET /api/endpoint/metadata */ export type MetadataListResponse = BaseListResponse; + +export type { EndpointPrivileges } from './authz'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts index a6e7eef88724..26bf4ce6740a 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts @@ -5,16 +5,13 @@ * 2.0. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { IEsSearchResponse, IEsSearchRequest } from 'src/plugins/data/public'; -import { FactoryQueryTypes } from '../..'; +import type { IEsSearchResponse } from 'src/plugins/data/public'; import { EVENT_ENRICHMENT_INDICATOR_FIELD_MAP } from '../../../cti/constants'; -import { Inspect, Maybe, TimerangeInput } from '../../common'; +import { Inspect } from '../../common'; import { RequestBasicOptions } from '..'; export enum CtiQueries { eventEnrichment = 'eventEnrichment', - dataSource = 'dataSource', } export interface CtiEventEnrichmentRequestOptions extends RequestBasicOptions { @@ -43,33 +40,3 @@ export const validEventFields = Object.keys(EVENT_ENRICHMENT_INDICATOR_FIELD_MAP export const isValidEventField = (field: string): field is EventField => validEventFields.includes(field as EventField); - -export interface CtiDataSourceRequestOptions extends IEsSearchRequest { - defaultIndex: string[]; - factoryQueryType?: FactoryQueryTypes; - timerange?: TimerangeInput; -} - -export interface BucketItem { - key: string; - doc_count: number; -} -export interface Bucket { - buckets: Array; -} - -export type DatasetBucket = { - name?: Bucket; - dashboard?: Bucket; -} & BucketItem; - -export interface CtiDataSourceStrategyResponse extends Omit { - inspect?: Maybe; - rawResponse: { - aggregations?: Record & { - dataset?: { - buckets: DatasetBucket[]; - }; - }; - }; -} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index 340093995b29..00cbdb941c11 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -72,8 +72,6 @@ import { CtiEventEnrichmentRequestOptions, CtiEventEnrichmentStrategyResponse, CtiQueries, - CtiDataSourceRequestOptions, - CtiDataSourceStrategyResponse, } from './cti'; import { HostRulesRequestOptions, @@ -87,7 +85,6 @@ import { UserRulesStrategyResponse, } from './ueba'; -export * from './cti'; export * from './hosts'; export * from './matrix_histogram'; export * from './network'; @@ -181,8 +178,6 @@ export type StrategyResponseType = T extends HostsQ ? MatrixHistogramStrategyResponse : T extends CtiQueries.eventEnrichment ? CtiEventEnrichmentStrategyResponse - : T extends CtiQueries.dataSource - ? CtiDataSourceStrategyResponse : never; export type StrategyRequestType = T extends HostsQueries.hosts @@ -243,8 +238,6 @@ export type StrategyRequestType = T extends HostsQu ? MatrixHistogramRequestOptions : T extends CtiQueries.eventEnrichment ? CtiEventEnrichmentRequestOptions - : T extends CtiQueries.dataSource - ? CtiDataSourceRequestOptions : never; export interface DocValueFieldsInput { diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts index 75ff13b66b29..095401ff3142 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts @@ -10,8 +10,9 @@ import { OVERVIEW_CTI_LINKS, OVERVIEW_CTI_LINKS_ERROR_INNER_PANEL, OVERVIEW_CTI_LINKS_INFO_INNER_PANEL, + OVERVIEW_CTI_LINKS_WARNING_INNER_PANEL, OVERVIEW_CTI_TOTAL_EVENT_COUNT, - OVERVIEW_CTI_ENABLE_INTEGRATIONS_BUTTON, + OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON, } from '../../screens/overview'; import { loginAndWaitForPage } from '../../tasks/login'; @@ -27,11 +28,12 @@ describe('CTI Link Panel', () => { it('renders disabled threat intel module as expected', () => { loginAndWaitForPage(OVERVIEW_URL); cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_ERROR_INNER_PANEL}`).should('exist'); + cy.get(`${OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON}`).should('be.disabled'); cy.get(`${OVERVIEW_CTI_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 0 indicators'); cy.get(`${OVERVIEW_CTI_ENABLE_MODULE_BUTTON}`).should('exist'); cy.get(`${OVERVIEW_CTI_ENABLE_MODULE_BUTTON}`) .should('have.attr', 'href') - .and('match', /app\/integrations\/browse\?q=threat%20intelligence/); + .and('match', /filebeat-module-threatintel.html/); }); describe('enabled threat intel module', () => { @@ -47,16 +49,17 @@ describe('CTI Link Panel', () => { loginAndWaitForPage( `${OVERVIEW_URL}?sourcerer=(timerange:(from:%272021-07-08T04:00:00.000Z%27,kind:absolute,to:%272021-07-09T03:59:59.999Z%27))` ); + cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_WARNING_INNER_PANEL}`).should('exist'); cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_INFO_INNER_PANEL}`).should('exist'); + cy.get(`${OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON}`).should('be.disabled'); cy.get(`${OVERVIEW_CTI_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 0 indicators'); }); it('renders dashboard module as expected when there are events in the selected time period', () => { loginAndWaitForPage(OVERVIEW_URL); + cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_WARNING_INNER_PANEL}`).should('not.exist'); cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_INFO_INNER_PANEL}`).should('exist'); - cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_ENABLE_INTEGRATIONS_BUTTON}`).should('exist'); - cy.get(OVERVIEW_CTI_LINKS).should('not.contain.text', 'Anomali'); - cy.get(OVERVIEW_CTI_LINKS).should('contain.text', 'AbuseCH malware'); + cy.get(`${OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON}`).should('be.disabled'); cy.get(`${OVERVIEW_CTI_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 1 indicator'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts index bc335ff6680e..1945b7e3ce3e 100644 --- a/x-pack/plugins/security_solution/cypress/screens/overview.ts +++ b/x-pack/plugins/security_solution/cypress/screens/overview.ts @@ -150,9 +150,9 @@ export const OVERVIEW_REVENT_TIMELINES = '[data-test-subj="overview-recent-timel export const OVERVIEW_CTI_LINKS = '[data-test-subj="cti-dashboard-links"]'; export const OVERVIEW_CTI_LINKS_ERROR_INNER_PANEL = '[data-test-subj="cti-inner-panel-danger"]'; +export const OVERVIEW_CTI_LINKS_WARNING_INNER_PANEL = '[data-test-subj="cti-inner-panel-warning"]'; export const OVERVIEW_CTI_LINKS_INFO_INNER_PANEL = '[data-test-subj="cti-inner-panel-info"]'; -export const OVERVIEW_CTI_ENABLE_INTEGRATIONS_BUTTON = - '[data-test-subj="cti-enable-integrations-button"]'; +export const OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON = '[data-test-subj="cti-view-dashboard-button"]'; export const OVERVIEW_CTI_TOTAL_EVENT_COUNT = `${OVERVIEW_CTI_LINKS} [data-test-subj="header-panel-subtitle"]`; export const OVERVIEW_CTI_ENABLE_MODULE_BUTTON = '[data-test-subj="cti-enable-module-button"]'; diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx index 78a340d6bbca..6d5f81b07656 100644 --- a/x-pack/plugins/security_solution/public/app/app.tsx +++ b/x-pack/plugins/security_solution/public/app/app.tsx @@ -25,7 +25,7 @@ import { State } from '../common/store'; import { StartServices } from '../types'; import { PageRouter } from './routes'; import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; -import { UserPrivilegesProvider } from '../common/components/user_privileges'; +import { UserPrivilegesProvider } from '../common/components/user_privileges/user_privileges_context'; interface StartAppComponent { children: React.ReactNode; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/index.ts new file mode 100644 index 000000000000..dc77a6b9eea8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { initialUserPrivilegesState, UserPrivilegesState } from '../user_privileges_context'; +import { getEndpointPrivilegesInitialStateMock } from '../endpoint/mocks'; + +export const useUserPrivileges = jest.fn(() => { + const mockedPrivileges: UserPrivilegesState = { + ...initialUserPrivilegesState(), + endpointPrivileges: getEndpointPrivilegesInitialStateMock(), + }; + + return mockedPrivileges; +}); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts index adea89ce1a05..83443dc20b9b 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export * from './use_endpoint_privileges'; +export { useEndpointPrivileges } from './use_endpoint_privileges'; export { getEndpointPrivilegesInitialState } from './utils'; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts index 2851c92816ce..2348fdf017c8 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts @@ -5,24 +5,16 @@ * 2.0. */ -import type { EndpointPrivileges } from './use_endpoint_privileges'; -import { getEndpointPrivilegesInitialState } from './utils'; +import { EndpointPrivileges } from '../../../../../common/endpoint/types'; +import { getEndpointAuthzInitialStateMock } from '../../../../../common/endpoint/service/authz/mocks'; -export const getEndpointPrivilegesInitialStateMock = ( - overrides: Partial = {} -): EndpointPrivileges => { - // Get the initial state and set all permissions to `true` (enabled) for testing +export const getEndpointPrivilegesInitialStateMock = ({ + loading = false, + ...overrides +}: Partial = {}): EndpointPrivileges => { const endpointPrivilegesMock: EndpointPrivileges = { - ...( - Object.entries(getEndpointPrivilegesInitialState()) as Array< - [keyof EndpointPrivileges, boolean] - > - ).reduce((mockPrivileges, [key, value]) => { - mockPrivileges[key] = !value; - - return mockPrivileges; - }, {} as EndpointPrivileges), - ...overrides, + ...getEndpointAuthzInitialStateMock(overrides), + loading, }; return endpointPrivilegesMock; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts index d4ba29a4ef95..4daef6cca45b 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts @@ -6,14 +6,14 @@ */ import { act, renderHook, RenderHookResult, RenderResult } from '@testing-library/react-hooks'; -import { useHttp, useCurrentUser } from '../../../lib/kibana'; -import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges'; +import { useCurrentUser, useKibana } from '../../../lib/kibana'; +import { useEndpointPrivileges } from './use_endpoint_privileges'; import { securityMock } from '../../../../../../security/public/mocks'; -import { appRoutesService } from '../../../../../../fleet/common'; import { AuthenticatedUser } from '../../../../../../security/common'; import { licenseService } from '../../../hooks/use_license'; -import { fleetGetCheckPermissionsHttpMock } from '../../../../management/pages/mocks'; import { getEndpointPrivilegesInitialStateMock } from './mocks'; +import { EndpointPrivileges } from '../../../../../common/endpoint/types'; +import { getEndpointPrivilegesInitialState } from './utils'; jest.mock('../../../lib/kibana'); jest.mock('../../../hooks/use_license', () => { @@ -32,10 +32,9 @@ const licenseServiceMock = licenseService as jest.Mocked; describe('When using useEndpointPrivileges hook', () => { let authenticatedUser: AuthenticatedUser; - let fleetApiMock: ReturnType; let result: RenderResult; let unmount: ReturnType['unmount']; - let waitForNextUpdate: ReturnType['waitForNextUpdate']; + let releaseFleetAuthz: () => void; let render: () => RenderHookResult; beforeEach(() => { @@ -45,14 +44,19 @@ describe('When using useEndpointPrivileges hook', () => { (useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser); - fleetApiMock = fleetGetCheckPermissionsHttpMock( - useHttp() as Parameters[0] - ); licenseServiceMock.isPlatinumPlus.mockReturnValue(true); + // Add a daly to fleet service that provides authz information + const fleetAuthz = useKibana().services.fleet!.authz; + + // Add a delay to the fleet Authz promise to test out the `loading` property + useKibana().services.fleet!.authz = new Promise((resolve) => { + releaseFleetAuthz = () => resolve(fleetAuthz); + }); + render = () => { const hookRenderResponse = renderHook(() => useEndpointPrivileges()); - ({ result, unmount, waitForNextUpdate } = hookRenderResponse); + ({ result, unmount } = hookRenderResponse); return hookRenderResponse; }; }); @@ -62,88 +66,22 @@ describe('When using useEndpointPrivileges hook', () => { }); it('should return `loading: true` while retrieving privileges', async () => { - // Add a daly to the API response that we can control from the test - let releaseApiResponse: () => void; - fleetApiMock.responseProvider.checkPermissions.mockDelay.mockReturnValue( - new Promise((resolve) => { - releaseApiResponse = () => resolve(); - }) - ); (useCurrentUser as jest.Mock).mockReturnValue(null); const { rerender } = render(); - expect(result.current).toEqual( - getEndpointPrivilegesInitialStateMock({ - canAccessEndpointManagement: false, - canAccessFleet: false, - loading: true, - }) - ); + expect(result.current).toEqual(getEndpointPrivilegesInitialState()); // Make user service available (useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser); rerender(); - expect(result.current).toEqual( - getEndpointPrivilegesInitialStateMock({ - canAccessEndpointManagement: false, - canAccessFleet: false, - loading: true, - }) - ); + expect(result.current).toEqual(getEndpointPrivilegesInitialState()); // Release the API response await act(async () => { - fleetApiMock.waitForApi(); - releaseApiResponse!(); + releaseFleetAuthz(); + await useKibana().services.fleet!.authz; }); - expect(result.current).toEqual(getEndpointPrivilegesInitialStateMock()); - }); - - it('should call Fleet permissions api to determine user privilege to fleet', async () => { - render(); - await waitForNextUpdate(); - await fleetApiMock.waitForApi(); - expect(useHttp().get as jest.Mock).toHaveBeenCalledWith( - appRoutesService.getCheckPermissionsPath() - ); - }); - it('should set privileges to false if user does not have superuser role', async () => { - authenticatedUser.roles = []; - render(); - await waitForNextUpdate(); - await fleetApiMock.waitForApi(); - expect(result.current).toEqual( - getEndpointPrivilegesInitialStateMock({ - canAccessEndpointManagement: false, - }) - ); - }); - - it('should set privileges to false if fleet api check returns failure', async () => { - fleetApiMock.responseProvider.checkPermissions.mockReturnValue({ - error: 'MISSING_SECURITY', - success: false, - }); - - render(); - await waitForNextUpdate(); - await fleetApiMock.waitForApi(); - expect(result.current).toEqual( - getEndpointPrivilegesInitialStateMock({ - canAccessEndpointManagement: false, - canAccessFleet: false, - }) - ); + expect(result.current).toEqual(getEndpointPrivilegesInitialStateMock()); }); - - it.each([['canIsolateHost'], ['canCreateArtifactsByPolicy']])( - 'should set %s to false if license is not PlatinumPlus', - async (privilege) => { - licenseServiceMock.isPlatinumPlus.mockReturnValue(false); - render(); - await waitForNextUpdate(); - expect(result.current).toEqual(expect.objectContaining({ [privilege]: false })); - } - ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts index 448cb215941d..6fa0c51f500d 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts @@ -6,24 +6,14 @@ */ import { useEffect, useMemo, useRef, useState } from 'react'; -import { useCurrentUser, useHttp } from '../../../lib/kibana'; -import { appRoutesService, CheckPermissionsResponse } from '../../../../../../fleet/common'; +import { useCurrentUser, useKibana } from '../../../lib/kibana'; import { useLicense } from '../../../hooks/use_license'; -import { Immutable } from '../../../../../common/endpoint/types'; - -export interface EndpointPrivileges { - loading: boolean; - /** If user has permissions to access Fleet */ - canAccessFleet: boolean; - /** If user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */ - canAccessEndpointManagement: boolean; - /** if user has permissions to create Artifacts by Policy */ - canCreateArtifactsByPolicy: boolean; - /** If user has permissions to use the Host isolation feature */ - canIsolateHost: boolean; - /** @deprecated do not use. instead, use one of the other privileges defined */ - isPlatinumPlus: boolean; -} +import { EndpointPrivileges, Immutable } from '../../../../../common/endpoint/types'; +import { + calculateEndpointAuthz, + getEndpointAuthzInitialState, +} from '../../../../../common/endpoint/service/authz'; +import { FleetAuthz } from '../../../../../../fleet/common'; /** * Retrieve the endpoint privileges for the current user. @@ -32,23 +22,39 @@ export interface EndpointPrivileges { * to keep API calls to a minimum. */ export const useEndpointPrivileges = (): Immutable => { - const http = useHttp(); const user = useCurrentUser(); + const fleetServices = useKibana().services.fleet; const isMounted = useRef(true); - const isPlatinumPlusLicense = useLicense().isPlatinumPlus(); - const [canAccessFleet, setCanAccessFleet] = useState(false); + const licenseService = useLicense(); const [fleetCheckDone, setFleetCheckDone] = useState(false); + const [fleetAuthz, setFleetAuthz] = useState(null); + + const privileges = useMemo(() => { + const privilegeList: EndpointPrivileges = Object.freeze({ + loading: !fleetCheckDone || !user, + ...(fleetAuthz + ? calculateEndpointAuthz(licenseService, fleetAuthz) + : getEndpointAuthzInitialState()), + }); + + return privilegeList; + }, [fleetCheckDone, user, fleetAuthz, licenseService]); // Check if user can access fleet useEffect(() => { + if (!fleetServices) { + setFleetCheckDone(true); + return; + } + + setFleetCheckDone(false); + (async () => { try { - const fleetPermissionsResponse = await http.get( - appRoutesService.getCheckPermissionsPath() - ); + const fleetAuthzForCurrentUser = await fleetServices.authz; if (isMounted.current) { - setCanAccessFleet(fleetPermissionsResponse.success); + setFleetAuthz(fleetAuthzForCurrentUser); } } finally { if (isMounted.current) { @@ -56,30 +62,7 @@ export const useEndpointPrivileges = (): Immutable => { } } })(); - }, [http]); - - // Check if user has `superuser` role - const isSuperUser = useMemo(() => { - if (user?.roles) { - return user.roles.includes('superuser'); - } - return false; - }, [user?.roles]); - - const privileges = useMemo(() => { - const privilegeList: EndpointPrivileges = Object.freeze({ - loading: !fleetCheckDone || !user, - canAccessFleet, - canAccessEndpointManagement: canAccessFleet && isSuperUser, - canCreateArtifactsByPolicy: isPlatinumPlusLicense, - canIsolateHost: isPlatinumPlusLicense, - // FIXME: Remove usages of the property below - /** @deprecated */ - isPlatinumPlus: isPlatinumPlusLicense, - }); - - return privilegeList; - }, [canAccessFleet, fleetCheckDone, isSuperUser, user, isPlatinumPlusLicense]); + }, [fleetServices]); // Capture if component is unmounted useEffect( diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.ts index df91314479f1..0c314ba5573c 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.ts @@ -5,15 +5,12 @@ * 2.0. */ -import { EndpointPrivileges } from './use_endpoint_privileges'; +import { EndpointPrivileges } from '../../../../../common/endpoint/types'; +import { getEndpointAuthzInitialState } from '../../../../../common/endpoint/service/authz'; export const getEndpointPrivilegesInitialState = (): EndpointPrivileges => { return { loading: true, - canAccessFleet: false, - canAccessEndpointManagement: false, - canIsolateHost: false, - canCreateArtifactsByPolicy: false, - isPlatinumPlus: false, + ...getEndpointAuthzInitialState(), }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.ts new file mode 100644 index 000000000000..3a5d942d3b53 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { useContext } from 'react'; +import { DeepReadonly } from 'utility-types'; +import { UserPrivilegesContext, UserPrivilegesState } from './user_privileges_context'; + +export const useUserPrivileges = (): DeepReadonly => + useContext(UserPrivilegesContext); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx b/x-pack/plugins/security_solution/public/common/components/user_privileges/user_privileges_context.tsx similarity index 81% rename from x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx rename to x-pack/plugins/security_solution/public/common/components/user_privileges/user_privileges_context.tsx index 05ccadeaf67a..5c681e5dbbae 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/user_privileges_context.tsx @@ -5,16 +5,14 @@ * 2.0. */ -import React, { createContext, useContext, useEffect, useState } from 'react'; -import { DeepReadonly } from 'utility-types'; - -import { Capabilities } from '../../../../../../../src/core/public'; -import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges'; +import React, { createContext, useEffect, useState } from 'react'; +import { Capabilities } from '../../../../../../../src/core/types'; +import { SERVER_APP_ID } from '../../../../common/constants'; import { useFetchListPrivileges } from '../../../detections/components/user_privileges/use_fetch_list_privileges'; -import { EndpointPrivileges, useEndpointPrivileges } from './endpoint'; +import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges'; +import { getEndpointPrivilegesInitialState, useEndpointPrivileges } from './endpoint'; +import { EndpointPrivileges } from '../../../../common/endpoint/types'; -import { SERVER_APP_ID } from '../../../../common/constants'; -import { getEndpointPrivilegesInitialState } from './endpoint/utils'; export interface UserPrivilegesState { listPrivileges: ReturnType; detectionEnginePrivileges: ReturnType; @@ -28,8 +26,9 @@ export const initialUserPrivilegesState = (): UserPrivilegesState => ({ endpointPrivileges: getEndpointPrivilegesInitialState(), kibanaSecuritySolutionsPrivileges: { crud: false, read: false }, }); - -const UserPrivilegesContext = createContext(initialUserPrivilegesState()); +export const UserPrivilegesContext = createContext( + initialUserPrivilegesState() +); interface UserPrivilegesProviderProps { kibanaCapabilities: Capabilities; @@ -73,6 +72,3 @@ export const UserPrivilegesProvider = ({ ); }; - -export const useUserPrivileges = (): DeepReadonly => - useContext(UserPrivilegesContext); diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 528592051ccc..9ad5abc1c7ed 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -25,8 +25,8 @@ import { import { FieldHook } from '../../shared_imports'; import { SUB_PLUGINS_REDUCER } from './utils'; import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage'; -import { UserPrivilegesProvider } from '../components/user_privileges'; import { CASES_FEATURE_ID } from '../../../common/constants'; +import { UserPrivilegesProvider } from '../components/user_privileges/user_privileges_context'; const state: State = mockGlobalState; diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx index 0447130e1bd1..32911a2c8e4a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx @@ -13,7 +13,7 @@ import { Capabilities } from 'src/core/public'; import { useKibana } from '../../../common/lib/kibana'; import * as api from '../../containers/detection_engine/alerts/api'; import { TestProviders } from '../../../common/mock/test_providers'; -import { UserPrivilegesProvider } from '../../../common/components/user_privileges'; +import { UserPrivilegesProvider } from '../../../common/components/user_privileges/user_privileges_context'; jest.mock('../../../common/lib/kibana'); jest.mock('../../containers/detection_engine/alerts/api'); diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx index 3b987a721141..493b41bc0165 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx @@ -8,18 +8,21 @@ import React from 'react'; import { act, fireEvent } from '@testing-library/react'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint'; -import { - EndpointPrivileges, - useEndpointPrivileges, -} from '../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; import { SearchExceptions, SearchExceptionsProps } from '.'; import { getEndpointPrivilegesInitialStateMock } from '../../../common/components/user_privileges/endpoint/mocks'; -jest.mock('../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); +import { + initialUserPrivilegesState, + UserPrivilegesState, +} from '../../../common/components/user_privileges/user_privileges_context'; +import { EndpointPrivileges } from '../../../../common/endpoint/types'; + +jest.mock('../../../common/components/user_privileges'); let onSearchMock: jest.Mock; -const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; +const mockUseUserPrivileges = useUserPrivileges as jest.Mock; describe('Search exceptions', () => { let appTestContext: AppContextTestRender; @@ -28,13 +31,16 @@ describe('Search exceptions', () => { props?: Partial ) => ReturnType; - const loadedUserEndpointPrivilegesState = ( + const loadedUserPrivilegesState = ( endpointOverrides: Partial = {} - ): EndpointPrivileges => - getEndpointPrivilegesInitialStateMock({ - isPlatinumPlus: false, - ...endpointOverrides, - }); + ): UserPrivilegesState => { + return { + ...initialUserPrivilegesState(), + endpointPrivileges: getEndpointPrivilegesInitialStateMock({ + ...endpointOverrides, + }), + }; + }; beforeEach(() => { onSearchMock = jest.fn(); @@ -51,11 +57,11 @@ describe('Search exceptions', () => { return renderResult; }; - mockUseEndpointPrivileges.mockReturnValue(loadedUserEndpointPrivilegesState()); + mockUseUserPrivileges.mockReturnValue(loadedUserPrivilegesState()); }); afterAll(() => { - mockUseEndpointPrivileges.mockReset(); + mockUseUserPrivileges.mockReset(); }); it('should have a default value', () => { @@ -102,8 +108,8 @@ describe('Search exceptions', () => { it('should hide policies selector when no license', () => { const generator = new EndpointDocGenerator('policy-list'); const policy = generator.generatePolicyPackagePolicy(); - mockUseEndpointPrivileges.mockReturnValue( - loadedUserEndpointPrivilegesState({ isPlatinumPlus: false }) + mockUseUserPrivileges.mockReturnValue( + loadedUserPrivilegesState({ canCreateArtifactsByPolicy: false }) ); const element = render({ policyList: [policy], hasPolicyFilter: true }); @@ -113,8 +119,8 @@ describe('Search exceptions', () => { it('should display policies selector when right license', () => { const generator = new EndpointDocGenerator('policy-list'); const policy = generator.generatePolicyPackagePolicy(); - mockUseEndpointPrivileges.mockReturnValue( - loadedUserEndpointPrivilegesState({ isPlatinumPlus: true }) + mockUseUserPrivileges.mockReturnValue( + loadedUserPrivilegesState({ canCreateArtifactsByPolicy: true }) ); const element = render({ policyList: [policy], hasPolicyFilter: true }); diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx index 569916ac2031..5489f7a394c9 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton } from '@elastic/e import { i18n } from '@kbn/i18n'; import { PolicySelectionItem, PoliciesSelector } from '../policies_selector'; import { ImmutableArray, PolicyData } from '../../../../common/endpoint/types'; -import { useEndpointPrivileges } from '../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; export interface SearchExceptionsProps { defaultValue?: string; @@ -34,7 +34,7 @@ export const SearchExceptions = memo( defaultExcludedPolicies, hideRefreshButton = false, }) => { - const { isPlatinumPlus } = useEndpointPrivileges(); + const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; const [query, setQuery] = useState(defaultValue); const [includedPolicies, setIncludedPolicies] = useState(defaultIncludedPolicies || ''); const [excludedPolicies, setExcludedPolicies] = useState(defaultExcludedPolicies || ''); @@ -92,7 +92,7 @@ export const SearchExceptions = memo( data-test-subj="searchField" /> - {isPlatinumPlus && hasPolicyFilter && policyList ? ( + {canCreateArtifactsByPolicy && hasPolicyFilter && policyList ? ( { let history: AppContextTestRender['history']; let mockedContext: AppContextTestRender; - const useEndpointPrivilegesMock = useEndpointPrivileges as jest.Mock; + const useUserPrivilegesMock = _useUserPrivileges as jest.Mock; + + const setEndpointPrivileges = (overrides: Partial = {}) => { + const newPrivileges = _useUserPrivileges(); + + useUserPrivilegesMock.mockReturnValue({ + ...newPrivileges, + endpointPrivileges: { + ...newPrivileges.endpointPrivileges, + ...overrides, + }, + }); + }; + const waitForApiCall = () => { return waitFor(() => expect(getHostIsolationExceptionItemsMock).toHaveBeenCalled()); }; @@ -162,7 +176,7 @@ describe('When on the host isolation exceptions page', () => { describe('has canIsolateHost privileges', () => { beforeEach(async () => { - useEndpointPrivilegesMock.mockReturnValue({ canIsolateHost: true }); + setEndpointPrivileges({ canIsolateHost: true }); getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock); }); @@ -185,7 +199,7 @@ describe('When on the host isolation exceptions page', () => { describe('does not have canIsolateHost privileges', () => { beforeEach(() => { - useEndpointPrivilegesMock.mockReturnValue({ canIsolateHost: false }); + setEndpointPrivileges({ canIsolateHost: false }); }); it('should not show the create flyout if the user navigates to the create url', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx index a9da5c6d135a..816aef5ca2dc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx @@ -31,11 +31,11 @@ import { EDIT_HOST_ISOLATION_EXCEPTION_LABEL, } from './components/translations'; import { getEndpointListPath } from '../../../common/routing'; -import { useEndpointPrivileges } from '../../../../common/components/user_privileges/endpoint'; import { MANAGEMENT_DEFAULT_PAGE_SIZE, MANAGEMENT_PAGE_SIZE_OPTIONS, } from '../../../common/constants'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; type HostIsolationExceptionPaginatedContent = PaginatedContentProps< Immutable, @@ -44,7 +44,7 @@ type HostIsolationExceptionPaginatedContent = PaginatedContentProps< export const HostIsolationExceptionsList = () => { const history = useHistory(); - const privileges = useEndpointPrivileges(); + const privileges = useUserPrivileges().endpointPrivileges; const location = useHostIsolationExceptionsSelector(getCurrentLocation); const navigateCallback = useHostIsolationExceptionsNavigateCallback(); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx index 3252c5a27d85..3a7308fef75f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx @@ -10,7 +10,7 @@ import { EuiEmptyPrompt, EuiButton, EuiPageTemplate, EuiLink } from '@elastic/eu import { FormattedMessage } from '@kbn/i18n-react'; import { usePolicyDetailsNavigateCallback } from '../../policy_hooks'; import { useGetLinkTo } from './use_policy_trusted_apps_empty_hooks'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; +import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; interface CommonProps { policyId: string; @@ -18,7 +18,7 @@ interface CommonProps { } export const PolicyTrustedAppsEmptyUnassigned = memo(({ policyId, policyName }) => { - const { isPlatinumPlus } = useEndpointPrivileges(); + const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; const navigateCallback = usePolicyDetailsNavigateCallback(); const { onClickHandler, toRouteUrl } = useGetLinkTo(policyId, policyName); const onClickPrimaryButtonHandler = useCallback( @@ -49,7 +49,7 @@ export const PolicyTrustedAppsEmptyUnassigned = memo(({ policyId, p /> } actions={[ - ...(isPlatinumPlus + ...(canCreateArtifactsByPolicy ? [ { it('should hide assign button on empty state with unassigned policies when downgraded to a gold or below license', async () => { mockUseEndpointPrivileges.mockReturnValue( getEndpointPrivilegesInitialStateMock({ - isPlatinumPlus: false, + canCreateArtifactsByPolicy: false, }) ); const component = render(); @@ -184,7 +184,7 @@ describe('Policy trusted apps layout', () => { it('should hide the `Assign trusted applications` button when there is data and the license is downgraded to gold or below', async () => { mockUseEndpointPrivileges.mockReturnValue( getEndpointPrivilegesInitialStateMock({ - isPlatinumPlus: false, + canCreateArtifactsByPolicy: false, }) ); const component = render(); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx index f39b080e56e3..3cf8e60c5e16 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx @@ -32,10 +32,10 @@ import { import { usePolicyDetailsNavigateCallback, usePolicyDetailsSelector } from '../../policy_hooks'; import { PolicyTrustedAppsFlyout } from '../flyout'; import { PolicyTrustedAppsList } from '../list/policy_trusted_apps_list'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { useAppUrl } from '../../../../../../common/lib/kibana'; import { APP_UI_ID } from '../../../../../../../common/constants'; import { getTrustedAppsListPath } from '../../../../../common/routing'; +import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; export const PolicyTrustedAppsLayout = React.memo(() => { const { getAppUrl } = useAppUrl(); @@ -44,7 +44,7 @@ export const PolicyTrustedAppsLayout = React.memo(() => { const isDoesTrustedAppExistsLoading = usePolicyDetailsSelector(doesTrustedAppExistsLoading); const policyItem = usePolicyDetailsSelector(policyDetails); const navigateCallback = usePolicyDetailsNavigateCallback(); - const { isPlatinumPlus } = useEndpointPrivileges(); + const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; const totalAssignedCount = usePolicyDetailsSelector(getTotalPolicyTrustedAppsListPagination); const hasTrustedApps = usePolicyDetailsSelector(getHasTrustedApps); const isLoadedHasTrustedApps = usePolicyDetailsSelector(getIsLoadedHasTrustedApps); @@ -138,7 +138,9 @@ export const PolicyTrustedAppsLayout = React.memo(() => { - {isPlatinumPlus && assignTrustedAppButton} + + {canCreateArtifactsByPolicy && assignTrustedAppButton} + @@ -169,7 +171,7 @@ export const PolicyTrustedAppsLayout = React.memo(() => { )} - {isPlatinumPlus && showListFlyout ? : null} + {canCreateArtifactsByPolicy && showListFlyout ? : null} ) : null; }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx index 7410dd20d928..32568ec2b48e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx @@ -16,14 +16,12 @@ import { policyDetailsPageAllApiHttpMocks } from '../../../test_utils'; import { isFailedResourceState, isLoadedResourceState } from '../../../../../state'; import { fireEvent, within, act, waitFor } from '@testing-library/react'; import { APP_UI_ID } from '../../../../../../../common/constants'; -import { - EndpointPrivileges, - useEndpointPrivileges, -} from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; +import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; +import { EndpointPrivileges } from '../../../../../../../common/endpoint/types'; -jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); -const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; +jest.mock('../../../../../../common/components/user_privileges'); +const mockUseUserPrivileges = useUserPrivileges as jest.Mock; describe('when rendering the PolicyTrustedAppsList', () => { // The index (zero based) of the card created by the generator that is policy specific @@ -78,11 +76,14 @@ describe('when rendering the PolicyTrustedAppsList', () => { }; afterAll(() => { - mockUseEndpointPrivileges.mockReset(); + mockUseUserPrivileges.mockReset(); }); beforeEach(() => { appTestContext = createAppRootMockRenderer(); - mockUseEndpointPrivileges.mockReturnValue(loadedUserEndpointPrivilegesState()); + mockUseUserPrivileges.mockReturnValue({ + ...mockUseUserPrivileges(), + endpointPrivileges: loadedUserEndpointPrivilegesState(), + }); mockedApis = policyDetailsPageAllApiHttpMocks(appTestContext.coreStart.http); appTestContext.setExperimentalFlag({ trustedAppsByPolicyEnabled: true }); @@ -317,11 +318,12 @@ describe('when rendering the PolicyTrustedAppsList', () => { }); it('does not show remove option in actions menu if license is downgraded to gold or below', async () => { - mockUseEndpointPrivileges.mockReturnValue( - loadedUserEndpointPrivilegesState({ - isPlatinumPlus: false, - }) - ); + mockUseUserPrivileges.mockReturnValue({ + ...mockUseUserPrivileges(), + endpointPrivileges: loadedUserEndpointPrivilegesState({ + canCreateArtifactsByPolicy: false, + }), + }); await render(); await toggleCardActionMenu(POLICY_SPECIFIC_CARD_INDEX); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx index 3453bc529b27..fa4d4e40b3e5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx @@ -38,7 +38,7 @@ import { ContextMenuItemNavByRouterProps } from '../../../../../components/conte import { ArtifactEntryCollapsibleCardProps } from '../../../../../components/artifact_entry_card'; import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator'; import { RemoveTrustedAppFromPolicyModal } from './remove_trusted_app_from_policy_modal'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; +import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; const DATA_TEST_SUBJ = 'policyTrustedAppsGrid'; @@ -52,7 +52,7 @@ export const PolicyTrustedAppsList = memo( const toasts = useToasts(); const history = useHistory(); const { getAppUrl } = useAppUrl(); - const { isPlatinumPlus } = useEndpointPrivileges(); + const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; const policyId = usePolicyDetailsSelector(policyIdFromParams); const isLoading = usePolicyDetailsSelector(isPolicyTrustedAppListLoading); const defaultFilter = usePolicyDetailsSelector(getCurrentPolicyArtifactsFilter); @@ -158,7 +158,7 @@ export const PolicyTrustedAppsList = memo( ]; const thisTrustedAppCardProps: ArtifactCardGridCardComponentProps = { expanded: Boolean(isCardExpanded[trustedApp.id]), - actions: isPlatinumPlus + actions: canCreateArtifactsByPolicy ? [ ...fullDetailsAction, { @@ -194,7 +194,14 @@ export const PolicyTrustedAppsList = memo( } return newCardProps; - }, [allPoliciesById, getAppUrl, getTestId, isCardExpanded, trustedAppItems, isPlatinumPlus]); + }, [ + allPoliciesById, + getAppUrl, + getTestId, + isCardExpanded, + trustedAppItems, + canCreateArtifactsByPolicy, + ]); const provideCardProps = useCallback['cardComponentProps']>( (item) => { diff --git a/x-pack/plugins/security_solution/public/overview/components/link_panel/helpers.ts b/x-pack/plugins/security_solution/public/overview/components/link_panel/helpers.ts index e2adaaae3554..45d26d9269f6 100644 --- a/x-pack/plugins/security_solution/public/overview/components/link_panel/helpers.ts +++ b/x-pack/plugins/security_solution/public/overview/components/link_panel/helpers.ts @@ -5,6 +5,13 @@ * 2.0. */ +import { LinkPanelListItem } from '.'; + +export const isLinkPanelListItem = ( + item: LinkPanelListItem | Partial +): item is LinkPanelListItem => + typeof item.title === 'string' && typeof item.path === 'string' && typeof item.count === 'number'; + export interface EventCounts { [key: string]: number; } diff --git a/x-pack/plugins/security_solution/public/overview/components/link_panel/index.ts b/x-pack/plugins/security_solution/public/overview/components/link_panel/index.ts index 9a827b137ae7..9d404abcf222 100644 --- a/x-pack/plugins/security_solution/public/overview/components/link_panel/index.ts +++ b/x-pack/plugins/security_solution/public/overview/components/link_panel/index.ts @@ -6,5 +6,6 @@ */ export { InnerLinkPanel } from './inner_link_panel'; +export { isLinkPanelListItem } from './helpers'; export { LinkPanel } from './link_panel'; export type { LinkPanelListItem } from './types'; diff --git a/x-pack/plugins/security_solution/public/overview/components/link_panel/link_panel.tsx b/x-pack/plugins/security_solution/public/overview/components/link_panel/link_panel.tsx index 00a225635fb8..ed67fdb1c96f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/link_panel/link_panel.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/link_panel/link_panel.tsx @@ -71,7 +71,7 @@ const LinkPanelComponent = ({ splitPanel, subtitle, }: { - button?: React.ReactNode; + button: React.ReactNode; columns: Array>; dataTestSubj: string; defaultSortField?: string; @@ -134,16 +134,14 @@ const LinkPanelComponent = ({ {splitPanel} {infoPanel} - {chunkedItems.length > 0 && ( - - )} + diff --git a/x-pack/plugins/security_solution/public/overview/components/link_panel/types.ts b/x-pack/plugins/security_solution/public/overview/components/link_panel/types.ts index 1b8836fc2438..f6c0fb6f3837 100644 --- a/x-pack/plugins/security_solution/public/overview/components/link_panel/types.ts +++ b/x-pack/plugins/security_solution/public/overview/components/link_panel/types.ts @@ -21,5 +21,4 @@ export interface LinkPanelViewProps { listItems: LinkPanelListItem[]; splitPanel?: JSX.Element; totalCount?: number; - allIntegrationsInstalled?: boolean; } diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx index 36f386e49c5c..2697e4a571ad 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx @@ -6,21 +6,24 @@ */ import React from 'react'; +import { EMPTY_LIST_ITEMS } from '../../containers/overview_cti_links/helpers'; +import { useKibana } from '../../../common/lib/kibana'; import * as i18n from './translations'; import { DisabledLinkPanel } from '../link_panel/disabled_link_panel'; import { ThreatIntelPanelView } from './threat_intel_panel_view'; -import { useIntegrationsPageLink } from './use_integrations_page_link'; export const CtiDisabledModuleComponent = () => { - const integrationsLink = useIntegrationsPageLink(); + const threatIntelDocLink = `${ + useKibana().services.docLinks.links.filebeat.base + }/filebeat-module-threatintel.html`; return ( diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.test.tsx index fc36a0c4337c..db83d9e1bcfe 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.test.tsx @@ -19,15 +19,20 @@ import { mockGlobalState, SUB_PLUGINS_REDUCER, } from '../../../common/mock'; -import { mockTheme, mockProps, mockTiDataSources, mockCtiLinksResponse } from './mock'; +import { mockTheme, mockProps, mockCtiEventCountsResponse, mockCtiLinksResponse } from './mock'; +import { useCtiEventCounts } from '../../containers/overview_cti_links/use_cti_event_counts'; import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; -import { useTiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources'; +import { useRequestEventCounts } from '../../containers/overview_cti_links/use_request_event_counts'; jest.mock('../../../common/lib/kibana'); -jest.mock('../../containers/overview_cti_links/use_ti_data_sources'); -const useTiDataSourcesMock = useTiDataSources as jest.Mock; -useTiDataSourcesMock.mockReturnValue(mockTiDataSources); +jest.mock('../../containers/overview_cti_links/use_cti_event_counts'); +const useCTIEventCountsMock = useCtiEventCounts as jest.Mock; +useCTIEventCountsMock.mockReturnValue(mockCtiEventCountsResponse); + +jest.mock('../../containers/overview_cti_links/use_request_event_counts'); +const useRequestEventCountsMock = useRequestEventCounts as jest.Mock; +useRequestEventCountsMock.mockReturnValue([true, {}]); jest.mock('../../containers/overview_cti_links'); const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; @@ -49,12 +54,42 @@ describe('CtiEnabledModule', () => { - + + + + + ); + + expect(screen.getByTestId('cti-with-events')).toBeInTheDocument(); + }); + + it('renders CtiWithNoEvents when there are no events', () => { + useCTIEventCountsMock.mockReturnValueOnce({ totalCount: 0 }); + render( + + + + + + + + ); + + expect(screen.getByTestId('cti-with-no-events')).toBeInTheDocument(); + }); + + it('renders null while event counts are loading', () => { + useCTIEventCountsMock.mockReturnValueOnce({ totalCount: -1 }); + const { container } = render( + + + + ); - expect(screen.getByText('Showing: 5 indicators')).toBeInTheDocument(); + expect(container.firstChild).toBeNull(); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.tsx index a339676ac361..5a40c79d6e5e 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.tsx @@ -7,28 +7,37 @@ import React from 'react'; import { ThreatIntelLinkPanelProps } from '.'; -import { useTiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources'; -import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; -import { ThreatIntelPanelView } from './threat_intel_panel_view'; +import { useCtiEventCounts } from '../../containers/overview_cti_links/use_cti_event_counts'; +import { CtiNoEvents } from './cti_no_events'; +import { CtiWithEvents } from './cti_with_events'; -export const CtiEnabledModuleComponent: React.FC = (props) => { - const { to, from, allIntegrationsInstalled, allTiDataSources, setQuery, deleteQuery } = props; - const { tiDataSources, totalCount } = useTiDataSources({ - to, - from, - allTiDataSources, - setQuery, - deleteQuery, - }); - const { listItems } = useCtiDashboardLinks({ to, from, tiDataSources }); +export type CtiEnabledModuleProps = Omit; - return ( - - ); +export const CtiEnabledModuleComponent: React.FC = (props) => { + const { eventCountsByDataset, totalCount } = useCtiEventCounts(props); + const { to, from } = props; + + switch (totalCount) { + case -1: + return null; + case 0: + return ( +
+ +
+ ); + default: + return ( +
+ +
+ ); + } }; export const CtiEnabledModule = React.memo(CtiEnabledModuleComponent); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.test.tsx new file mode 100644 index 000000000000..8f624dabd64d --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.test.tsx @@ -0,0 +1,70 @@ +/* + * 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 React from 'react'; +import { Provider } from 'react-redux'; +import { cloneDeep } from 'lodash/fp'; +import { render, screen } from '@testing-library/react'; +import { I18nProvider } from '@kbn/i18n-react'; +import { CtiNoEvents } from './cti_no_events'; +import { ThemeProvider } from 'styled-components'; +import { createStore, State } from '../../../common/store'; +import { + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { mockEmptyCtiLinksResponse, mockTheme, mockProps } from './mock'; +import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; + +jest.mock('../../../common/lib/kibana'); + +jest.mock('../../containers/overview_cti_links'); +const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; +useCtiDashboardLinksMock.mockReturnValue(mockEmptyCtiLinksResponse); + +describe('CtiNoEvents', () => { + const state: State = mockGlobalState; + + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + beforeEach(() => { + const myState = cloneDeep(state); + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + }); + + it('renders warning inner panel', () => { + render( + + + + + + + + ); + + expect(screen.getByTestId('cti-dashboard-links')).toBeInTheDocument(); + expect(screen.getByTestId('cti-inner-panel-warning')).toBeInTheDocument(); + }); + + it('renders event counts as 0', () => { + render( + + + + + + + + ); + + expect(screen.getByText('Showing: 0 indicators')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.tsx new file mode 100644 index 000000000000..fa7ac50c0876 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.tsx @@ -0,0 +1,42 @@ +/* + * 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 React from 'react'; +import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; +import { ThreatIntelPanelView } from './threat_intel_panel_view'; +import { InnerLinkPanel } from '../link_panel'; +import * as i18n from './translations'; +import { emptyEventCountsByDataset } from '../../containers/overview_cti_links/helpers'; + +const warning = ( + +); + +export const CtiNoEventsComponent = ({ to, from }: { to: string; from: string }) => { + const { buttonHref, listItems, isPluginDisabled } = useCtiDashboardLinks( + emptyEventCountsByDataset, + to, + from + ); + + return ( + + ); +}; + +export const CtiNoEvents = React.memo(CtiNoEventsComponent); +CtiNoEvents.displayName = 'CtiNoEvents'; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.test.tsx new file mode 100644 index 000000000000..a50e3e91ab9e --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.test.tsx @@ -0,0 +1,57 @@ +/* + * 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 React from 'react'; +import { Provider } from 'react-redux'; +import { cloneDeep } from 'lodash/fp'; +import { mount } from 'enzyme'; +import { I18nProvider } from '@kbn/i18n-react'; +import { CtiWithEvents } from './cti_with_events'; +import { ThemeProvider } from 'styled-components'; +import { createStore, State } from '../../../common/store'; +import { + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { mockCtiLinksResponse, mockTheme, mockCtiWithEventsProps } from './mock'; +import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; + +jest.mock('../../../common/lib/kibana'); + +jest.mock('../../containers/overview_cti_links'); +const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; +useCtiDashboardLinksMock.mockReturnValue(mockCtiLinksResponse); + +describe('CtiWithEvents', () => { + const state: State = mockGlobalState; + + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + beforeEach(() => { + const myState = cloneDeep(state); + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + }); + + it('renders total event count as expected', () => { + const wrapper = mount( + + + + + + + + ); + + expect(wrapper.find('[data-test-subj="cti-total-event-count"]').text()).toEqual( + `Showing: ${mockCtiWithEventsProps.totalCount} indicators` + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.tsx new file mode 100644 index 000000000000..f78451e205b1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.tsx @@ -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 React from 'react'; +import { isEqual } from 'lodash'; +import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; +import { ThreatIntelPanelView } from './threat_intel_panel_view'; + +export const CtiWithEventsComponent = ({ + eventCountsByDataset, + from, + to, + totalCount, +}: { + eventCountsByDataset: { [key: string]: number }; + from: string; + to: string; + totalCount: number; +}) => { + const { buttonHref, isPluginDisabled, listItems } = useCtiDashboardLinks( + eventCountsByDataset, + to, + from + ); + + return ( + + ); +}; + +CtiWithEventsComponent.displayName = 'CtiWithEvents'; + +export const CtiWithEvents = React.memo( + CtiWithEventsComponent, + (prevProps, nextProps) => + prevProps.to === nextProps.to && + prevProps.from === nextProps.from && + prevProps.totalCount === nextProps.totalCount && + isEqual(prevProps.eventCountsByDataset, nextProps.eventCountsByDataset) +); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.test.tsx index 71d6d5eb0c58..dfd9c6c9a7fc 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.test.tsx @@ -19,19 +19,19 @@ import { mockGlobalState, SUB_PLUGINS_REDUCER, } from '../../../common/mock'; -import { mockTheme, mockProps, mockTiDataSources, mockCtiLinksResponse } from './mock'; -import { useTiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources'; -import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; +import { mockTheme, mockProps, mockCtiEventCountsResponse } from './mock'; +import { useRequestEventCounts } from '../../containers/overview_cti_links/use_request_event_counts'; +import { useCtiEventCounts } from '../../containers/overview_cti_links/use_cti_event_counts'; jest.mock('../../../common/lib/kibana'); -jest.mock('../../containers/overview_cti_links/use_ti_data_sources'); -const useTiDataSourcesMock = useTiDataSources as jest.Mock; -useTiDataSourcesMock.mockReturnValue(mockTiDataSources); +jest.mock('../../containers/overview_cti_links/use_request_event_counts'); +const useRequestEventCountsMock = useRequestEventCounts as jest.Mock; +useRequestEventCountsMock.mockReturnValue([true, {}]); -jest.mock('../../containers/overview_cti_links'); -const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; -useCtiDashboardLinksMock.mockReturnValue(mockCtiLinksResponse); +jest.mock('../../containers/overview_cti_links/use_cti_event_counts'); +const useCTIEventCountsMock = useCtiEventCounts as jest.Mock; +useCTIEventCountsMock.mockReturnValue(mockCtiEventCountsResponse); describe('ThreatIntelLinkPanel', () => { const state: State = mockGlobalState; @@ -49,44 +49,40 @@ describe('ThreatIntelLinkPanel', () => { - + ); expect(wrapper.find('[data-test-subj="cti-enabled-module"]').length).toEqual(1); - expect(wrapper.find('[data-test-subj="cti-enable-integrations-button"]').length).toEqual(0); }); - it('renders Enable source buttons when not all integrations installed', () => { + it('renders CtiDisabledModule when Threat Intel module is disabled', () => { const wrapper = mount( - + ); - expect(wrapper.find('[data-test-subj="cti-enable-integrations-button"]').length).not.toBe(0); + + expect(wrapper.find('[data-test-subj="cti-disabled-module"]').length).toEqual(1); }); - it('renders CtiDisabledModule when Threat Intel module is disabled', () => { + it('renders null while Threat Intel module state is loading', () => { const wrapper = mount( - + ); - expect(wrapper.find('[data-test-subj="cti-disabled-module"]').length).toEqual(1); + expect(wrapper.html()).toEqual(''); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.tsx index c89199c2cb0c..5348c12fb6c8 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; -import { TiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources'; import { CtiEnabledModule } from './cti_enabled_module'; import { CtiDisabledModule } from './cti_disabled_module'; @@ -16,26 +15,27 @@ export type ThreatIntelLinkPanelProps = Pick< GlobalTimeArgs, 'from' | 'to' | 'deleteQuery' | 'setQuery' > & { - allIntegrationsInstalled: boolean | undefined; - allTiDataSources: TiDataSources[]; + isThreatIntelModuleEnabled: boolean | undefined; }; const ThreatIntelLinkPanelComponent: React.FC = (props) => { - const { allIntegrationsInstalled, allTiDataSources } = props; - const isThreatIntelModuleEnabled = allTiDataSources.length > 0; - return isThreatIntelModuleEnabled ? ( -
- -
- ) : ( -
- -
- ); + switch (props.isThreatIntelModuleEnabled) { + case true: + return ( +
+ +
+ ); + case false: + return ( +
+ +
+ ); + case undefined: + default: + return null; + } }; export const ThreatIntelLinkPanel = React.memo(ThreatIntelLinkPanelComponent); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/mock.ts b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/mock.ts index c4cf876cbdc7..1d02acaf65f4 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/mock.ts +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/mock.ts @@ -15,13 +15,6 @@ export const mockTheme = getMockTheme({ }, }); -export const mockTiDataSources = { - totalCount: 5, - tiDataSources: [ - { dataset: 'ti_abusech', name: 'AbuseCH', count: 5, path: '/dashboard_path_abuseurl' }, - ], -}; - export const mockEventCountsByDataset = { abuseurl: 1, abusemalware: 1, @@ -38,6 +31,8 @@ export const mockCtiEventCountsResponse = { }; export const mockCtiLinksResponse = { + isPluginDisabled: false, + buttonHref: '/button', listItems: [ { title: 'abuseurl', count: 1, path: '/dashboard_path_abuseurl' }, { title: 'abusemalware', count: 2, path: '/dashboard_path_abusemalware' }, @@ -68,10 +63,6 @@ export const mockProps = { from: '2020-01-21T20:49:57.080Z', setQuery: jest.fn(), deleteQuery: jest.fn(), - allIntegrationsInstalled: true, - allTiDataSources: [ - { dataset: 'ti_abusech', name: 'AbuseCH', count: 5, path: '/dashboard_path_abuseurl' }, - ], }; export const mockCtiWithEventsProps = { diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx index 3697d27015fd..189f230c02c8 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx @@ -9,14 +9,14 @@ import React, { useMemo } from 'react'; import { EuiButton, EuiTableFieldDataColumnType } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '../../../common/lib/kibana'; import * as i18n from './translations'; import { LinkPanel, InnerLinkPanel, LinkPanelListItem } from '../link_panel'; import { LinkPanelViewProps } from '../link_panel/types'; import { shortenCountIntoString } from '../../../common/utils/shorten_count_into_string'; import { Link } from '../link_panel/link'; -import { ID as CTIEventCountQueryId } from '../../containers/overview_cti_links/use_ti_data_sources'; +import { ID as CTIEventCountQueryId } from '../../containers/overview_cti_links/use_cti_event_counts'; import { LINK_COPY } from '../overview_risky_host_links/translations'; -import { useIntegrationsPageLink } from './use_integrations_page_link'; const columns: Array> = [ { name: 'Name', field: 'title', sortable: true, truncateText: true, width: '100%' }, @@ -39,43 +39,51 @@ const columns: Array> = [ ]; export const ThreatIntelPanelView: React.FC = ({ + buttonHref = '', + isPluginDisabled, isInspectEnabled = true, listItems, splitPanel, totalCount = 0, - allIntegrationsInstalled, }) => { - const integrationsLink = useIntegrationsPageLink(); + const threatIntelDashboardDocLink = `${ + useKibana().services.docLinks.links.filebeat.base + }/load-kibana-dashboards.html`; return ( ( + + {i18n.VIEW_DASHBOARD} + + ), + [buttonHref] + ), columns, dataTestSubj: 'cti-dashboard-links', infoPanel: useMemo( - () => ( - <> - {allIntegrationsInstalled === false ? ( - - {i18n.DANGER_BUTTON} - - } - /> - ) : null} - - ), - [allIntegrationsInstalled, integrationsLink] + () => + isPluginDisabled ? ( + + {i18n.INFO_BUTTON} + + } + /> + ) : null, + [isPluginDisabled, threatIntelDashboardDocLink] ), inspectQueryId: isInspectEnabled ? CTIEventCountQueryId : undefined, listItems, diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts index e112942b0974..4a64462b27ad 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts @@ -53,14 +53,15 @@ export const DANGER_TITLE = i18n.translate( export const DANGER_BODY = i18n.translate( 'xpack.securitySolution.overview.ctiDashboardEnableThreatIntel', { - defaultMessage: 'You need to enable threat intel sources in order to view data.', + defaultMessage: + 'You need to enable the filebeat threatintel module in order to view data from different sources.', } ); export const DANGER_BUTTON = i18n.translate( - 'xpack.securitySolution.overview.ctiDashboardDangerButton', + 'xpack.securitySolution.overview.ctiDashboardDangerPanelButton', { - defaultMessage: 'Enable sources', + defaultMessage: 'Enable Module', } ); @@ -71,17 +72,3 @@ export const PANEL_TITLE = i18n.translate('xpack.securitySolution.overview.ctiDa export const VIEW_DASHBOARD = i18n.translate('xpack.securitySolution.overview.ctiViewDasboard', { defaultMessage: 'View dashboard', }); - -export const SOME_MODULES_DISABLE_TITLE = i18n.translate( - 'xpack.securitySolution.overview.ctiDashboardSomeModulesDisabledTItle', - { - defaultMessage: 'Some threat intel sources are disabled', - } -); - -export const OTHER_DATA_SOURCE_TITLE = i18n.translate( - 'xpack.securitySolution.overview.ctiDashboardOtherDatasourceTitle', - { - defaultMessage: 'Others', - } -); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/api.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/api.ts deleted file mode 100644 index ad737ac410e3..000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/api.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { KibanaServices } from '../../../common/lib/kibana'; -import { EPM_API_ROUTES } from '../../../../../fleet/common'; - -export interface IntegrationResponse { - id: string; - status: string; - savedObject?: { - attributes?: { - installed_kibana: Array<{ - type: string; - id: string; - }>; - }; - }; -} - -export const fetchFleetIntegrations = () => - KibanaServices.get().http.fetch<{ - response: IntegrationResponse[]; - }>(EPM_API_ROUTES.LIST_PATTERN, { - method: 'GET', - }); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/helpers.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/helpers.ts new file mode 100644 index 000000000000..9ac61cc9487e --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/helpers.ts @@ -0,0 +1,60 @@ +/* + * 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 { SavedObjectAttributes } from '@kbn/securitysolution-io-ts-alerting-types'; +import { CTI_DATASET_KEY_MAP } from '../../../../common/cti/constants'; +import { LinkPanelListItem } from '../../components/link_panel'; +import { EventCounts } from '../../components/link_panel/helpers'; + +export const ctiTitles = Object.keys(CTI_DATASET_KEY_MAP) as string[]; + +export const EMPTY_LIST_ITEMS: LinkPanelListItem[] = ctiTitles.map((title) => ({ + title, + count: 0, + path: '', +})); + +const TAG_REQUEST_BODY_SEARCH = 'threat intel'; +export const TAG_REQUEST_BODY = { + type: 'tag', + search: TAG_REQUEST_BODY_SEARCH, + searchFields: ['name'], +}; + +export const DASHBOARD_SO_TITLE_PREFIX = '[Filebeat Threat Intel] '; +export const OVERVIEW_DASHBOARD_LINK_TITLE = 'Overview'; + +export const getCtiListItemsWithoutLinks = (eventCounts: EventCounts): LinkPanelListItem[] => { + return EMPTY_LIST_ITEMS.map((item) => ({ + ...item, + count: eventCounts[CTI_DATASET_KEY_MAP[item.title]] ?? 0, + })); +}; + +export const isOverviewItem = (item: { path?: string; title?: string }) => + item.title === OVERVIEW_DASHBOARD_LINK_TITLE; + +export const createLinkFromDashboardSO = ( + dashboardSO: { attributes?: SavedObjectAttributes }, + eventCountsByDataset: EventCounts, + path: string +) => { + const title = + typeof dashboardSO.attributes?.title === 'string' + ? dashboardSO.attributes.title.replace(DASHBOARD_SO_TITLE_PREFIX, '') + : undefined; + return { + title, + count: typeof title === 'string' ? eventCountsByDataset[CTI_DATASET_KEY_MAP[title]] : undefined, + path, + }; +}; + +export const emptyEventCountsByDataset = Object.values(CTI_DATASET_KEY_MAP).reduce((acc, id) => { + acc[id] = 0; + return acc; +}, {} as { [key: string]: number }); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx index b1310e363eef..a546d20e4958 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx @@ -6,30 +6,35 @@ */ import { useState, useEffect, useCallback } from 'react'; import { SavedObjectAttributes } from '@kbn/securitysolution-io-ts-alerting-types'; -import { TiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources'; -import { LinkPanelListItem } from '../../components/link_panel'; import { useKibana } from '../../../common/lib/kibana'; +import { + TAG_REQUEST_BODY, + createLinkFromDashboardSO, + getCtiListItemsWithoutLinks, + isOverviewItem, + EMPTY_LIST_ITEMS, +} from './helpers'; +import { LinkPanelListItem, isLinkPanelListItem } from '../../components/link_panel'; -const TAG_REQUEST_BODY_SEARCH = 'threat intel'; -export const TAG_REQUEST_BODY = { - type: 'tag', - search: TAG_REQUEST_BODY_SEARCH, - searchFields: ['name'], -}; - -export const useCtiDashboardLinks = ({ - to, - from, - tiDataSources = [], -}: { - to: string; - from: string; - tiDataSources?: TiDataSources[]; -}) => { - const [installedDashboardIds, setInstalledDashboardIds] = useState([]); - const dashboardLocator = useKibana().services.dashboard?.locator; +export const useCtiDashboardLinks = ( + eventCountsByDataset: { [key: string]: number }, + to: string, + from: string +) => { + const createDashboardUrl = useKibana().services.dashboard?.dashboardUrlGenerator?.createUrl; const savedObjectsClient = useKibana().services.savedObjects.client; + const [buttonHref, setButtonHref] = useState(); + const [listItems, setListItems] = useState(EMPTY_LIST_ITEMS); + + const [isPluginDisabled, setIsDashboardPluginDisabled] = useState(false); + const handleDisabledPlugin = useCallback(() => { + if (!isPluginDisabled) { + setIsDashboardPluginDisabled(true); + } + setListItems(getCtiListItemsWithoutLinks(eventCountsByDataset)); + }, [setIsDashboardPluginDisabled, setListItems, eventCountsByDataset, isPluginDisabled]); + const handleTagsReceived = useCallback( (TagsSO?) => { if (TagsSO?.savedObjects?.length) { @@ -44,7 +49,9 @@ export const useCtiDashboardLinks = ({ ); useEffect(() => { - if (savedObjectsClient) { + if (!createDashboardUrl || !savedObjectsClient) { + handleDisabledPlugin(); + } else { savedObjectsClient .find(TAG_REQUEST_BODY) .then(handleTagsReceived) @@ -56,40 +63,53 @@ export const useCtiDashboardLinks = ({ }>; }) => { if (DashboardsSO?.savedObjects?.length) { - setInstalledDashboardIds( - DashboardsSO.savedObjects.map((SO) => SO.id ?? '').filter(Boolean) + const dashboardUrls = await Promise.all( + DashboardsSO.savedObjects.map((SO) => + createDashboardUrl({ + dashboardId: SO.id, + timeRange: { + to, + from, + }, + }) + ) ); + const items = DashboardsSO.savedObjects + ?.reduce((acc: LinkPanelListItem[], dashboardSO, i) => { + const item = createLinkFromDashboardSO( + dashboardSO, + eventCountsByDataset, + dashboardUrls[i] + ); + if (isOverviewItem(item)) { + setButtonHref(item.path); + } else if (isLinkPanelListItem(item)) { + acc.push(item); + } + return acc; + }, []) + .sort((a, b) => (a.title > b.title ? 1 : -1)); + setListItems(items); + } else { + handleDisabledPlugin(); } } ); } - }, [handleTagsReceived, savedObjectsClient]); - - const listItems = tiDataSources.map((tiDataSource) => { - const listItem: LinkPanelListItem = { - title: tiDataSource.name, - count: tiDataSource.count, - path: '', - }; - - if ( - tiDataSource.dashboardId && - installedDashboardIds.includes(tiDataSource.dashboardId) && - dashboardLocator - ) { - listItem.path = dashboardLocator.getRedirectUrl({ - dashboardId: tiDataSource.dashboardId, - timeRange: { - to, - from, - }, - }); - } - - return listItem; - }); + }, [ + createDashboardUrl, + eventCountsByDataset, + from, + handleDisabledPlugin, + handleTagsReceived, + isPluginDisabled, + savedObjectsClient, + to, + ]); return { + buttonHref, + isPluginDisabled, listItems, }; }; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_all_ti_data_sources.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_all_ti_data_sources.ts deleted file mode 100644 index 5686be269121..000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_all_ti_data_sources.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { useMemo } from 'react'; -import { useTiDataSources } from './use_ti_data_sources'; - -export const useAllTiDataSources = () => { - const { to, from } = useMemo( - () => ({ - to: new Date().toISOString(), - from: new Date(0).toISOString(), - }), - [] - ); - - const { tiDataSources, isInitiallyLoaded } = useTiDataSources({ to, from }); - - return { tiDataSources, isInitiallyLoaded }; -}; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_cti_event_counts.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_cti_event_counts.ts new file mode 100644 index 000000000000..c8076ab6a448 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_cti_event_counts.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState, useMemo } from 'react'; +import { useRequestEventCounts } from './use_request_event_counts'; +import { emptyEventCountsByDataset } from './helpers'; +import { CtiEnabledModuleProps } from '../../components/overview_cti_links/cti_enabled_module'; + +export const ID = 'ctiEventCountQuery'; + +export const useCtiEventCounts = ({ deleteQuery, from, setQuery, to }: CtiEnabledModuleProps) => { + const [isInitialLoading, setIsInitialLoading] = useState(true); + + const [loading, { data, inspect, totalCount, refetch }] = useRequestEventCounts(to, from); + + const eventCountsByDataset = useMemo( + () => + data.reduce( + (acc, item) => { + if (item.y && item.g) { + const id = item.g; + acc[id] += item.y; + } + return acc; + }, + { ...emptyEventCountsByDataset } as { [key: string]: number } + ), + [data] + ); + + useEffect(() => { + if (isInitialLoading && data) { + setIsInitialLoading(false); + } + }, [isInitialLoading, data]); + + useEffect(() => { + if (!loading && !isInitialLoading) { + setQuery({ id: ID, inspect, loading, refetch }); + } + }, [setQuery, inspect, loading, refetch, isInitialLoading, setIsInitialLoading]); + + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, [deleteQuery]); + + useEffect(() => { + refetch(); + }, [to, from, refetch]); + + return { + eventCountsByDataset, + loading, + totalCount, + }; +}; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_is_threat_intel_module_enabled.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_is_threat_intel_module_enabled.ts new file mode 100644 index 000000000000..0dc0e8a3fe1f --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_is_threat_intel_module_enabled.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useState, useEffect, useMemo } from 'react'; +import { useRequestEventCounts } from './use_request_event_counts'; + +export const useIsThreatIntelModuleEnabled = () => { + const [isThreatIntelModuleEnabled, setIsThreatIntelModuleEnabled] = useState< + boolean | undefined + >(); + + const { to, from } = useMemo( + () => ({ + to: new Date().toISOString(), + from: new Date(0).toISOString(), + }), + [] + ); + + const [, { totalCount }] = useRequestEventCounts(to, from); + + useEffect(() => { + if (totalCount !== -1) { + setIsThreatIntelModuleEnabled(totalCount > 0); + } + }, [totalCount]); + + return isThreatIntelModuleEnabled; +}; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_request_event_counts.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_request_event_counts.ts new file mode 100644 index 000000000000..a1bf4d9d35f6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_request_event_counts.ts @@ -0,0 +1,54 @@ +/* + * 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 { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { convertToBuildEsQuery } from '../../../common/lib/keury'; +import { getEsQueryConfig } from '../../../../../../../src/plugins/data/common'; +import { MatrixHistogramType } from '../../../../common/search_strategy'; +import { EVENT_DATASET } from '../../../../common/cti/constants'; +import { useMatrixHistogram } from '../../../common/containers/matrix_histogram'; +import { useKibana } from '../../../common/lib/kibana'; +import { DEFAULT_THREAT_INDEX_KEY } from '../../../../common/constants'; + +export const useRequestEventCounts = (to: string, from: string) => { + const { uiSettings } = useKibana().services; + const defaultThreatIndices = uiSettings.get(DEFAULT_THREAT_INDEX_KEY); + + const [filterQuery] = convertToBuildEsQuery({ + config: getEsQueryConfig(uiSettings), + indexPattern: { + fields: [ + { + name: 'event.kind', + type: 'string', + }, + ], + title: defaultThreatIndices.toString(), + }, + queries: [{ query: 'event.type:indicator', language: 'kuery' }], + filters: [], + }); + + const matrixHistogramRequest = useMemo(() => { + return { + endDate: to, + errorMessage: i18n.translate('xpack.securitySolution.overview.errorFetchingEvents', { + defaultMessage: 'Error fetching events', + }), + filterQuery, + histogramType: MatrixHistogramType.events, + indexNames: defaultThreatIndices, + stackByField: EVENT_DATASET, + startDate: from, + size: 0, + }; + }, [to, from, filterQuery, defaultThreatIndices]); + + const results = useMatrixHistogram(matrixHistogramRequest); + + return results; +}; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_data_sources.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_data_sources.ts deleted file mode 100644 index 865af2266f2e..000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_data_sources.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { Observable } from 'rxjs'; -import { filter } from 'rxjs/operators'; -import { useEffect, useState } from 'react'; -import { useObservable, withOptionalSignal } from '@kbn/securitysolution-hook-utils'; -import { useKibana } from '../../../common/lib/kibana'; -import { - DataPublicPluginStart, - isCompleteResponse, - isErrorResponse, -} from '../../../../../../../src/plugins/data/public'; -import { - Bucket, - CtiQueries, - CtiDataSourceStrategyResponse, - CtiDataSourceRequestOptions, -} from '../../../../common'; -import { DEFAULT_THREAT_INDEX_KEY } from '../../../../common/constants'; -import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; -import { OTHER_DATA_SOURCE_TITLE } from '../../components/overview_cti_links/translations'; -import { OTHER_TI_DATASET_KEY } from '../../../../common/cti/constants'; - -type GetThreatIntelSourcProps = CtiDataSourceRequestOptions & { - data: DataPublicPluginStart; - signal: AbortSignal; -}; -export const ID = 'ctiEventCountQuery'; - -export const getTiDataSources = ({ - data, - defaultIndex, - timerange, - signal, -}: GetThreatIntelSourcProps): Observable => - data.search.search( - { - defaultIndex, - factoryQueryType: CtiQueries.dataSource, - timerange, - }, - { - strategy: 'securitySolutionSearchStrategy', - abortSignal: signal, - } - ); - -export const getTiDataSourcesComplete = ( - props: GetThreatIntelSourcProps -): Observable => { - return getTiDataSources(props).pipe( - filter((response) => { - return isErrorResponse(response) || isCompleteResponse(response); - }) - ); -}; - -const getTiDataSourcesWithOptionalSignal = withOptionalSignal(getTiDataSourcesComplete); - -export const useTiDataSourcesComplete = () => useObservable(getTiDataSourcesWithOptionalSignal); - -export interface TiDataSources { - dataset: string; - name: string; - count: number; - dashboardId?: string; -} -interface TiDataSourcesProps extends Partial { - allTiDataSources?: TiDataSources[]; -} - -export const useTiDataSources = ({ - to, - from, - allTiDataSources, - setQuery, - deleteQuery, -}: TiDataSourcesProps) => { - const [tiDataSources, setTiDataSources] = useState([]); - const [isInitiallyLoaded, setIsInitiallyLoaded] = useState(false); - const { data, uiSettings } = useKibana().services; - const defaultThreatIndices = uiSettings.get(DEFAULT_THREAT_INDEX_KEY); - const { result, start, loading } = useTiDataSourcesComplete(); - - useEffect(() => { - start({ - data, - timerange: to && from ? { to, from, interval: '' } : undefined, - defaultIndex: defaultThreatIndices, - }); - }, [to, from, start, data, defaultThreatIndices]); - - useEffect(() => { - if (!loading && result?.rawResponse && result?.inspect && setQuery) { - setQuery({ - id: ID, - inspect: { - dsl: result?.inspect?.dsl ?? [], - response: [JSON.stringify(result.rawResponse, null, 2)], - }, - loading, - refetch: () => {}, - }); - } - }, [setQuery, loading, result]); - - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - }, [deleteQuery]); - - useEffect(() => { - if (result && !isInitiallyLoaded) { - setIsInitiallyLoaded(true); - } - }, [isInitiallyLoaded, result]); - - useEffect(() => { - if (!loading && result) { - const datasets = result?.rawResponse?.aggregations?.dataset?.buckets ?? []; - const getChildAggregationValue = (aggregation?: Bucket) => aggregation?.buckets?.[0]?.key; - - const integrationMap = datasets.reduce((acc: Record, dataset) => { - const datasetName = getChildAggregationValue(dataset?.name); - if (datasetName) { - return { - ...acc, - [dataset.key]: { - dataset: dataset?.key, - name: datasetName, - dashboardId: getChildAggregationValue(dataset?.dashboard), - count: dataset?.doc_count, - }, - }; - } else { - const otherTiDatasetKey = OTHER_TI_DATASET_KEY; - const otherDatasetCount = acc[otherTiDatasetKey]?.count ?? 0; - return { - ...acc, - [otherTiDatasetKey]: { - dataset: otherTiDatasetKey, - name: OTHER_DATA_SOURCE_TITLE, - count: otherDatasetCount + (dataset?.doc_count ?? 0), - }, - }; - } - }, {}); - - if (Array.isArray(allTiDataSources)) { - allTiDataSources.forEach((integration) => { - if (!integrationMap[integration.dataset]) { - integrationMap[integration.dataset] = { - ...integration, - count: 0, - }; - } - }); - } - - setTiDataSources(Object.values(integrationMap)); - } - }, [result, loading, allTiDataSources]); - - const totalCount = tiDataSources.reduce((acc, val) => acc + val.count, 0); - - return { tiDataSources, totalCount, isInitiallyLoaded }; -}; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_integrations.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_integrations.ts deleted file mode 100644 index 24bdc191b3d6..000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_integrations.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useState } from 'react'; - -import { installationStatuses } from '../../../../../fleet/common'; -import { TI_INTEGRATION_PREFIX } from '../../../../common/cti/constants'; -import { fetchFleetIntegrations, IntegrationResponse } from './api'; - -export interface Integration { - id: string; - dashboardIds: string[]; -} - -interface TiIntegrationStatus { - allIntegrationsInstalled: boolean; -} - -export const useTiIntegrations = () => { - const [tiIntegrationsStatus, setTiIntegrationsStatus] = useState( - null - ); - - useEffect(() => { - const getPackages = async () => { - try { - const { response: integrations } = await fetchFleetIntegrations(); - const tiIntegrations = integrations.filter((integration: IntegrationResponse) => - integration.id.startsWith(TI_INTEGRATION_PREFIX) - ); - - const allIntegrationsInstalled = tiIntegrations.every( - (integration: IntegrationResponse) => - integration.status === installationStatuses.Installed - ); - - setTiIntegrationsStatus({ - allIntegrationsInstalled, - }); - } catch (e) { - setTiIntegrationsStatus({ - allIntegrationsInstalled: false, - }); - } - }; - - getPackages(); - }, []); - - return tiIntegrationsStatus; -}; diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index b38072464c65..33fd1918dad5 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -17,19 +17,20 @@ import { UseMessagesStorage, } from '../../common/containers/local_storage/use_messages_storage'; import { Overview } from './index'; -import { - initialUserPrivilegesState, - useUserPrivileges, -} from '../../common/components/user_privileges'; +import { useUserPrivileges } from '../../common/components/user_privileges'; import { useSourcererDataView } from '../../common/containers/sourcerer'; import { useFetchIndex } from '../../common/containers/source'; -import { useAllTiDataSources } from '../containers/overview_cti_links/use_all_ti_data_sources'; -import { useTiIntegrations } from '../containers/overview_cti_links/use_ti_integrations'; -import { mockCtiLinksResponse, mockTiDataSources } from '../components/overview_cti_links/mock'; +import { useIsThreatIntelModuleEnabled } from '../containers/overview_cti_links/use_is_threat_intel_module_enabled'; +import { useCtiEventCounts } from '../containers/overview_cti_links/use_cti_event_counts'; +import { + mockCtiEventCountsResponse, + mockCtiLinksResponse, +} from '../components/overview_cti_links/mock'; import { useCtiDashboardLinks } from '../containers/overview_cti_links'; -import { EndpointPrivileges } from '../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { useHostsRiskScore } from '../containers/overview_risky_host_links/use_hosts_risk_score'; +import { initialUserPrivilegesState } from '../../common/components/user_privileges/user_privileges_context'; +import { EndpointPrivileges } from '../../../common/endpoint/types'; jest.mock('../../common/lib/kibana'); jest.mock('../../common/containers/source'); @@ -70,17 +71,18 @@ jest.mock('../../common/components/user_privileges', () => { jest.mock('../../common/containers/local_storage/use_messages_storage'); jest.mock('../containers/overview_cti_links'); +jest.mock('../containers/overview_cti_links/use_cti_event_counts'); const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; useCtiDashboardLinksMock.mockReturnValue(mockCtiLinksResponse); -jest.mock('../containers/overview_cti_links/use_all_ti_data_sources'); -const useAllTiDataSourcesMock = useAllTiDataSources as jest.Mock; -useAllTiDataSourcesMock.mockReturnValue(mockTiDataSources); +jest.mock('../containers/overview_cti_links/use_cti_event_counts'); +const useCTIEventCountsMock = useCtiEventCounts as jest.Mock; +useCTIEventCountsMock.mockReturnValue(mockCtiEventCountsResponse); -jest.mock('../containers/overview_cti_links/use_ti_integrations'); -const useTiIntegrationsMock = useTiIntegrations as jest.Mock; -useTiIntegrationsMock.mockReturnValue({}); +jest.mock('../containers/overview_cti_links/use_is_threat_intel_module_enabled'); +const useIsThreatIntelModuleEnabledMock = useIsThreatIntelModuleEnabled as jest.Mock; +useIsThreatIntelModuleEnabledMock.mockReturnValue(true); jest.mock('../containers/overview_risky_host_links/use_hosts_risk_score'); const useHostsRiskScoreMock = useHostsRiskScore as jest.Mock; @@ -299,8 +301,8 @@ describe('Overview', () => { }); describe('Threat Intel Dashboard Links', () => { - it('invokes useAllTiDataSourcesMock hook only once', () => { - useAllTiDataSourcesMock.mockClear(); + it('invokes useIsThreatIntelModuleEnabled hook only once', () => { + useIsThreatIntelModuleEnabledMock.mockClear(); mount( @@ -308,7 +310,7 @@ describe('Overview', () => { ); - expect(useAllTiDataSourcesMock).toHaveBeenCalledTimes(1); + expect(useIsThreatIntelModuleEnabledMock).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 1df49fed0735..67ee6c55ac06 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -30,8 +30,7 @@ import { ENDPOINT_METADATA_INDEX } from '../../../common/constants'; import { useSourcererDataView } from '../../common/containers/sourcerer'; import { useDeepEqualSelector } from '../../common/hooks/use_selector'; import { ThreatIntelLinkPanel } from '../components/overview_cti_links'; -import { useAllTiDataSources } from '../containers/overview_cti_links/use_all_ti_data_sources'; -import { useTiIntegrations } from '../containers/overview_cti_links/use_ti_integrations'; +import { useIsThreatIntelModuleEnabled } from '../containers/overview_cti_links/use_is_threat_intel_module_enabled'; import { useUserPrivileges } from '../../common/components/user_privileges'; import { RiskyHostLinks } from '../components/overview_risky_host_links'; import { useAlertsPrivileges } from '../../detections/containers/detection_engine/alerts/use_alerts_privileges'; @@ -76,10 +75,7 @@ const OverviewComponent = () => { endpointPrivileges: { canAccessFleet }, } = useUserPrivileges(); const { hasIndexRead, hasKibanaREAD } = useAlertsPrivileges(); - const { tiDataSources: allTiDataSources, isInitiallyLoaded: allTiDataSourcesLoaded } = - useAllTiDataSources(); - const tiIntegrationStatus = useTiIntegrations(); - const isTiLoaded = tiIntegrationStatus && allTiDataSourcesLoaded; + const isThreatIntelModuleEnabled = useIsThreatIntelModuleEnabled(); const riskyHostsEnabled = useIsExperimentalFeatureEnabled('riskyHostsEnabled'); @@ -154,16 +150,13 @@ const OverviewComponent = () => { - {isTiLoaded && ( - - )} + {riskyHostsEnabled && ( diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 9b9d72805425..dce08e2522be 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -17,9 +17,8 @@ import { createMockAgentPolicyService, createMockAgentService, createArtifactsClientMock, - createFleetAuthzMock, } from '../../../fleet/server/mocks'; -import { createMockConfig } from '../lib/detection_engine/routes/__mocks__'; +import { createMockConfig, requestContextMock } from '../lib/detection_engine/routes/__mocks__'; import { EndpointAppContextService, EndpointAppContextServiceSetupContract, @@ -40,6 +39,7 @@ import { parseExperimentalConfigValue } from '../../common/experimental_features import { createCasesClientMock } from '../../../cases/server/client/mocks'; import { requestContextFactoryMock } from '../request_context_factory.mock'; import { EndpointMetadataService } from './services/metadata'; +import { createFleetAuthzMock } from '../../../fleet/common'; /** * Creates a mocked EndpointAppContext. @@ -183,8 +183,7 @@ export function createRouteHandlerContext( dataClient: jest.Mocked, savedObjectsClient: jest.Mocked ) { - const context = - xpackMocks.createRequestHandlerContext() as unknown as jest.Mocked; + const context = requestContextMock.create() as jest.Mocked; context.core.elasticsearch.client = dataClient; context.core.savedObjects.client = savedObjectsClient; return context; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts index 29a4e5ce0b29..bd72c5a4044e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts @@ -48,6 +48,7 @@ import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data' import { legacyMetadataSearchResponseMock } from '../metadata/support/test_support'; import { AGENT_ACTIONS_INDEX, ElasticsearchAssetType } from '../../../../../fleet/common'; import { CasesClientMock } from '../../../../../cases/server/client/mocks'; +import { EndpointAuthz } from '../../../../common/endpoint/types/authz'; interface CallRouteInterface { body?: HostIsolationRequestBody; @@ -55,6 +56,7 @@ interface CallRouteInterface { searchResponse?: HostMetadata; mockUser?: any; license?: License; + authz?: Partial; } const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } }); @@ -182,7 +184,7 @@ describe('Host Isolation', () => { // it returns the requestContext mock used in the call, to assert internal calls (e.g. the indexed document) callRoute = async ( routePrefix: string, - { body, idxResponse, searchResponse, mockUser, license }: CallRouteInterface, + { body, idxResponse, searchResponse, mockUser, license, authz = {} }: CallRouteInterface, indexExists?: { endpointDsExists: boolean } ): Promise> => { const asUser = mockUser ? mockUser : superUser; @@ -191,6 +193,12 @@ describe('Host Isolation', () => { ); const ctx = createRouteHandlerContext(mockScopedClient, mockSavedObjectClient); + + ctx.securitySolution.endpointAuthz = { + ...ctx.securitySolution.endpointAuthz, + ...authz, + }; + // mock _index_template ctx.core.elasticsearch.client.asInternalUser.indices.existsIndexTemplate = jest .fn() @@ -206,6 +214,7 @@ describe('Host Isolation', () => { statusCode: 404, }); }); + const withIdxResp = idxResponse ? idxResponse : { statusCode: 201 }; const mockIndexResponse = jest.fn().mockImplementation(() => Promise.resolve(withIdxResp)); const mockSearchResponse = jest @@ -213,19 +222,25 @@ describe('Host Isolation', () => { .mockImplementation(() => Promise.resolve({ body: legacyMetadataSearchResponseMock(searchResponse) }) ); + if (indexExists) { ctx.core.elasticsearch.client.asInternalUser.index = mockIndexResponse; } + ctx.core.elasticsearch.client.asCurrentUser.index = mockIndexResponse; ctx.core.elasticsearch.client.asCurrentUser.search = mockSearchResponse; + const withLicense = license ? license : Platinum; licenseEmitter.next(withLicense); + const mockRequest = httpServerMock.createKibanaRequest({ body }); const [, routeHandler]: [ RouteConfig, RequestHandler ] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(routePrefix))!; + await routeHandler(ctx, mockRequest, mockResponse); + return ctx as unknown as jest.Mocked; }; }); @@ -424,14 +439,17 @@ describe('Host Isolation', () => { }); expect(mockResponse.ok).toBeCalled(); }); - it('prohibits license levels less than platinum from isolating hosts', async () => { - licenseEmitter.next(Gold); + + it('prohibits isolating hosts if no authz for it', async () => { await callRoute(ISOLATE_HOST_ROUTE, { body: { endpoint_ids: ['XYZ'] }, + authz: { canIsolateHost: false }, license: Gold, }); + expect(mockResponse.forbidden).toBeCalled(); }); + it('allows any license level to unisolate', async () => { licenseEmitter.next(Gold); await callRoute(UNISOLATE_HOST_ROUTE, { @@ -442,37 +460,33 @@ describe('Host Isolation', () => { }); }); - describe('User Level', () => { - it('allows superuser to perform isolation', async () => { - const superU = { username: 'foo', roles: ['superuser'] }; + describe('User Authorization Level', () => { + it('allows user to perform isolation when canIsolateHost is true', async () => { await callRoute(ISOLATE_HOST_ROUTE, { body: { endpoint_ids: ['XYZ'] }, - mockUser: superU, }); expect(mockResponse.ok).toBeCalled(); }); - it('allows superuser to perform unisolation', async () => { - const superU = { username: 'foo', roles: ['superuser'] }; + + it('allows user to perform unisolation when canUnIsolateHost is true', async () => { await callRoute(UNISOLATE_HOST_ROUTE, { body: { endpoint_ids: ['XYZ'] }, - mockUser: superU, }); expect(mockResponse.ok).toBeCalled(); }); - it('prohibits non-admin user from performing isolation', async () => { - const superU = { username: 'foo', roles: ['user'] }; + it('prohibits user from performing isolation if canIsolateHost is false', async () => { await callRoute(ISOLATE_HOST_ROUTE, { body: { endpoint_ids: ['XYZ'] }, - mockUser: superU, + authz: { canIsolateHost: false }, }); expect(mockResponse.forbidden).toBeCalled(); }); - it('prohibits non-admin user from performing unisolation', async () => { - const superU = { username: 'foo', roles: ['user'] }; + + it('prohibits user from performing un-isolation if canUnIsolateHost is false', async () => { await callRoute(UNISOLATE_HOST_ROUTE, { body: { endpoint_ids: ['XYZ'] }, - mockUser: superU, + authz: { canUnIsolateHost: false }, }); expect(mockResponse.forbidden).toBeCalled(); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts index 02f0cb486764..51f88730eb6f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts @@ -33,7 +33,6 @@ import { import { getMetadataForEndpoints } from '../../services'; import { EndpointAppContext } from '../../types'; import { APP_ID } from '../../../../common/constants'; -import { userCanIsolate } from '../../../../common/endpoint/actions'; import { doLogsEndpointActionDsExists } from '../../utils'; /** @@ -100,25 +99,20 @@ export const isolationRequestHandler = function ( SecuritySolutionRequestHandlerContext > { return async (context, req, res) => { - // only allow admin users - const user = endpointContext.service.security?.authc.getCurrentUser(req); - if (!userCanIsolate(user?.roles)) { - return res.forbidden({ - body: { - message: 'You do not have permission to perform this action', - }, - }); - } + const { canIsolateHost, canUnIsolateHost } = context.securitySolution.endpointAuthz; - // isolation requires plat+ - if (isolate && !endpointContext.service.getLicenseService()?.isPlatinumPlus()) { + // Ensure user has authorization to use this api + if ((!canIsolateHost && isolate) || (!canUnIsolateHost && !isolate)) { return res.forbidden({ body: { - message: 'Your license level does not allow for this action', + message: + 'You do not have permission to perform this action or license level does not allow for this action', }, }); } + const user = endpointContext.service.security?.authc.getCurrentUser(req); + // fetch the Agent IDs to send the commands to const endpointIDs = [...new Set(req.body.endpoint_ids)]; // dedupe const endpointData = await getMetadataForEndpoints(endpointIDs, context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index 86bba6969919..8abe054daeaf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -30,6 +30,7 @@ import type { SecuritySolutionApiRequestHandlerContext, SecuritySolutionRequestHandlerContext, } from '../../../../types'; +import { getEndpointAuthzInitialStateMock } from '../../../../../common/endpoint/service/authz'; const createMockClients = () => { const core = coreMock.createRequestHandlerContext(); @@ -93,6 +94,7 @@ const createSecuritySolutionRequestContextMock = ( return { core, + endpointAuthz: getEndpointAuthzInitialStateMock(), getConfig: jest.fn(() => clients.config), getFrameworkRequest: jest.fn(() => { return { diff --git a/x-pack/plugins/security_solution/server/request_context_factory.ts b/x-pack/plugins/security_solution/server/request_context_factory.ts index f6c1d6b44eca..d4adf5500438 100644 --- a/x-pack/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/plugins/security_solution/server/request_context_factory.ts @@ -17,7 +17,18 @@ import { SecuritySolutionPluginCoreSetupDependencies, SecuritySolutionPluginSetupDependencies, } from './plugin_contract'; -import { SecuritySolutionApiRequestHandlerContext } from './types'; +import { + SecuritySolutionApiRequestHandlerContext, + SecuritySolutionRequestHandlerContext, +} from './types'; +import { Immutable } from '../common/endpoint/types'; +import { EndpointAuthz } from '../common/endpoint/types/authz'; +import { + calculateEndpointAuthz, + getEndpointAuthzInitialState, +} from '../common/endpoint/service/authz'; +import { licenseService } from './lib/license'; +import { FleetAuthz } from '../../fleet/common'; export interface IRequestContextFactory { create( @@ -41,7 +52,7 @@ export class RequestContextFactory implements IRequestContextFactory { } public async create( - context: RequestHandlerContext, + context: Omit, request: KibanaRequest ): Promise { const { options, appClientFactory } = this; @@ -55,9 +66,31 @@ export class RequestContextFactory implements IRequestContextFactory { config, }); + let endpointAuthz: Immutable; + let fleetAuthz: FleetAuthz; + + // If Fleet is enabled, then get its Authz + if (startPlugins.fleet) { + fleetAuthz = context.fleet?.authz ?? (await startPlugins.fleet?.authz.fromRequest(request)); + } + return { core: context.core, + get endpointAuthz(): Immutable { + // Lazy getter of endpoint Authz. No point in defining it if it is never used. + if (!endpointAuthz) { + // If no fleet (fleet plugin is optional in the configuration), then just turn off all permissions + if (!startPlugins.fleet) { + endpointAuthz = getEndpointAuthzInitialState(); + } else { + endpointAuthz = calculateEndpointAuthz(licenseService, fleetAuthz); + } + } + + return endpointAuthz; + }, + getConfig: () => config, getFrameworkRequest: () => frameworkRequest, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/index.ts index e43af97e84af..5857a0417239 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/index.ts @@ -9,9 +9,7 @@ import type { FactoryQueryTypes } from '../../../../../common/search_strategy/se import { CtiQueries } from '../../../../../common/search_strategy/security_solution/cti'; import type { SecuritySolutionFactory } from '../types'; import { eventEnrichment } from './event_enrichment'; -import { dataSource } from './threat_intel_source'; export const ctiFactoryTypes: Record> = { [CtiQueries.eventEnrichment]: eventEnrichment, - [CtiQueries.dataSource]: dataSource, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/index.ts deleted file mode 100644 index 0951503b04cd..000000000000 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SecuritySolutionFactory } from '../../types'; -import { - CtiDataSourceStrategyResponse, - CtiQueries, - CtiDataSourceRequestOptions, -} from '../../../../../../common'; -import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; -import { inspectStringifyObject } from '../../../../../utils/build_query'; -import { buildTiDataSourceQuery } from './query.threat_intel_source.dsl'; - -export const dataSource: SecuritySolutionFactory = { - buildDsl: (options: CtiDataSourceRequestOptions) => buildTiDataSourceQuery(options), - parse: async ( - options: CtiDataSourceRequestOptions, - response: IEsSearchResponse - ): Promise => { - const inspect = { - dsl: [inspectStringifyObject(buildTiDataSourceQuery(options))], - }; - - return { - ...response, - inspect, - }; - }, -}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.test.ts deleted file mode 100644 index 832006930a32..000000000000 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { buildTiDataSourceQuery } from './query.threat_intel_source.dsl'; -import { CtiQueries } from '../../../../../../common'; - -export const mockOptions = { - defaultIndex: ['logs-ti_*', 'filebeat-8*'], - docValueFields: [], - factoryQueryType: CtiQueries.dataSource, - filterQuery: '', - timerange: { - interval: '12h', - from: '2020-09-06T15:23:52.757Z', - to: '2020-09-07T15:23:52.757Z', - }, -}; - -export const expectedDsl = { - body: { - aggs: { - dataset: { - terms: { - field: 'event.dataset', - }, - aggs: { - name: { - terms: { - field: 'threat.feed.name', - }, - }, - dashboard: { - terms: { - field: 'threat.feed.dashboard_id', - }, - }, - }, - }, - }, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: '2020-09-06T15:23:52.757Z', - lte: '2020-09-07T15:23:52.757Z', - format: 'strict_date_optional_time', - }, - }, - }, - ], - }, - }, - }, - ignore_unavailable: true, - index: ['logs-ti_*', 'filebeat-8*'], - size: 0, - track_total_hits: true, - allow_no_indices: true, -}; - -describe('buildbuildTiDataSourceQueryQuery', () => { - test('build query from options correctly', () => { - expect(buildTiDataSourceQuery(mockOptions)).toEqual(expectedDsl); - }); -}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.ts deleted file mode 100644 index 08463146a683..000000000000 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CtiDataSourceRequestOptions } from '../../../../../../common'; - -export const buildTiDataSourceQuery = ({ - timerange, - defaultIndex, -}: CtiDataSourceRequestOptions) => { - const filter = []; - - if (timerange) { - filter.push({ - range: { - '@timestamp': { - gte: timerange.from, - lte: timerange.to, - format: 'strict_date_optional_time', - }, - }, - }); - } - - const dslQuery = { - size: 0, - index: defaultIndex, - allow_no_indices: true, - ignore_unavailable: true, - track_total_hits: true, - body: { - aggs: { - dataset: { - terms: { field: 'event.dataset' }, - aggs: { - name: { - terms: { field: 'threat.feed.name' }, - }, - dashboard: { - terms: { - field: 'threat.feed.dashboard_id', - }, - }, - }, - }, - }, - query: { - bool: { - filter, - }, - }, - }, - }; - - return dslQuery; -}; diff --git a/x-pack/plugins/security_solution/server/types.ts b/x-pack/plugins/security_solution/server/types.ts index 82616aa36d27..75686d783407 100644 --- a/x-pack/plugins/security_solution/server/types.ts +++ b/x-pack/plugins/security_solution/server/types.ts @@ -17,10 +17,12 @@ import { AppClient } from './client'; import { ConfigType } from './config'; import { IRuleExecutionLogClient } from './lib/detection_engine/rule_execution_log/types'; import { FrameworkRequest } from './lib/framework'; +import { EndpointAuthz } from '../common/endpoint/types/authz'; export { AppClient }; export interface SecuritySolutionApiRequestHandlerContext extends RequestHandlerContext { + endpointAuthz: EndpointAuthz; getConfig: () => ConfigType; getFrameworkRequest: () => FrameworkRequest; getAppClient: () => AppClient; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 58d04788e98e..76d3f07facf0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -23438,6 +23438,7 @@ "xpack.securitySolution.overview.auditBeatProcessTitle": "プロセス", "xpack.securitySolution.overview.auditBeatSocketTitle": "ソケット", "xpack.securitySolution.overview.auditBeatUserTitle": "ユーザー", + "xpack.securitySolution.overview.ctiDashboardDangerPanelButton": "モジュールを有効にする", "xpack.securitySolution.overview.ctiDashboardDangerPanelTitle": "表示する脅威インテリジェンスデータがありません", "xpack.securitySolution.overview.ctiDashboardEnableThreatIntel": "別のソースからデータを表示するには、filebeat脅威インテリジェンスモジュールを有効にする必要があります。", "xpack.securitySolution.overview.ctiDashboardInfoPanelBody": "このガイドに従い、ダッシュボードを有効にして、ビジュアライゼーションにソースを表示できるようにしてください。", @@ -23459,6 +23460,7 @@ "xpack.securitySolution.overview.endpointNotice.message": "脅威防御、検出、深いセキュリティデータの可視化を実現し、ホストを保護します。", "xpack.securitySolution.overview.endpointNotice.title": "Endpoint Security", "xpack.securitySolution.overview.endpointNotice.tryButton": "Endpoint Securityを試す", + "xpack.securitySolution.overview.errorFetchingEvents": "イベントの取得エラー", "xpack.securitySolution.overview.eventsTitle": "イベント数", "xpack.securitySolution.overview.filebeatCiscoTitle": "Cisco", "xpack.securitySolution.overview.filebeatNetflowTitle": "Netflow", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index da71c1796066..01997e32f243 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23828,6 +23828,7 @@ "xpack.securitySolution.overview.auditBeatProcessTitle": "进程", "xpack.securitySolution.overview.auditBeatSocketTitle": "套接字", "xpack.securitySolution.overview.auditBeatUserTitle": "用户", + "xpack.securitySolution.overview.ctiDashboardDangerPanelButton": "启用模块", "xpack.securitySolution.overview.ctiDashboardDangerPanelTitle": "没有可显示的威胁情报数据", "xpack.securitySolution.overview.ctiDashboardEnableThreatIntel": "您需要启用 filebeat threatintel 模块,以便查看不同源的数据。", "xpack.securitySolution.overview.ctiDashboardInfoPanelBody": "按照此指南启用您的仪表板,以便可以在可视化中查看您的源。", @@ -23850,6 +23851,7 @@ "xpack.securitySolution.overview.endpointNotice.message": "使用威胁防御、检测和深度安全数据可见性功能保护您的主机。", "xpack.securitySolution.overview.endpointNotice.title": "Endpoint Security", "xpack.securitySolution.overview.endpointNotice.tryButton": "试用 Endpoint Security", + "xpack.securitySolution.overview.errorFetchingEvents": "提取事件时出错", "xpack.securitySolution.overview.eventsTitle": "事件计数", "xpack.securitySolution.overview.filebeatCiscoTitle": "Cisco", "xpack.securitySolution.overview.filebeatNetflowTitle": "NetFlow", diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts index d568e7224fd2..1815ab91b531 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts @@ -199,8 +199,7 @@ export default function (providerContext: FtrProviderContext) { .expect(400); }); - // https://github.com/elastic/kibana/issues/118257 - it.skip('should not allow multiple limited packages on the same agent policy', async function () { + it('should not allow multiple limited packages on the same agent policy', async function () { await supertest .post(`/api/fleet/package_policies`) .set('kbn-xsrf', 'xxxx') @@ -215,7 +214,7 @@ export default function (providerContext: FtrProviderContext) { package: { name: 'endpoint', title: 'Endpoint', - version: '0.13.0', + version: '1.3.0-dev.0', }, }) .expect(200); @@ -233,7 +232,7 @@ export default function (providerContext: FtrProviderContext) { package: { name: 'endpoint', title: 'Endpoint', - version: '0.13.0', + version: '1.3.0-dev.0', }, }) .expect(400); diff --git a/x-pack/test/functional/apps/api_keys/home_page.ts b/x-pack/test/functional/apps/api_keys/home_page.ts index 590724752758..c2dbcc1046f5 100644 --- a/x-pack/test/functional/apps/api_keys/home_page.ts +++ b/x-pack/test/functional/apps/api_keys/home_page.ts @@ -42,6 +42,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await security.testUser.setRoles(['kibana_admin']); await security.testUser.setRoles(['test_api_keys']); await pageObjects.common.navigateToApp('apiKeys'); + + // Delete any API keys created outside of these tests + await pageObjects.apiKeys.bulkDeleteApiKeys(); }); afterEach(async () => { diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json index ec5e2aae6e2e..a2e0c2d2921d 100644 --- a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json @@ -31,9 +31,6 @@ } }, "type": "file" - }, - "feed": { - "name": "AbuseCH malware" } }, "abusemalware": { @@ -75,4 +72,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json index bc5f6e3db916..8840cd4bee0d 100644 --- a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json @@ -796,14 +796,6 @@ "type": "keyword" } } - }, - "feed":{ - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } } } }