[Cases] Ping the ResponseOps team when an attachment types is registered (#152854)

## Summary

Plugins can register case attachments and provide migration for them.
This PR adds a test that will fail if someone registers a new attachment
type or add a new migration. To fix the test you need to update the test
file. When the test file is updated the ResponseOps team will get
notified to review the new changes.

Fixes: https://github.com/elastic/kibana/issues/146252

## Testing
Please test if your integrations are working as expected. Nothing should
change.

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Christos Nasikas 2023-03-09 10:29:05 -05:00 committed by GitHub
parent 8a9789bb24
commit da307207dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 350 additions and 97 deletions

View file

@ -12,10 +12,6 @@ import { identity } from 'fp-ts/lib/function';
import { SavedObjectsUtils } from '@kbn/core/server';
import {
isCommentRequestTypeExternalReference,
isCommentRequestTypePersistableState,
} from '../../../common/utils/attachments';
import type { CaseResponse } from '../../../common/api';
import { CommentRequestRt, throwErrors } from '../../../common/api';
@ -26,6 +22,7 @@ import type { CasesClientArgs } from '..';
import { decodeCommentRequest } from '../utils';
import { Operations } from '../../authorization';
import type { AddArgs } from './types';
import { validateRegisteredAttachments } from './validators';
/**
* Create an attachment to a case.
@ -58,23 +55,11 @@ export const addComment = async (
entities: [{ owner: comment.owner, id: savedObjectID }],
});
if (
isCommentRequestTypeExternalReference(query) &&
!externalReferenceAttachmentTypeRegistry.has(query.externalReferenceAttachmentTypeId)
) {
throw Boom.badRequest(
`Attachment type ${query.externalReferenceAttachmentTypeId} is not registered.`
);
}
if (
isCommentRequestTypePersistableState(query) &&
!persistableStateAttachmentTypeRegistry.has(query.persistableStateAttachmentTypeId)
) {
throw Boom.badRequest(
`Attachment type ${query.persistableStateAttachmentTypeId} is not registered.`
);
}
validateRegisteredAttachments({
query,
persistableStateAttachmentTypeRegistry,
externalReferenceAttachmentTypeRegistry,
});
const createdDate = new Date().toISOString();

View file

@ -23,6 +23,7 @@ import { decodeCommentRequest } from '../utils';
import type { OwnerEntity } from '../../authorization';
import { Operations } from '../../authorization';
import type { BulkCreateArgs } from './types';
import { validateRegisteredAttachments } from './validators';
/**
* Create an attachment to a case.
@ -40,10 +41,20 @@ export const bulkCreate = async (
fold(throwErrors(Boom.badRequest), identity)
);
const { logger, authorization, externalReferenceAttachmentTypeRegistry } = clientArgs;
const {
logger,
authorization,
externalReferenceAttachmentTypeRegistry,
persistableStateAttachmentTypeRegistry,
} = clientArgs;
attachments.forEach((attachment) => {
decodeCommentRequest(attachment, externalReferenceAttachmentTypeRegistry);
validateRegisteredAttachments({
query: attachment,
persistableStateAttachmentTypeRegistry,
externalReferenceAttachmentTypeRegistry,
});
});
try {

View file

@ -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 Boom from '@hapi/boom';
import {
isCommentRequestTypeExternalReference,
isCommentRequestTypePersistableState,
} from '../../../common/utils/attachments';
import type { CommentRequest } from '../../../common/api';
import type { ExternalReferenceAttachmentTypeRegistry } from '../../attachment_framework/external_reference_registry';
import type { PersistableStateAttachmentTypeRegistry } from '../../attachment_framework/persistable_state_registry';
export const validateRegisteredAttachments = ({
query,
persistableStateAttachmentTypeRegistry,
externalReferenceAttachmentTypeRegistry,
}: {
query: CommentRequest;
persistableStateAttachmentTypeRegistry: PersistableStateAttachmentTypeRegistry;
externalReferenceAttachmentTypeRegistry: ExternalReferenceAttachmentTypeRegistry;
}) => {
if (
isCommentRequestTypeExternalReference(query) &&
!externalReferenceAttachmentTypeRegistry.has(query.externalReferenceAttachmentTypeId)
) {
throw Boom.badRequest(
`Attachment type ${query.externalReferenceAttachmentTypeId} is not registered.`
);
}
if (
isCommentRequestTypePersistableState(query) &&
!persistableStateAttachmentTypeRegistry.has(query.persistableStateAttachmentTypeId)
) {
throw Boom.badRequest(
`Attachment type ${query.persistableStateAttachmentTypeId} is not registered.`
);
}
};

View file

@ -23,5 +23,4 @@ export const config: PluginConfigDescriptor<ConfigType> = {
export const plugin = (initializerContext: PluginInitializerContext) =>
new CasePlugin(initializerContext);
export type { PluginSetupContract } from './types';
export type { PluginStartContract } from './types';
export type { CasesSetup, CasesStart } from './types';

View file

@ -10,6 +10,8 @@ import type { CaseSavedObject } from './common/types';
import type { CasePostRequest, CommentAttributes } from '../common/api';
import { CaseSeverity, CaseStatuses, CommentType, ConnectorTypes } from '../common/api';
import { SECURITY_SOLUTION_OWNER } from '../common/constants';
import type { CasesStart } from './types';
import { createCasesClientMock } from './client/mocks';
export const mockCases: CaseSavedObject[] = [
{
@ -417,3 +419,15 @@ export const newCase: CasePostRequest = {
},
owner: SECURITY_SOLUTION_OWNER,
};
const casesClientMock = createCasesClientMock();
export const mockCasesContract = (): CasesStart => ({
getCasesClientWithRequest: jest.fn().mockResolvedValue(casesClientMock),
getExternalReferenceAttachmentTypeRegistry: jest.fn(),
getPersistableStateAttachmentTypeRegistry: jest.fn(),
});
export const casesPluginMock = {
createStartContract: mockCasesContract,
};

View file

@ -46,7 +46,7 @@ import {
} from './saved_object_types';
import type { CasesClient } from './client';
import type { CasesRequestHandlerContext, PluginSetupContract, PluginStartContract } from './types';
import type { CasesRequestHandlerContext, CasesSetup, CasesStart } from './types';
import { CasesClientFactory } from './client/factory';
import { getCasesKibanaFeature } from './features';
import { registerRoutes } from './routes/api/register_routes';
@ -101,7 +101,7 @@ export class CasePlugin {
this.userProfileService = new UserProfileService(this.logger);
}
public setup(core: CoreSetup, plugins: PluginsSetup): PluginSetupContract {
public setup(core: CoreSetup, plugins: PluginsSetup): CasesSetup {
this.logger.debug(
`Setting up Case Workflow with core contract [${Object.keys(
core
@ -176,7 +176,7 @@ export class CasePlugin {
};
}
public start(core: CoreStart, plugins: PluginsStart): PluginStartContract {
public start(core: CoreStart, plugins: PluginsStart): CasesStart {
this.logger.debug(`Starting Case Workflow`);
if (plugins.taskManager) {
@ -226,6 +226,9 @@ export class CasePlugin {
return {
getCasesClientWithRequest,
getExternalReferenceAttachmentTypeRegistry: () =>
this.externalReferenceAttachmentTypeRegistry,
getPersistableStateAttachmentTypeRegistry: () => this.persistableStateAttachmentTypeRegistry,
};
}

View file

@ -14,6 +14,8 @@ import type {
} from '@kbn/actions-plugin/server/types';
import type { CasesClient } from './client';
import type { AttachmentFramework } from './attachment_framework/types';
import type { ExternalReferenceAttachmentTypeRegistry } from './attachment_framework/external_reference_registry';
import type { PersistableStateAttachmentTypeRegistry } from './attachment_framework/persistable_state_registry';
export interface CaseRequestContext {
getCasesClient: () => Promise<CasesClient>;
@ -43,7 +45,7 @@ export type RegisterActionType = <
/**
* Cases server exposed contract for interacting with cases entities.
*/
export interface PluginStartContract {
export interface CasesStart {
/**
* Returns a client which can be used to interact with the cases backend entities.
*
@ -51,9 +53,11 @@ export interface PluginStartContract {
* @returns a {@link CasesClient}
*/
getCasesClientWithRequest(request: KibanaRequest): Promise<CasesClient>;
getExternalReferenceAttachmentTypeRegistry(): ExternalReferenceAttachmentTypeRegistry;
getPersistableStateAttachmentTypeRegistry(): PersistableStateAttachmentTypeRegistry;
}
export interface PluginSetupContract {
export interface CasesSetup {
attachmentFramework: AttachmentFramework;
}

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE = 'ml_anomaly_swimlane' as const;
export const CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS = 'ml_anomaly_charts' as const;

View file

@ -10,7 +10,8 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { CasesUiSetup } from '@kbn/cases-plugin/public';
import type { CoreStart } from '@kbn/core/public';
import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE, getEmbeddableComponent } from '../embeddables';
import { CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS } from '../../common/constants/cases';
import { getEmbeddableComponent } from '../embeddables';
import type { MlStartDependencies } from '../plugin';
import { PLUGIN_ICON } from '../../common/constants/app';
@ -20,13 +21,13 @@ export function registerAnomalyChartsCasesAttachment(
pluginStart: MlStartDependencies
) {
const EmbeddableComponent = getEmbeddableComponent(
ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE,
CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS,
coreStart,
pluginStart
);
cases.attachmentFramework.registerPersistableState({
id: ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE,
id: CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS,
icon: PLUGIN_ICON,
displayName: i18n.translate('xpack.ml.cases.anomalyCharts.displayName', {
defaultMessage: 'Anomaly charts',

View file

@ -10,9 +10,9 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { CasesUiSetup } from '@kbn/cases-plugin/public';
import type { CoreStart } from '@kbn/core/public';
import { CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE } from '../../common/constants/cases';
import { getEmbeddableComponent } from '../embeddables';
import type { MlStartDependencies } from '../plugin';
import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '..';
import { PLUGIN_ICON } from '../../common/constants/app';
export function registerAnomalySwimLaneCasesAttachment(
@ -21,13 +21,13 @@ export function registerAnomalySwimLaneCasesAttachment(
pluginStart: MlStartDependencies
) {
const EmbeddableComponent = getEmbeddableComponent(
ANOMALY_SWIMLANE_EMBEDDABLE_TYPE,
CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE,
coreStart,
pluginStart
);
cases.attachmentFramework.registerPersistableState({
id: ANOMALY_SWIMLANE_EMBEDDABLE_TYPE,
id: CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE,
icon: PLUGIN_ICON,
displayName: i18n.translate('xpack.ml.cases.anomalySwimLane.displayName', {
defaultMessage: 'Anomaly swim lane',

View file

@ -67,6 +67,10 @@ import { ML_ALERT_TYPES } from '../common/constants/alerts';
import { alertingRoutes } from './routes/alerting';
import { registerCollector } from './usage';
import { SavedObjectsSyncService } from './saved_objects/sync_task';
import {
CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE,
CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS,
} from '../common/constants/cases';
export type MlPluginSetup = SharedServices;
export type MlPluginStart = void;
@ -249,6 +253,16 @@ export class MlServerPlugin
registerCollector(plugins.usageCollection, coreSetup.savedObjects.getKibanaIndex());
}
if (plugins.cases) {
plugins.cases.attachmentFramework.registerPersistableState({
id: CASE_ATTACHMENT_TYPE_ID_ANOMALY_SWIMLANE,
});
plugins.cases.attachmentFramework.registerPersistableState({
id: CASE_ATTACHMENT_TYPE_ID_ANOMALY_EXPLORER_CHARTS,
});
}
return sharedServicesProviders;
}

View file

@ -26,6 +26,7 @@ import {
TaskManagerSetupContract,
TaskManagerStartContract,
} from '@kbn/task-manager-plugin/server';
import type { CasesSetup } from '@kbn/cases-plugin/server';
import type { RouteGuard } from './lib/route_guard';
import type { ResolveMlCapabilities } from '../common/types/capabilities';
import type { MlLicense } from '../common/license';
@ -63,6 +64,7 @@ export interface PluginsSetup {
actions?: ActionsPlugin['setup'];
usageCollection?: UsageCollectionSetup;
taskManager: TaskManagerSetupContract;
cases?: CasesSetup;
}
export interface PluginsStart {

View file

@ -13,3 +13,5 @@ export const ACTIONS_INDEX = `.logs-${OSQUERY_INTEGRATION_NAME}.actions`;
export const ACTION_RESPONSES_INDEX = `.logs-${OSQUERY_INTEGRATION_NAME}.action.responses`;
export const DEFAULT_PLATFORM = 'linux,windows,darwin';
export const CASE_ATTACHMENT_TYPE_ID = 'osquery';

View file

@ -8,6 +8,7 @@
import { EuiAvatar } from '@elastic/eui';
import React from 'react';
import type { ExternalReferenceAttachmentType } from '@kbn/cases-plugin/public/client/attachment_framework/types';
import { CASE_ATTACHMENT_TYPE_ID } from '../../../common/constants';
import { getLazyExternalContent } from './lazy_external_reference_content';
import type { ServicesWrapperProps } from '../services_wrapper';
import OsqueryLogo from '../../components/osquery_icon/osquery.svg';
@ -15,7 +16,7 @@ import OsqueryLogo from '../../components/osquery_icon/osquery.svg';
export const getExternalReferenceAttachmentRegular = (
services: ServicesWrapperProps['services']
): ExternalReferenceAttachmentType => ({
id: 'osquery',
id: CASE_ATTACHMENT_TYPE_ID,
displayName: 'Osquery',
getAttachmentViewObject: () => ({
type: 'regular',

View file

@ -42,6 +42,7 @@ import { createDataViews } from './create_data_views';
import { createActionHandler } from './handlers/action';
import { registerFeatures } from './utils/register_features';
import { CASE_ATTACHMENT_TYPE_ID } from '../common/constants';
export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginStart> {
private readonly logger: Logger;
@ -93,6 +94,8 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
this.telemetryEventsSender.setup(this.telemetryReceiver, plugins.taskManager, core.analytics);
plugins.cases.attachmentFramework.registerExternalReference({ id: CASE_ATTACHMENT_TYPE_ID });
return {
osqueryCreateAction: (
params: CreateLiveQueryRequestBodySchema,

View file

@ -23,6 +23,7 @@ import type {
import type { PluginStart as DataViewsPluginStart } from '@kbn/data-views-plugin/server';
import type { RuleRegistryPluginStartContract } from '@kbn/rule-registry-plugin/server';
import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
import type { CasesSetup } from '@kbn/cases-plugin/server';
import type { CreateLiveQueryRequestBodySchema } from '../common/schemas/routes/live_query';
export interface OsqueryPluginSetup {
@ -38,6 +39,7 @@ export interface OsqueryPluginStart {}
export interface SetupPlugins {
usageCollection?: UsageCollectionSetup;
actions: ActionsPlugin['setup'];
cases: CasesSetup;
data: DataPluginSetup;
features: PluginSetupContract;
security: SecurityPluginStart;

View file

@ -7,10 +7,7 @@
import type { KibanaRequest, Logger } from '@kbn/core/server';
import type { ExceptionListClient, ListsServerExtensionRegistrar } from '@kbn/lists-plugin/server';
import type {
CasesClient,
PluginStartContract as CasesPluginStartContract,
} from '@kbn/cases-plugin/server';
import type { CasesClient, CasesStart } from '@kbn/cases-plugin/server';
import type { SecurityPluginStart } from '@kbn/security-plugin/server';
import type { FleetStartContract, MessageSigningServiceInterface } from '@kbn/fleet-plugin/server';
import type { PluginStartContract as AlertsPluginStartContract } from '@kbn/alerting-plugin/server';
@ -58,7 +55,7 @@ export interface EndpointAppContextServiceStartContract {
registerListsServerExtension?: ListsServerExtensionRegistrar;
licenseService: LicenseService;
exceptionListsClient: ExceptionListClient | undefined;
cases: CasesPluginStartContract | undefined;
cases: CasesStart | undefined;
featureUsageService: FeatureUsageService;
experimentalFeatures: ExperimentalFeatures;
messageSigningService: MessageSigningServiceInterface | undefined;

View file

@ -35,14 +35,10 @@ import {
createMockPackageService,
createMessageSigningServiceMock,
} from '@kbn/fleet-plugin/server/mocks';
// A TS error (TS2403) is thrown when attempting to export the mock function below from Cases
// plugin server `index.ts`. Its unclear what is actually causing the error. Since this is a Mock
// file and not bundled with the application, adding a eslint disable below and using import from
// a restricted path.
import { createCasesClientMock } from '@kbn/cases-plugin/server/client/mocks';
import { createFleetAuthzMock } from '@kbn/fleet-plugin/common/mocks';
import type { RequestFixtureOptions } from '@kbn/core-http-router-server-mocks';
import type { ElasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
import { casesPluginMock } from '@kbn/cases-plugin/server/mocks';
import { getEndpointAuthzInitialStateMock } from '../../common/endpoint/service/authz/mocks';
import { xpackMocks } from '../fixtures';
import { createMockConfig, requestContextMock } from '../lib/detection_engine/routes/__mocks__';
@ -116,7 +112,6 @@ export const createMockEndpointAppContextServiceStartContract =
const config = createMockConfig();
const logger = loggingSystemMock.create().get('mock_endpoint_app_context');
const casesClientMock = createCasesClientMock();
const savedObjectsStart = savedObjectsServiceMock.createStartContract();
const security = securityMock.createStart();
const agentService = createMockAgentService();
@ -157,6 +152,8 @@ export const createMockEndpointAppContextServiceStartContract =
jest.fn(() => ({ privileges: { kibana: [] } }))
);
const casesMock = casesPluginMock.createStartContract();
return {
endpointMetadataService,
endpointFleetServicesFactory,
@ -172,9 +169,7 @@ export const createMockEndpointAppContextServiceStartContract =
Parameters<FleetStartContract['registerExternalCallback']>
>(),
exceptionListsClient: listMock.getExceptionListClient(),
cases: {
getCasesClientWithRequest: jest.fn(async () => casesClientMock),
},
cases: casesMock,
featureUsageService: createFeatureUsageServiceMock(),
experimentalFeatures: createMockConfig().experimentalFeatures,
messageSigningService: createMessageSigningServiceMock(),

View file

@ -777,6 +777,8 @@ describe('Response actions', () => {
{} as KibanaRequest
)) as CasesClientMock;
casesClient.attachments.add.mockClear();
let counter = 1;
casesClient.cases.getCasesByAlertID.mockImplementation(async () => {
return [

View file

@ -16,7 +16,7 @@ import type {
PluginSetupContract as AlertingPluginSetup,
PluginStartContract as AlertingPluginStart,
} from '@kbn/alerting-plugin/server';
import type { PluginStartContract as CasesPluginStart } from '@kbn/cases-plugin/server';
import type { CasesStart } from '@kbn/cases-plugin/server';
import type { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
import type { IEventLogClientService, IEventLogService } from '@kbn/event-log-plugin/server';
import type { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server';
@ -65,7 +65,7 @@ export interface SecuritySolutionPluginSetupDependencies {
export interface SecuritySolutionPluginStartDependencies {
alerting: AlertingPluginStart;
cases?: CasesPluginStart;
cases?: CasesStart;
cloudExperiments?: CloudExperimentsPluginStart;
data: DataPluginStart;
dataViews: DataViewsPluginStart;

View file

@ -16,3 +16,5 @@ export enum FactoryQueryType {
IndicatorGrid = 'indicatorGrid',
Barchart = 'barchart',
}
export const CASE_ATTACHMENT_TYPE_ID = 'indicator';

View file

@ -12,12 +12,11 @@ import { ExternalReferenceAttachmentType } from '@kbn/cases-plugin/public/client
import React from 'react';
import { EuiAvatar } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { CASE_ATTACHMENT_TYPE_ID } from '../../../../common/constants';
import { EMPTY_VALUE } from '../../../common/constants';
import { Indicator, RawIndicatorFieldId } from '../../../../common/types/indicator';
import { getIndicatorFieldAndValue } from '../../indicators';
const ExternalAttachmentTypeId = 'indicator';
/**
* Indicator name, type, feed name and first seen values,
* rendered in the comment section of a case's attachment or in the flyout
@ -43,7 +42,7 @@ const AttachmentChildrenLazy = React.lazy(
* - the component that renders the comment in teh case attachment
*/
export const generateAttachmentType = (): ExternalReferenceAttachmentType => ({
id: ExternalAttachmentTypeId,
id: CASE_ATTACHMENT_TYPE_ID,
displayName: 'indicator',
getAttachmentViewObject: () => ({
event: (
@ -82,7 +81,7 @@ export const generateAttachmentsWithoutOwner = (
externalReferenceStorage: {
type: ExternalReferenceStorageType.elasticSearchDoc,
},
externalReferenceAttachmentTypeId: ExternalAttachmentTypeId,
externalReferenceAttachmentTypeId: CASE_ATTACHMENT_TYPE_ID,
externalReferenceMetadata: attachmentMetadata as unknown as { [p: string]: JsonValue },
},
];

View file

@ -6,7 +6,10 @@
*/
import type { PluginInitializerContext, Logger } from '@kbn/core/server';
import { THREAT_INTELLIGENCE_SEARCH_STRATEGY_NAME } from '../common/constants';
import {
CASE_ATTACHMENT_TYPE_ID,
THREAT_INTELLIGENCE_SEARCH_STRATEGY_NAME,
} from '../common/constants';
import {
IThreatIntelligencePlugin,
ThreatIntelligencePluginCoreSetupDependencies,
@ -39,6 +42,8 @@ export class ThreatIntelligencePlugin implements IThreatIntelligencePlugin {
this.logger.debug(`search strategy "${THREAT_INTELLIGENCE_SEARCH_STRATEGY_NAME}" registered`);
});
plugins.cases.attachmentFramework.registerExternalReference({ id: CASE_ATTACHMENT_TYPE_ID });
return {};
}

View file

@ -6,11 +6,12 @@
*/
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
import { DataPluginSetup, DataPluginStart } from '@kbn/data-plugin/server/plugin';
import type { CasesSetup } from '@kbn/cases-plugin/server';
export interface ThreatIntelligencePluginSetupDependencies {
data: DataPluginSetup;
cases: CasesSetup;
}
export interface ThreatIntelligencePluginStartDependencies {

View file

@ -6,16 +6,13 @@
*/
import { Plugin, CoreSetup, CoreStart, PluginInitializerContext, Logger } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server';
import { SpacesPluginStart } from '@kbn/spaces-plugin/server';
import { SecurityPluginStart } from '@kbn/security-plugin/server';
import { PluginStartContract as CasesPluginStart } from '@kbn/cases-plugin/server';
import { CasesPatchRequest } from '@kbn/cases-plugin/common/api';
import { PluginSetupContract as CasesSetup } from '@kbn/cases-plugin/server/types';
import type { CasesStart, CasesSetup } from '@kbn/cases-plugin/server';
import { getPersistableStateAttachment } from './attachments/persistable_state';
import { getExternalReferenceAttachment } from './attachments/external_reference';
import { registerRoutes } from './routes';
export interface FixtureSetupDeps {
features: FeaturesPluginSetup;
@ -25,12 +22,11 @@ export interface FixtureSetupDeps {
export interface FixtureStartDeps {
security?: SecurityPluginStart;
spaces?: SpacesPluginStart;
cases?: CasesPluginStart;
cases: CasesStart;
}
export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, FixtureStartDeps> {
private readonly log: Logger;
private casesPluginStart?: CasesPluginStart;
constructor(initContext: PluginInitializerContext) {
this.log = initContext.logger.get();
}
@ -39,37 +35,9 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
deps.cases.attachmentFramework.registerExternalReference(getExternalReferenceAttachment());
deps.cases.attachmentFramework.registerPersistableState(getPersistableStateAttachment());
const router = core.http.createRouter();
/**
* This simply wraps the cases patch case api so that we can test updating the status of an alert using
* the cases client interface instead of going through the case plugin's RESTful interface
*/
router.patch(
{
path: '/api/cases_user/cases',
validate: {
body: schema.object({}, { unknowns: 'allow' }),
},
},
async (context, request, response) => {
try {
const client = await this.casesPluginStart?.getCasesClientWithRequest(request);
if (!client) {
throw new Error('Cases client was undefined');
}
registerRoutes(core, this.log);
}
return response.ok({
body: await client.cases.update(request.body as CasesPatchRequest),
});
} catch (error) {
this.log.error(`CasesClientUser failure: ${error}`);
throw error;
}
}
);
}
public start(core: CoreStart, plugins: FixtureStartDeps) {
this.casesPluginStart = plugins.cases;
}
public start(core: CoreStart, plugins: FixtureStartDeps) {}
public stop() {}
}

View file

@ -0,0 +1,120 @@
/*
* 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 { createHash } from 'crypto';
import semverCompare from 'semver/functions/compare';
import type { CasesPatchRequest } from '@kbn/cases-plugin/common/api';
import { schema } from '@kbn/config-schema';
import type { CoreSetup, Logger } from '@kbn/core/server';
import type {
ExternalReferenceAttachmentType,
PersistableStateAttachmentTypeSetup,
} from '@kbn/cases-plugin/server/attachment_framework/types';
import type { FixtureStartDeps } from './plugin';
const hashParts = (parts: string[]): string => {
const hash = createHash('sha1');
const hashFeed = parts.join('-');
return hash.update(hashFeed).digest('hex');
};
const extractMigrationInfo = (type: PersistableStateAttachmentTypeSetup) => {
const migrationMap = typeof type.migrations === 'function' ? type.migrations() : type.migrations;
const migrationVersions = Object.keys(migrationMap ?? {});
migrationVersions.sort(semverCompare);
return { migrationVersions };
};
const getExternalReferenceAttachmentTypeHash = (type: ExternalReferenceAttachmentType) => {
return hashParts([type.id]);
};
const getPersistableStateAttachmentTypeHash = (type: PersistableStateAttachmentTypeSetup) => {
const { migrationVersions } = extractMigrationInfo(type);
return hashParts([type.id, migrationVersions.join(',')]);
};
export const registerRoutes = (core: CoreSetup<FixtureStartDeps>, logger: Logger) => {
const router = core.http.createRouter();
/**
* This simply wraps the cases patch case api so that we can test updating the status of an alert using
* the cases client interface instead of going through the case plugin's RESTful interface
*/
router.patch(
{
path: '/api/cases_user/cases',
validate: {
body: schema.object({}, { unknowns: 'allow' }),
},
},
async (context, request, response) => {
try {
const [_, { cases }] = await core.getStartServices();
const client = await cases.getCasesClientWithRequest(request);
return response.ok({
body: await client.cases.update(request.body as CasesPatchRequest),
});
} catch (error) {
logger.error(`CasesClientUser failure: ${error}`);
throw error;
}
}
);
router.get(
{ path: '/api/cases_fixture/registered_external_reference_attachments', validate: {} },
async (context, request, response) => {
try {
const [_, { cases }] = await core.getStartServices();
const externalReferenceAttachmentTypeRegistry =
cases.getExternalReferenceAttachmentTypeRegistry();
const allTypes = externalReferenceAttachmentTypeRegistry.list();
const hashMap = allTypes.reduce((map, type) => {
map[type.id] = getExternalReferenceAttachmentTypeHash(type);
return map;
}, {} as Record<string, string>);
return response.ok({
body: hashMap,
});
} catch (error) {
logger.error(`Error : ${error}`);
throw error;
}
}
);
router.get(
{ path: '/api/cases_fixture/registered_persistable_state_attachments', validate: {} },
async (context, request, response) => {
try {
const [_, { cases }] = await core.getStartServices();
const persistableStateAttachmentTypeRegistry =
cases.getPersistableStateAttachmentTypeRegistry();
const allTypes = persistableStateAttachmentTypeRegistry.list();
const hashMap = allTypes.reduce((map, type) => {
map[type.id] = getPersistableStateAttachmentTypeHash(type);
return map;
}, {} as Record<string, string>);
return response.ok({
body: hashMap,
});
} catch (error) {
logger.error(`Error : ${error}`);
throw error;
}
}
);
};

View file

@ -469,5 +469,41 @@ export default ({ getService }: FtrProviderContext): void => {
expectedHttpCode: 400,
});
});
it('400s when bulk creating a non registered external reference attachment type', async () => {
const postedCase = await createCase(supertest, postCaseReq);
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [
postExternalReferenceSOReq,
{ ...postExternalReferenceSOReq, externalReferenceAttachmentTypeId: 'not-exists' },
],
expectedHttpCode: 400,
});
});
// This test is intended to fail when new external reference attachment types are registered.
// To resolve, add the new external reference attachment types ID to this list. This will trigger
// a CODEOWNERS review by Response Ops.
describe('check registered external reference attachment types', () => {
const getRegisteredTypes = () => {
return supertest
.get('/api/cases_fixture/registered_external_reference_attachments')
.expect(200)
.then((response) => response.body);
};
it('should check changes on all registered external reference attachment types', async () => {
const types = await getRegisteredTypes();
expect(types).to.eql({
'.files': '559a37324c84f1f2eadcc5bce43115d09501ffe4',
'.test': 'ab2204830c67f5cf992c9aa2f7e3ead752cc60a1',
indicator: 'e1ea6f0518f2e0e4b0b5c0739efe805598cf2516',
osquery: '99bee68fce8ee84e81d67c536e063d3e1a2cee96',
});
});
});
});
};

View file

@ -261,6 +261,19 @@ export default ({ getService }: FtrProviderContext): void => {
expectedHttpCode: 400,
});
});
it('400s when bulk creating a non registered persistable state attachment type', async () => {
const postedCase = await createCase(supertest, postCaseReq);
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [
persistableStateAttachment,
{ ...persistableStateAttachment, persistableStateAttachmentTypeId: 'not-exists' },
],
expectedHttpCode: 400,
});
});
});
describe('Migrations', () => {
@ -319,5 +332,27 @@ export default ({ getService }: FtrProviderContext): void => {
});
});
});
// This test is intended to fail when new persistable state attachment types are registered.
// To resolve, add the new persistable state attachment types ID to this list. This will trigger
// a CODEOWNERS review by Response Ops.
describe('check registered persistable state attachment types', () => {
const getRegisteredTypes = () => {
return supertest
.get('/api/cases_fixture/registered_persistable_state_attachments')
.expect(200)
.then((response) => response.body);
};
it('should check changes on all registered persistable state attachment types', async () => {
const types = await getRegisteredTypes();
expect(types).to.eql({
'.test': 'dde5bd7492d266a0d54b77b5eddbeca95e19651c',
ml_anomaly_charts: 'f9bab0d17e31b89ae52a1b0d25fe117d9f23b38d',
ml_anomaly_swimlane: 'cf30664ea040f8e4190c816d093566ae22df54fe',
});
});
});
});
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { PluginSetupContract as CasesSetup } from '@kbn/cases-plugin/server/types';
import { CasesSetup } from '@kbn/cases-plugin/server/types';
import { Plugin, CoreSetup } from '@kbn/core/server';
import { getExternalReferenceAttachment } from './attachments/external_reference';
import { getPersistableStateAttachmentServer } from './attachments/persistable_state';