diff --git a/x-pack/platform/plugins/shared/integration_assistant/server/constants.ts b/x-pack/platform/plugins/shared/integration_assistant/server/constants.ts index 83577961095d7..b7edc9d5e8e7d 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/server/constants.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/server/constants.ts @@ -15,3 +15,6 @@ export enum LogFormat { STRUCTURED = 'structured', UNSTRUCTURED = 'unstructured', } +export const FLEET_ALL_ROLE = 'fleet-all' as const; +export const INTEGRATIONS_ALL_ROLE = 'integrations-all' as const; +export const ACTIONS_AND_CONNECTORS_ALL_ROLE = 'actions:execute-advanced-connectors' as const; diff --git a/x-pack/platform/plugins/shared/integration_assistant/server/integration_builder/build_integration.test.ts b/x-pack/platform/plugins/shared/integration_assistant/server/integration_builder/build_integration.test.ts index eaa50e87d41b4..01d3976b9dd6b 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/server/integration_builder/build_integration.test.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/server/integration_builder/build_integration.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { buildPackage, renderPackageManifestYAML } from './build_integration'; +import { buildPackage, isValidName, renderPackageManifestYAML } from './build_integration'; import { testIntegration } from '../../__jest__/fixtures/build_integration'; import { generateUniqueId, ensureDirSync, createSync } from '../util'; import { createDataStream } from './data_stream'; @@ -39,6 +39,7 @@ jest.mock('adm-zip', () => { return jest.fn().mockImplementation(() => ({ addLocalFolder: jest.fn(), toBuffer: jest.fn(), + addFile: jest.fn(), })); }); @@ -46,8 +47,8 @@ describe('buildPackage', () => { const packagePath = `${mockedDataPath}/integration-assistant-${mockedId}`; const integrationPath = `${packagePath}/integration-1.0.0`; - const firstDatastreamName = 'datastream_1'; - const secondDatastreamName = 'datastream_2'; + const firstDatastreamName = 'datastream_one'; + const secondDatastreamName = 'datastream_two'; const firstDataStreamInputTypes: InputType[] = ['filestream', 'kafka']; const secondDataStreamInputTypes: InputType[] = ['kafka']; @@ -74,8 +75,8 @@ describe('buildPackage', () => { const firstDataStream: DataStream = { name: firstDatastreamName, - title: 'Datastream_1', - description: 'Datastream_1 description', + title: 'datastream_one', + description: 'datastream_one description', inputTypes: firstDataStreamInputTypes, docs: firstDataStreamDocs, rawSamples: ['{"test1": "test1"}'], @@ -85,8 +86,8 @@ describe('buildPackage', () => { const secondDataStream: DataStream = { name: secondDatastreamName, - title: 'Datastream_2', - description: 'Datastream_2 description', + title: 'datastream_two', + description: 'datastream_two description', inputTypes: secondDataStreamInputTypes, docs: secondDataStreamDocs, rawSamples: ['{"test1": "test1"}'], @@ -123,15 +124,6 @@ describe('buildPackage', () => { expect(createSync).toHaveBeenCalledWith(`${integrationPath}/manifest.yml`, expect.any(String)); }); - it('Should create logo files if info is present in the integration', async () => { - testIntegration.logo = 'logo'; - - await buildPackage(testIntegration); - - expect(ensureDirSync).toHaveBeenCalledWith(`${integrationPath}/img`); - expect(createSync).toHaveBeenCalledWith(`${integrationPath}/img/logo.svg`, expect.any(Buffer)); - }); - it('Should not create logo files if info is not present in the integration', async () => { jest.clearAllMocks(); testIntegration.logo = undefined; @@ -186,19 +178,19 @@ describe('buildPackage', () => { it('Should call createReadme once with sorted fields', async () => { jest.clearAllMocks(); - const firstDSFieldsMapping = [{ name: 'name a', description: 'description 1', type: 'type 1' }]; + const firstDSFieldsMapping = [{ name: 'name_a', description: 'description 1', type: 'type 1' }]; const firstDataStreamFields = [ - { name: 'name b', description: 'description 1', type: 'type 1' }, + { name: 'name_b', description: 'description 1', type: 'type 1' }, ]; const secondDSFieldsMapping = [ - { name: 'name c', description: 'description 2', type: 'type 2' }, - { name: 'name e', description: 'description 3', type: 'type 3' }, + { name: 'name_c', description: 'description 2', type: 'type 2' }, + { name: 'name_e', description: 'description 3', type: 'type 3' }, ]; const secondDataStreamFields = [ - { name: 'name d', description: 'description 2', type: 'type 2' }, + { name: 'name_d', description: 'description 2', type: 'type 2' }, ]; (createFieldMapping as jest.Mock).mockReturnValueOnce(firstDSFieldsMapping); @@ -217,17 +209,17 @@ describe('buildPackage', () => { { datastream: firstDatastreamName, fields: [ - { name: 'name a', description: 'description 1', type: 'type 1' }, + { name: 'name_a', description: 'description 1', type: 'type 1' }, - { name: 'name b', description: 'description 1', type: 'type 1' }, + { name: 'name_b', description: 'description 1', type: 'type 1' }, ], }, { datastream: secondDatastreamName, fields: [ - { name: 'name c', description: 'description 2', type: 'type 2' }, - { name: 'name d', description: 'description 2', type: 'type 2' }, - { name: 'name e', description: 'description 3', type: 'type 3' }, + { name: 'name_c', description: 'description 2', type: 'type 2' }, + { name: 'name_d', description: 'description 2', type: 'type 2' }, + { name: 'name_e', description: 'description 3', type: 'type 3' }, ], }, ] @@ -239,13 +231,13 @@ describe('renderPackageManifestYAML', () => { test('generates the package manifest correctly', () => { const integration: Integration = { title: 'Sample Integration', - name: 'sample-integration', + name: 'sample_integration', description: ' This is a sample integration\n\nWith multiple lines and weird spacing. \n\n And more lines ', logo: 'some-logo.png', dataStreams: [ { - name: 'data-stream-1', + name: 'data_stream_one', title: 'Data Stream 1', description: 'This is data stream 1', inputTypes: ['filestream'], @@ -257,7 +249,7 @@ describe('renderPackageManifestYAML', () => { samplesFormat: { name: 'ndjson', multiline: false }, }, { - name: 'data-stream-2', + name: 'data_stream_two', title: 'Data Stream 2', description: 'This is data stream 2\nWith multiple lines of description\nBut otherwise, nothing special', @@ -292,3 +284,59 @@ describe('renderPackageManifestYAML', () => { }); }); }); + +describe('isValidName', () => { + it('should return true for valid names', () => { + expect(isValidName('validName')).toBe(true); + expect(isValidName('Valid_Name')).toBe(true); + expect(isValidName('anotherValidName')).toBe(true); + }); + + it('should return false for names with numbers', () => { + expect(isValidName('invalid123')).toBe(false); + expect(isValidName('123invalid')).toBe(false); + expect(isValidName('invalid_123')).toBe(false); + }); + + it('should return false for empty string', () => { + expect(isValidName('')).toBe(false); + }); + + it('should return false for names with spaces', () => { + expect(isValidName('invalid name')).toBe(false); + expect(isValidName(' invalid')).toBe(false); + expect(isValidName('invalid ')).toBe(false); + expect(isValidName('invalid name with spaces')).toBe(false); + }); + + it('should return false for names with special characters', () => { + expect(isValidName('invalid@name')).toBe(false); + expect(isValidName('invalid#name')).toBe(false); + expect(isValidName('invalid$name')).toBe(false); + expect(isValidName('invalid%name')).toBe(false); + expect(isValidName('invalid^name')).toBe(false); + expect(isValidName('invalid&name')).toBe(false); + expect(isValidName('invalid*name')).toBe(false); + expect(isValidName('invalid(name')).toBe(false); + expect(isValidName('invalid/name')).toBe(false); + }); + + it('should return false for names with dashes', () => { + expect(isValidName('invalid-name')).toBe(false); + expect(isValidName('invalid-name-with-dashes')).toBe(false); + }); + + it('should return false for names with periods', () => { + expect(isValidName('invalid.name')).toBe(false); + expect(isValidName('invalid.name.with.periods')).toBe(false); + }); + + it('should return false for names with mixed invalid characters', () => { + expect(isValidName('invalid@name#with$special%characters')).toBe(false); + expect(isValidName('invalid name with spaces and 123')).toBe(false); + }); + + it('should return false for names with empty string', () => { + expect(isValidName('')).toBe(false); + }); +}); diff --git a/x-pack/platform/plugins/shared/integration_assistant/server/integration_builder/build_integration.ts b/x-pack/platform/plugins/shared/integration_assistant/server/integration_builder/build_integration.ts index 785c11125afd9..e63e7d0648da7 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/server/integration_builder/build_integration.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/server/integration_builder/build_integration.ts @@ -34,6 +34,12 @@ function configureNunjucks() { export async function buildPackage(integration: Integration): Promise { configureNunjucks(); + if (!isValidName(integration.name)) { + throw new Error( + `Invalid integration name: ${integration.name}, Should only contain letters and underscores` + ); + } + const workingDir = joinPath(getDataPath(), `integration-assistant-${generateUniqueId()}`); const packageDirectoryName = `${integration.name}-${initialVersion}`; const packageDir = createDirectories(workingDir, integration, packageDirectoryName); @@ -41,6 +47,11 @@ export async function buildPackage(integration: Integration): Promise { const dataStreamsDir = joinPath(packageDir, 'data_stream'); const fieldsPerDatastream = integration.dataStreams.map((dataStream) => { const dataStreamName = dataStream.name; + if (!isValidName(dataStreamName)) { + throw new Error( + `Invalid datastream name: ${dataStreamName}, Should only contain letters and underscores` + ); + } const specificDataStreamDir = joinPath(dataStreamsDir, dataStreamName); const dataStreamFields = createDataStream(integration.name, specificDataStreamDir, dataStream); @@ -60,12 +71,15 @@ export async function buildPackage(integration: Integration): Promise { }); createReadme(packageDir, integration.name, integration.dataStreams, fieldsPerDatastream); - const zipBuffer = await createZipArchive(workingDir, packageDirectoryName); + const zipBuffer = await createZipArchive(integration, workingDir, packageDirectoryName); removeDirSync(workingDir); return zipBuffer; } - +export function isValidName(input: string): boolean { + const regex = /^[a-zA-Z_]+$/; + return input.length > 0 && regex.test(input); +} function createDirectories( workingDir: string, integration: Integration, @@ -84,17 +98,6 @@ function createPackage(packageDir: string, integration: Integration): void { createPackageManifest(packageDir, integration); // Skipping creation of system tests temporarily for custom package generation // createPackageSystemTests(packageDir, integration); - if (integration?.logo !== undefined) { - createLogo(packageDir, integration.logo); - } -} - -function createLogo(packageDir: string, logo: string): void { - const logoDir = joinPath(packageDir, 'img'); - ensureDirSync(logoDir); - - const buffer = Buffer.from(logo, 'base64'); - createSync(joinPath(logoDir, 'logo.svg'), buffer); } function createBuildFile(packageDir: string): void { @@ -113,10 +116,20 @@ function createChangelog(packageDir: string): void { createSync(joinPath(packageDir, 'changelog.yml'), changelogTemplate); } -async function createZipArchive(workingDir: string, packageDirectoryName: string): Promise { +async function createZipArchive( + integration: Integration, + workingDir: string, + packageDirectoryName: string +): Promise { const tmpPackageDir = joinPath(workingDir, packageDirectoryName); const zip = new AdmZip(); zip.addLocalFolder(tmpPackageDir, packageDirectoryName); + + if (integration.logo) { + const logoDir = joinPath(packageDirectoryName, 'img/logo.svg'); + const logoBuffer = Buffer.from(integration.logo, 'base64'); + zip.addFile(logoDir, logoBuffer); + } const buffer = zip.toBuffer(); return buffer; } diff --git a/x-pack/platform/plugins/shared/integration_assistant/server/routes/analyze_logs_routes.ts b/x-pack/platform/plugins/shared/integration_assistant/server/routes/analyze_logs_routes.ts index 93ac55f6f712c..de6f3fc054dc6 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/server/routes/analyze_logs_routes.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/server/routes/analyze_logs_routes.ts @@ -10,7 +10,12 @@ import { getRequestAbortedSignal } from '@kbn/data-plugin/server'; import { APMTracer } from '@kbn/langchain/server/tracers/apm'; import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith'; import { ANALYZE_LOGS_PATH, AnalyzeLogsRequestBody, AnalyzeLogsResponse } from '../../common'; -import { ROUTE_HANDLER_TIMEOUT } from '../constants'; +import { + ACTIONS_AND_CONNECTORS_ALL_ROLE, + FLEET_ALL_ROLE, + INTEGRATIONS_ALL_ROLE, + ROUTE_HANDLER_TIMEOUT, +} from '../constants'; import { getLogFormatDetectionGraph } from '../graphs/log_type_detection/graph'; import type { IntegrationAssistantRouteHandlerContext } from '../plugin'; import { getLLMClass, getLLMType } from '../util/llm'; @@ -39,9 +44,11 @@ export function registerAnalyzeLogsRoutes( version: '1', security: { authz: { - enabled: false, - reason: - 'This route is opted out from authorization because the privileges are not defined yet.', + requiredPrivileges: [ + FLEET_ALL_ROLE, + INTEGRATIONS_ALL_ROLE, + ACTIONS_AND_CONNECTORS_ALL_ROLE, + ], }, }, validate: { diff --git a/x-pack/platform/plugins/shared/integration_assistant/server/routes/build_integration_routes.ts b/x-pack/platform/plugins/shared/integration_assistant/server/routes/build_integration_routes.ts index f62d6d55f933d..94bcbfaedaebf 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/server/routes/build_integration_routes.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/server/routes/build_integration_routes.ts @@ -14,6 +14,11 @@ import { withAvailability } from './with_availability'; import { isErrorThatHandlesItsOwnResponse } from '../lib/errors'; import { handleCustomErrors } from './routes_util'; import { GenerationErrorCode } from '../../common/constants'; +import { + ACTIONS_AND_CONNECTORS_ALL_ROLE, + FLEET_ALL_ROLE, + INTEGRATIONS_ALL_ROLE, +} from '../constants'; export function registerIntegrationBuilderRoutes( router: IRouter ) { @@ -27,9 +32,11 @@ export function registerIntegrationBuilderRoutes( version: '1', security: { authz: { - enabled: false, - reason: - 'This route is opted out from authorization because the privileges are not defined yet.', + requiredPrivileges: [ + FLEET_ALL_ROLE, + INTEGRATIONS_ALL_ROLE, + ACTIONS_AND_CONNECTORS_ALL_ROLE, + ], }, }, validate: { diff --git a/x-pack/platform/plugins/shared/integration_assistant/server/routes/categorization_routes.ts b/x-pack/platform/plugins/shared/integration_assistant/server/routes/categorization_routes.ts index 5f63ed9c7bf3c..72aaf1d963efb 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/server/routes/categorization_routes.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/server/routes/categorization_routes.ts @@ -14,7 +14,12 @@ import { CategorizationRequestBody, CategorizationResponse, } from '../../common'; -import { ROUTE_HANDLER_TIMEOUT } from '../constants'; +import { + ACTIONS_AND_CONNECTORS_ALL_ROLE, + FLEET_ALL_ROLE, + INTEGRATIONS_ALL_ROLE, + ROUTE_HANDLER_TIMEOUT, +} from '../constants'; import { getCategorizationGraph } from '../graphs/categorization'; import type { IntegrationAssistantRouteHandlerContext } from '../plugin'; import { getLLMClass, getLLMType } from '../util/llm'; @@ -42,9 +47,11 @@ export function registerCategorizationRoutes( version: '1', security: { authz: { - enabled: false, - reason: - 'This route is opted out from authorization because the privileges are not defined yet.', + requiredPrivileges: [ + FLEET_ALL_ROLE, + INTEGRATIONS_ALL_ROLE, + ACTIONS_AND_CONNECTORS_ALL_ROLE, + ], }, }, validate: { diff --git a/x-pack/platform/plugins/shared/integration_assistant/server/routes/cel_routes.ts b/x-pack/platform/plugins/shared/integration_assistant/server/routes/cel_routes.ts index 9ce16c3909119..63faeb6a71f26 100644 --- a/x-pack/platform/plugins/shared/integration_assistant/server/routes/cel_routes.ts +++ b/x-pack/platform/plugins/shared/integration_assistant/server/routes/cel_routes.ts @@ -10,7 +10,12 @@ import { getRequestAbortedSignal } from '@kbn/data-plugin/server'; import { APMTracer } from '@kbn/langchain/server/tracers/apm'; import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith'; import { CEL_INPUT_GRAPH_PATH, CelInputRequestBody, CelInputResponse } from '../../common'; -import { ROUTE_HANDLER_TIMEOUT } from '../constants'; +import { + ACTIONS_AND_CONNECTORS_ALL_ROLE, + FLEET_ALL_ROLE, + INTEGRATIONS_ALL_ROLE, + ROUTE_HANDLER_TIMEOUT, +} from '../constants'; import { getCelGraph } from '../graphs/cel'; import type { IntegrationAssistantRouteHandlerContext } from '../plugin'; import { getLLMClass, getLLMType } from '../util/llm'; @@ -34,9 +39,11 @@ export function registerCelInputRoutes(router: IRouter