From c7f4cb719beafc01b91a721f347be3cf4dd51b45 Mon Sep 17 00:00:00 2001 From: Julien Richard Date: Sun, 12 Jan 2025 14:57:42 +0100 Subject: [PATCH] [backend] Global rework on application logging --- .../opencti-graphql/src/config/conf.js | 107 +++++++----------- .../opencti-graphql/src/config/credentials.ts | 2 +- .../opencti-graphql/src/config/errors.js | 9 +- .../importCsv/importCsv-connector.ts | 8 +- .../opencti-graphql/src/database/ai-llm.ts | 4 +- .../opencti-graphql/src/database/engine.js | 2 +- .../src/database/file-search.js | 4 +- .../src/database/file-storage-helper.ts | 5 +- .../src/database/file-storage.js | 4 +- .../src/domain/backgroundTask-common.js | 4 +- .../src/domain/backgroundTask.js | 6 +- .../opencti-graphql/src/domain/stix.js | 12 +- .../src/domain/stixCoreObject.js | 30 +++-- .../opencti-graphql/src/domain/user.js | 2 +- .../src/graphql/authDirective.js | 4 +- .../src/graphql/loggerPlugin.js | 56 ++------- .../opencti-graphql/src/http/httpServer.js | 7 +- .../opencti-graphql/src/instrumentation.js | 25 ++-- .../src/manager/connectorManager.js | 2 +- .../src/manager/indicatorDecayManager.ts | 2 +- .../draftWorkspace/draftWorkspace-domain.ts | 4 +- .../exclusionList/exclusionList-domain.ts | 13 ++- .../modules/playbook/playbook-components.ts | 2 +- .../src/modules/xtm/xtm-domain.js | 12 +- .../telemetry/BatchExportingMetricReader.ts | 3 +- .../manager/indicatorDecayManager-test.ts | 4 +- .../tests/01-unit/utils/logger-test.ts | 29 +++-- .../02-resolvers/publicDashboard-test.js | 2 +- 28 files changed, 184 insertions(+), 180 deletions(-) diff --git a/opencti-platform/opencti-graphql/src/config/conf.js b/opencti-platform/opencti-graphql/src/config/conf.js index 4c382c061108..c52940cfb363 100644 --- a/opencti-platform/opencti-graphql/src/config/conf.js +++ b/opencti-platform/opencti-graphql/src/config/conf.js @@ -9,7 +9,7 @@ import DailyRotateFile from 'winston-daily-rotate-file'; import { HttpsProxyAgent } from 'https-proxy-agent'; import { HttpProxyAgent } from 'http-proxy-agent'; import { v4 as uuid } from 'uuid'; -import { GraphQLError } from 'graphql/error'; +import { GraphQLError } from 'graphql/index'; import * as O from '../schema/internalObject'; import * as M from '../schema/stixMetaObject'; import { @@ -30,7 +30,7 @@ import { ENTITY_TYPE_ENTITY_SETTING } from '../modules/entitySetting/entitySetti import { ENTITY_TYPE_MANAGER_CONFIGURATION } from '../modules/managerConfiguration/managerConfiguration-types'; import { ENTITY_TYPE_WORKSPACE } from '../modules/workspace/workspace-types'; import { ENTITY_TYPE_NOTIFIER } from '../modules/notifier/notifier-types'; -import { UnknownError, UnsupportedError } from './errors'; +import { UNKNOWN_ERROR, UnknownError, UnsupportedError } from './errors'; import { ENTITY_TYPE_PUBLIC_DASHBOARD } from '../modules/publicDashboard/publicDashboard-types'; import { AI_BUS } from '../modules/ai/ai-types'; import { SUPPORT_BUS } from '../modules/support/support-types'; @@ -47,8 +47,9 @@ const LINUX_CERTFILES = [ const DEFAULT_ENV = 'production'; export const OPENCTI_SESSION = 'opencti_session'; - export const PLATFORM_VERSION = pjson.version; +const LOG_APP = 'APP'; +const LOG_AUDIT = 'AUDIT'; export const booleanConf = (key, defaultValue = true) => { const configValue = nconf.get(key); @@ -107,7 +108,19 @@ export const extendedErrors = (metaExtension) => { } return {}; }; -const limitMetaErrorComplexityWrapper = (obj, acc, current_depth = 0) => { +const convertErrorObject = (error) => { + if (error instanceof GraphQLError) { + const extensions = error.extensions ?? {}; + const extensionsData = extensions.data ?? {}; + const { ...attributes } = extensionsData; + return { name: extensions.code ?? error.name, code: extensions.code, message: error.message, stack: error.stack, attributes }; + } + if (error instanceof Error) { + return { name: error.name, code: UNKNOWN_ERROR, message: error.message, stack: error.stack }; + } + return error; +}; +const prepareLogMetadataComplexityWrapper = (obj, acc, current_depth = 0) => { const noMaxDepth = current_depth < appLogLevelMaxDepthSize; const noMaxKeys = acc.current_nb_key < appLogLevelMaxDepthKeys; const isNotAKeyFunction = typeof obj !== 'function'; @@ -118,7 +131,7 @@ const limitMetaErrorComplexityWrapper = (obj, acc, current_depth = 0) => { // Recursively process each item in the truncated array const processedArray = []; for (let i = 0; i < limitedArray.length; i += 1) { - processedArray[i] = limitMetaErrorComplexityWrapper(limitedArray[i], acc, current_depth); + processedArray[i] = prepareLogMetadataComplexityWrapper(limitedArray[i], acc, current_depth); } return processedArray; } @@ -126,23 +139,26 @@ const limitMetaErrorComplexityWrapper = (obj, acc, current_depth = 0) => { return `${obj.substring(0, appLogLevelMaxStringSize - 3)}...`; } if (typeof obj === 'object') { + const workingObject = convertErrorObject(obj); // Create a new object to hold the processed properties const limitedObject = {}; - const keys = Object.keys(obj); // Get the keys of the object + const keys = Object.keys(workingObject); // Get the keys of the object const newDepth = current_depth + 1; for (let i = 0; i < keys.length; i += 1) { acc.current_nb_key += 1; const key = keys[i]; - limitedObject[key] = limitMetaErrorComplexityWrapper(obj[key], acc, newDepth); + limitedObject[key] = prepareLogMetadataComplexityWrapper(workingObject[key], acc, newDepth); } return limitedObject; } } return obj; }; -export const limitMetaErrorComplexity = (obj) => { +// Prepare the data - Format the errors and limit complexity +export const prepareLogMetadata = (obj, extra = {}) => { const acc = { current_nb_key: 0 }; - return limitMetaErrorComplexityWrapper(obj, acc); + const protectedObj = prepareLogMetadataComplexityWrapper(obj, acc); + return { ...extra, ...protectedObj, version: PLATFORM_VERSION }; }; const appLogTransports = []; @@ -230,74 +246,37 @@ const telemetryLogger = winston.createLogger({ transports: telemetryLogTransports, }); -// Specific case to fail any test that produce an error log -const LOG_APP = 'APP'; -const buildMetaErrors = (error) => { - const errors = []; - if (error instanceof GraphQLError) { - const extensions = error.extensions ?? {}; - const extensionsData = extensions.data ?? {}; - const { cause: _, ...attributes } = extensionsData; - const baseError = { name: extensions.code ?? error.name, message: error.message, stack: error.stack, attributes }; - errors.push(baseError); - if (extensionsData.cause && extensionsData.cause instanceof Error) { - errors.push(...buildMetaErrors(extensionsData.cause)); - } - } else if (error instanceof Error) { - const baseError = { name: error.name, message: error.message, stack: error.stack }; - errors.push(baseError); - } - return errors; -}; -const addBasicMetaInformation = (category, error, meta) => { - const logMeta = { ...meta }; - if (error) logMeta.errors = buildMetaErrors(error); - return { category, version: PLATFORM_VERSION, ...logMeta }; -}; - export const logS3Debug = { debug: (message, detail) => { - logApp._log('info', message, null, { detail }); + logApp._log('info', message, { detail }); }, }; export const logApp = { - _log: (level, message, error, meta = {}) => { + _log: (level, message, meta = {}) => { if (appLogTransports.length > 0 && appLogger.isLevelEnabled(level)) { - const data = addBasicMetaInformation(LOG_APP, error, { ...meta, source: 'backend' }); - // Prevent meta information to be too massive. - const limitedData = limitMetaErrorComplexity(data); - appLogger.log(level, message, limitedData); - } - }, - _logWithError: (level, messageOrError, meta = {}) => { - const isError = messageOrError instanceof Error; - const message = isError ? messageOrError.message : messageOrError; - let error = null; - if (isError) { - if (messageOrError instanceof GraphQLError) { - error = messageOrError; - } else { - error = UnknownError(message, { cause: messageOrError }); + const data = prepareLogMetadata(meta, { category: LOG_APP, source: 'backend' }); + appLogger.log(level, message, data); + // Only add in support package starting warn level + if (appLogger.isLevelEnabled('warn')) { + supportLogger.log(level, message, data); } } - logApp._log(level, message, error, meta); - supportLogger.log(level, message, addBasicMetaInformation(LOG_APP, error, { ...meta, source: 'backend' })); }, - debug: (message, meta = {}) => logApp._log('debug', message, null, meta), - info: (message, meta = {}) => logApp._log('info', message, null, meta), - warn: (messageOrError, meta = {}) => logApp._logWithError('warn', messageOrError, meta), - error: (messageOrError, meta = {}) => logApp._logWithError('error', messageOrError, meta), + debug: (message, meta = {}) => logApp._log('debug', message, meta), + info: (message, meta = {}) => logApp._log('info', message, meta), + warn: (message, meta = {}) => logApp._log('warn', message, meta), + error: (message, meta = {}) => logApp._log('error', message, meta), query: (options, errCallback) => appLogger.query(options, errCallback), }; -const LOG_AUDIT = 'AUDIT'; export const logAudit = { _log: (level, user, operation, meta = {}) => { if (auditLogTransports.length > 0) { const metaUser = { email: user.user_email, ...user.origin }; const logMeta = isEmpty(meta) ? { auth: metaUser } : { resource: meta, auth: metaUser }; - auditLogger.log(level, operation, addBasicMetaInformation(LOG_AUDIT, null, logMeta)); + const data = prepareLogMetadata(logMeta, { category: LOG_AUDIT, source: 'backend' }); + auditLogger.log(level, operation, data); } }, info: (user, operation, meta = {}) => logAudit._log('info', user, operation, meta), @@ -305,12 +284,12 @@ export const logAudit = { }; export const logFrontend = { - _log: (level, message, error, meta = {}) => { - const info = { ...meta, source: 'frontend' }; - appLogger.log(level, message, addBasicMetaInformation(LOG_APP, error, info)); - supportLogger.log(level, message, addBasicMetaInformation(LOG_APP, error, info)); + _log: (level, message, meta = {}) => { + const data = prepareLogMetadata(meta, { category: LOG_APP, source: 'frontend' }); + appLogger.log(level, message, data); + supportLogger.log(level, message, data); }, - error: (message, meta = {}) => logFrontend._log('error', message, null, meta), + error: (message, meta = {}) => logFrontend._log('error', message, meta), }; export const logTelemetry = { diff --git a/opencti-platform/opencti-graphql/src/config/credentials.ts b/opencti-platform/opencti-graphql/src/config/credentials.ts index 77d5a8ff6786..1c0b2e304f94 100644 --- a/opencti-platform/opencti-graphql/src/config/credentials.ts +++ b/opencti-platform/opencti-graphql/src/config/credentials.ts @@ -41,7 +41,7 @@ export const enrichWithRemoteCredentials = async (prefix: string, baseConfigurat return secretResult; } } catch (e: any) { - logApp.error('[OPENCTI] Remote credentials data fail to fetch, fallback', { error: e, provider, source: prefix }); + logApp.error('[OPENCTI] Remote credentials data fail to fetch, fallback', { cause: e, provider, source: prefix }); } } // No compatible provider available diff --git a/opencti-platform/opencti-graphql/src/config/errors.js b/opencti-platform/opencti-graphql/src/config/errors.js index 56c6ac60cbf6..720b4abfdcca 100644 --- a/opencti-platform/opencti-graphql/src/config/errors.js +++ b/opencti-platform/opencti-graphql/src/config/errors.js @@ -87,7 +87,7 @@ export const ConfigurationError = (reason, data) => error(CONFIGURATION_ERROR, r ...data, }); -const UNKNOWN_ERROR = 'UNKNOWN_ERROR'; +export const UNKNOWN_ERROR = 'UNKNOWN_ERROR'; export const UnknownError = (reason, data) => error(UNKNOWN_ERROR, reason || 'An unknown error has occurred', { http_status: 500, genre: CATEGORY_TECHNICAL, @@ -148,6 +148,12 @@ export const ValidationError = (message, field, data) => error(VALIDATION_ERROR, ...(data ?? {}), }); +export const RESOURCE_NOT_FOUND_ERROR = 'RESOURCE_NOT_FOUND'; +export const ResourceNotFoundError = (data) => error(RESOURCE_NOT_FOUND_ERROR, 'Resource not found', { + http_status: 404, + ...data, +}); + const TYPE_LOCK = 'LOCK_ERROR'; export const TYPE_LOCK_ERROR = 'ExecutionError'; export const LockTimeoutError = (data, reason) => error(TYPE_LOCK, reason ?? 'Execution timeout, too many concurrent call on the same entities', { @@ -161,6 +167,7 @@ export const FUNCTIONAL_ERRORS = [ ALREADY_DELETED_ERROR, MISSING_REF_ERROR, VALIDATION_ERROR, + RESOURCE_NOT_FOUND_ERROR, TYPE_LOCK_ERROR ]; // endregion diff --git a/opencti-platform/opencti-graphql/src/connector/importCsv/importCsv-connector.ts b/opencti-platform/opencti-graphql/src/connector/importCsv/importCsv-connector.ts index 3379a47cbd77..2446755a015b 100644 --- a/opencti-platform/opencti-graphql/src/connector/importCsv/importCsv-connector.ts +++ b/opencti-platform/opencti-graphql/src/connector/importCsv/importCsv-connector.ts @@ -46,7 +46,7 @@ const processCSVforWorkbench = async (context: AuthContext, fileId: string, opts hasError = true; const errorData = { error: error.message, source: fileId }; await reportExpectation(context, applicantUser, workId, errorData); - logApp.error(`${LOG_PREFIX} Error streaming the CSV data`, { error }); + logApp.error(`${LOG_PREFIX} Error streaming the CSV data`, { cause: error }); }).on('end', async () => { if (!hasError) { // it's fine to use deprecated bundleProcess since this whole method is also deprecated for drafts. @@ -132,12 +132,12 @@ export const processCSVforWorkers = async (context: AuthContext, fileId: string, totalBundlesCount += bundleCount; } catch (error: any) { const errorData = { error: error.message, source: `${fileId}, from ${lineNumber} and ${BULK_LINE_PARSING_NUMBER} following lines.` }; - logApp.error(`${LOG_PREFIX} CSV line parsing error`, { error: errorData }); + logApp.error(`${LOG_PREFIX} CSV line parsing error`, { cause: errorData }); await reportExpectation(context, applicantUser, workId, errorData); } } } catch (error: any) { - logApp.error(`${LOG_PREFIX} CSV global parsing error`, { error }); + logApp.error(`${LOG_PREFIX} CSV global parsing error`, { cause: error }); const errorData = { error: error.message, source: fileId }; await reportExpectation(context, applicantUser, workId, errorData); // circuit breaker @@ -204,7 +204,7 @@ const consumeQueueCallback = async (context: AuthContext, message: string) => { await processCSVforWorkers(context, fileId, opts); } } catch (error: any) { - logApp.error(`${LOG_PREFIX} CSV global parsing error`, { error, source: fileId }); + logApp.error(`${LOG_PREFIX} CSV global parsing error`, { cause: error, source: fileId }); const errorData = { error: error.stack, source: fileId }; await reportExpectation(context, applicantUser, workId, errorData); } diff --git a/opencti-platform/opencti-graphql/src/database/ai-llm.ts b/opencti-platform/opencti-graphql/src/database/ai-llm.ts index 232adee08b73..3e58c500319a 100644 --- a/opencti-platform/opencti-graphql/src/database/ai-llm.ts +++ b/opencti-platform/opencti-graphql/src/database/ai-llm.ts @@ -57,7 +57,7 @@ export const queryMistralAi = async (busId: string | null, question: string, use logApp.error('[AI] No response from MistralAI', { busId, question }); return 'No response from MistralAI'; } catch (err) { - logApp.error('[AI] Cannot query MistralAI', { error: err }); + logApp.error('[AI] Cannot query MistralAI', { cause: err }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error return `An error occurred: ${err.toString()}`; @@ -92,7 +92,7 @@ export const queryChatGpt = async (busId: string | null, question: string, user: logApp.error('[AI] No response from OpenAI', { busId, question }); return 'No response from OpenAI'; } catch (err) { - logApp.error('[AI] Cannot query OpenAI', { error: err }); + logApp.error('[AI] Cannot query OpenAI', { cause: err }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error return `An error occurred: ${err.toString()}`; diff --git a/opencti-platform/opencti-graphql/src/database/engine.js b/opencti-platform/opencti-graphql/src/database/engine.js index e74b8ab69d4e..b6e76620cae0 100644 --- a/opencti-platform/opencti-graphql/src/database/engine.js +++ b/opencti-platform/opencti-graphql/src/database/engine.js @@ -1602,7 +1602,7 @@ export const elFindByIds = async (context, user, ids, opts = {}) => { }); const elements = data.hits.hits; if (elements.length > workingIds.length) { - logApp.warn('Search query returned more elements than expected', workingIds); + logApp.warn('Search query returned more elements than expected', { ids: workingIds }); } if (elements.length > 0) { const convertedHits = await elConvertHits(elements, { withoutRels }); diff --git a/opencti-platform/opencti-graphql/src/database/file-search.js b/opencti-platform/opencti-graphql/src/database/file-search.js index b074111e2402..eef8a4db5b6b 100644 --- a/opencti-platform/opencti-graphql/src/database/file-search.js +++ b/opencti-platform/opencti-graphql/src/database/file-search.js @@ -66,11 +66,11 @@ export const elIndexFiles = async (context, user, files) => { await elIndex(INDEX_FILES, documentBody, { pipeline: 'attachment' }); } catch (err) { // catch & log error - logApp.error('Error on file indexing', { message: err.message, causeStack: err.data?.cause?.stack, stack: err.stack, file_id }); + logApp.error('Error on file indexing', { cause: err, file_id }); // try to index without file content const documentWithoutFileData = R.dissoc('file_data', documentBody); await elIndex(INDEX_FILES, documentWithoutFileData).catch((e) => { - logApp.error('Error in fallback file indexing', { message: e.message, cause: e.cause, file_id }); + logApp.error('Error in fallback file indexing', { message: e.message, cause: e, file_id }); }); } } diff --git a/opencti-platform/opencti-graphql/src/database/file-storage-helper.ts b/opencti-platform/opencti-graphql/src/database/file-storage-helper.ts index e494ed77f205..04af9d38f52d 100644 --- a/opencti-platform/opencti-graphql/src/database/file-storage-helper.ts +++ b/opencti-platform/opencti-graphql/src/database/file-storage-helper.ts @@ -9,6 +9,7 @@ import { allFilesForPaths, EXPORT_STORAGE_PATH, FROM_TEMPLATE_STORAGE_PATH, IMPO import { deleteWorkForSource } from '../domain/work'; import { ENTITY_TYPE_SUPPORT_PACKAGE } from '../modules/support/support-types'; import { getDraftContext } from '../utils/draftContext'; +import { UnsupportedError } from '../config/errors'; interface FileUploadOpts { entity?:BasicStoreBase | unknown, // entity on which the file is uploaded @@ -54,7 +55,9 @@ interface S3File { * @param opts */ export const uploadToStorage = (context: AuthContext, user: AuthUser, filePath: string, fileUpload: FileUploadData, opts: FileUploadOpts) => { - if (getDraftContext(context, user)) throw new Error('Cannot upload file in draft context'); + if (getDraftContext(context, user)) { + throw UnsupportedError('Cannot upload file in draft context'); + } return upload(context, user, filePath, fileUpload, opts); }; diff --git a/opencti-platform/opencti-graphql/src/database/file-storage.js b/opencti-platform/opencti-graphql/src/database/file-storage.js index 753cee7c9fbb..daa46fd8c56b 100644 --- a/opencti-platform/opencti-graphql/src/database/file-storage.js +++ b/opencti-platform/opencti-graphql/src/database/file-storage.js @@ -173,7 +173,7 @@ export const downloadFile = async (id) => { } return object.Body; } catch (err) { - logApp.error('[FILE STORAGE] Cannot retrieve file from S3', { error: err, fileId: id }); + logApp.error('[FILE STORAGE] Cannot retrieve file from S3', { cause: err, fileId: id }); return null; } }; @@ -213,7 +213,7 @@ export const copyFile = async (context, copyProps) => { logApp.info('[FILE STORAGE] Copy file to S3 in success', { document: file, sourceId, targetId }); return file; } catch (err) { - logApp.error('[FILE STORAGE] Cannot copy file in S3', { error: err, sourceId, targetId }); + logApp.error('[FILE STORAGE] Cannot copy file in S3', { cause: err, sourceId, targetId }); return null; } }; diff --git a/opencti-platform/opencti-graphql/src/domain/backgroundTask-common.js b/opencti-platform/opencti-graphql/src/domain/backgroundTask-common.js index f33e2a292539..6441991f6dc2 100644 --- a/opencti-platform/opencti-graphql/src/domain/backgroundTask-common.js +++ b/opencti-platform/opencti-graphql/src/domain/backgroundTask-common.js @@ -306,7 +306,9 @@ const authorizedMembersForTask = (user, scope) => { }; export const createListTask = async (context, user, input) => { - if (getDraftContext(context, user)) throw new Error('Cannot create background task in draft'); + if (getDraftContext(context, user)) { + throw UnsupportedError('Cannot create background task in draft'); + } const { actions, ids, scope } = input; await checkActionValidity(context, user, input, scope, TASK_TYPE_LIST); const task = createDefaultTask(user, input, TASK_TYPE_LIST, ids.length, scope); diff --git a/opencti-platform/opencti-graphql/src/domain/backgroundTask.js b/opencti-platform/opencti-graphql/src/domain/backgroundTask.js index bd8f8478b2ec..d9affa97ea83 100644 --- a/opencti-platform/opencti-graphql/src/domain/backgroundTask.js +++ b/opencti-platform/opencti-graphql/src/domain/backgroundTask.js @@ -9,7 +9,7 @@ import { ABSTRACT_STIX_CORE_OBJECT, ABSTRACT_STIX_CORE_RELATIONSHIP, RULE_PREFIX import { buildEntityFilters, listEntities, storeLoadById } from '../database/middleware-loader'; import { checkActionValidity, createDefaultTask, TASK_TYPE_QUERY, TASK_TYPE_RULE } from './backgroundTask-common'; import { publishUserAction } from '../listener/UserActionListener'; -import { ForbiddenAccess } from '../config/errors'; +import { ForbiddenAccess, UnsupportedError } from '../config/errors'; import { STIX_SIGHTING_RELATIONSHIP } from '../schema/stixSightingRelationship'; import { ENTITY_TYPE_VOCABULARY } from '../modules/vocabulary/vocabulary-types'; import { ENTITY_TYPE_NOTIFICATION } from '../modules/notification/notification-types'; @@ -130,7 +130,9 @@ export const createRuleTask = async (context, user, ruleDefinition, input) => { }; export const createQueryTask = async (context, user, input) => { - if (getDraftContext(context, user)) throw new Error('Cannot create background task in draft'); + if (getDraftContext(context, user)) { + throw UnsupportedError('Cannot create background task in draft'); + } const { actions, filters, excluded_ids = [], search = null, scope } = input; await checkActionValidity(context, user, input, scope, TASK_TYPE_QUERY); const queryData = await executeTaskQuery(context, user, filters, search, scope); diff --git a/opencti-platform/opencti-graphql/src/domain/stix.js b/opencti-platform/opencti-graphql/src/domain/stix.js index b5117656c2c9..13bb24a57260 100644 --- a/opencti-platform/opencti-graphql/src/domain/stix.js +++ b/opencti-platform/opencti-graphql/src/domain/stix.js @@ -89,7 +89,9 @@ export const sendStixBundle = async (context, user, connectorId, bundle) => { }; export const askListExport = async (context, user, exportContext, format, selectedIds, listParams, type, contentMaxMarkings, fileMarkings) => { - if (!exportContext || !exportContext?.entity_type) throw new Error('entity_type is missing from askListExport'); + if (!exportContext || !exportContext?.entity_type) { + throw FunctionalError('entity_type is missing from askListExport'); + } const connectors = await connectorsForExport(context, user, format, true); const markingLevels = await Promise.all(contentMaxMarkings.map(async (id) => { @@ -273,7 +275,9 @@ const createSharingTask = async (context, type, containerId, organizationId) => }; export const addOrganizationRestriction = async (context, user, fromId, organizationId) => { - if (getDraftContext(context, user)) throw new Error('Cannot restrict organization in draft'); + if (getDraftContext(context, user)) { + throw UnsupportedError('Cannot restrict organization in draft'); + } const from = await internalLoadById(context, user, fromId); const updates = [{ key: INPUT_GRANTED_REFS, value: [organizationId], operation: UPDATE_OPERATION_ADD }]; // We skip references validation when updating organization sharing @@ -285,7 +289,9 @@ export const addOrganizationRestriction = async (context, user, fromId, organiza }; export const removeOrganizationRestriction = async (context, user, fromId, organizationId) => { - if (getDraftContext(context, user)) throw new Error('Cannot remove organization restriction in draft'); + if (getDraftContext(context, user)) { + throw UnsupportedError('Cannot remove organization restriction in draft'); + } const from = await internalLoadById(context, user, fromId); const updates = [{ key: INPUT_GRANTED_REFS, value: [organizationId], operation: UPDATE_OPERATION_REMOVE }]; // We skip references validation when updating organization sharing diff --git a/opencti-platform/opencti-graphql/src/domain/stixCoreObject.js b/opencti-platform/opencti-graphql/src/domain/stixCoreObject.js index 5a6c3100c2fa..8f9a4d17bb0d 100644 --- a/opencti-platform/opencti-graphql/src/domain/stixCoreObject.js +++ b/opencti-platform/opencti-graphql/src/domain/stixCoreObject.js @@ -4,7 +4,7 @@ import { internalFindByIds, internalLoadById, listEntitiesPaginated, listEntitie import { findAll as relationFindAll } from './stixCoreRelationship'; import { delEditContext, lockResource, notify, setEditContext, storeUpdateEvent } from '../database/redis'; import { BUS_TOPICS, logApp } from '../config/conf'; -import { ForbiddenAccess, FunctionalError, LockTimeoutError, TYPE_LOCK_ERROR, UnsupportedError } from '../config/errors'; +import { ForbiddenAccess, FunctionalError, LockTimeoutError, ResourceNotFoundError, TYPE_LOCK_ERROR, UnsupportedError } from '../config/errors'; import { isStixCoreObject, stixCoreObjectOptions } from '../schema/stixCoreObject'; import { ABSTRACT_STIX_CORE_OBJECT, @@ -365,7 +365,9 @@ export const stixCoreObjectsMultiDistribution = (context, user, args) => { // region export export const stixCoreObjectsExportAsk = async (context, user, args) => { - if (getDraftContext(context, user)) throw new Error('Cannot ask for export in draft'); + if (getDraftContext(context, user)) { + throw UnsupportedError('Cannot ask for export in draft'); + } const { exportContext, format, exportType, contentMaxMarkings, selectedIds, fileMarkings } = args; const { search, orderBy, orderMode, filters } = args; const argsFilters = { search, orderBy, orderMode, filters }; @@ -375,7 +377,9 @@ export const stixCoreObjectsExportAsk = async (context, user, args) => { return works.map((w) => workToExportFile(w)); }; export const stixCoreObjectExportAsk = async (context, user, stixCoreObjectId, input) => { - if (getDraftContext(context, user)) throw new Error('Cannot ask for export in draft'); + if (getDraftContext(context, user)) { + throw UnsupportedError('Cannot ask for export in draft'); + } const { format, exportType, contentMaxMarkings, fileMarkings } = input; const entity = await storeLoadById(context, user, stixCoreObjectId, ABSTRACT_STIX_CORE_OBJECT); const works = await askEntityExport(context, user, format, entity, exportType, contentMaxMarkings, fileMarkings); @@ -412,12 +416,14 @@ export const CONTENT_TYPE_FIELDS = 'fields'; export const CONTENT_TYPE_FILE = 'file'; export const askElementAnalysisForConnector = async (context, user, analyzedId, contentSource, contentType, connectorId) => { - if (getDraftContext(context, user)) throw new Error('Cannot ask for analysis in draft'); + if (getDraftContext(context, user)) { + throw UnsupportedError('Cannot ask for analysis in draft'); + } logApp.debug(`[JOBS] ask analysis for content type ${contentType} and content source ${contentSource}`); if (contentType === CONTENT_TYPE_FIELDS) return await askFieldsAnalysisForConnector(context, user, analyzedId, contentSource, connectorId); if (contentType === CONTENT_TYPE_FILE) return await askFileAnalysisForConnector(context, user, analyzedId, contentSource, connectorId); - throw new Error(`Content type ${contentType} not recognized`); + throw FunctionalError('Content type not recognized', { contentType }); }; export const CONTENT_SOURCE_CONTENT_MAPPING = 'content_mapping'; @@ -434,7 +440,7 @@ const askFieldsAnalysisForConnector = async (context, user, analyzedId, contentS const work = await createWork(context, user, connector, 'Content fields analysis', element.standard_id); if (contentSource !== CONTENT_SOURCE_CONTENT_MAPPING) { - throw new Error(`Fields content source not handled: ${contentSource}`); + throw FunctionalError('Fields content source not handled', { contentSource }); } const contentMappingFields = ['description', 'content']; @@ -460,7 +466,7 @@ const askFieldsAnalysisForConnector = async (context, user, analyzedId, contentS await publishAnalysisAction(user, analyzedId, connector, element); return work; } - throw new Error('No connector found for analysis'); + throw new ResourceNotFoundError('No connector found for analysis', { analyzedId, connectorId }); }; const askFileAnalysisForConnector = async (context, user, analyzedId, contentSource, connectorId) => { @@ -497,7 +503,7 @@ const askFileAnalysisForConnector = async (context, user, analyzedId, contentSou await publishAnalysisAction(user, analyzedId, connector, element); return work; } - throw new Error('No connector found for analysis'); + throw new ResourceNotFoundError('No connector found for analysis', { analyzedId, connectorId }); }; const getAnalysisFileName = (contentSource, contentType) => { @@ -615,7 +621,9 @@ export const stixCoreAnalysis = async (context, user, entityId, contentSource, c }; export const stixCoreObjectImportPush = async (context, user, id, file, args = {}) => { - if (getDraftContext(context, user)) throw new Error('Cannot import in draft'); + if (getDraftContext(context, user)) { + throw UnsupportedError('Cannot import in draft'); + } let lock; const { noTriggerImport, version: fileVersion, fileMarkings: file_markings, importContextEntities, fromTemplate = false } = args; const previous = await storeLoadByIdWithRefs(context, user, id); @@ -722,7 +730,9 @@ export const stixCoreObjectImportPush = async (context, user, id, file, args = { }; export const stixCoreObjectImportDelete = async (context, user, fileId) => { - if (getDraftContext(context, user)) throw new Error('Cannot delete imports in draft'); + if (getDraftContext(context, user)) { + throw UnsupportedError('Cannot delete imports in draft'); + } if (!fileId.startsWith('import')) { throw UnsupportedError('Cant delete an exported file with this method'); } diff --git a/opencti-platform/opencti-graphql/src/domain/user.js b/opencti-platform/opencti-graphql/src/domain/user.js index 915306c3eac0..f8f1dc583eb2 100644 --- a/opencti-platform/opencti-graphql/src/domain/user.js +++ b/opencti-platform/opencti-graphql/src/domain/user.js @@ -318,7 +318,7 @@ export const checkUserCanShareMarkings = async (context, user, markingsToShare) const contentMaxMarkingsIsShareable = markingsToShare.every((m) => ( shareableMarkings.some((shareableMarking) => m.definition_type === shareableMarking.definition_type && m.x_opencti_order <= shareableMarking.x_opencti_order))); if (!contentMaxMarkingsIsShareable) { - throw new Error('You are not allowed to share these markings.'); + throw ForbiddenAccess('You are not allowed to share these markings', { markings: markingsToShare }); } }; diff --git a/opencti-platform/opencti-graphql/src/graphql/authDirective.js b/opencti-platform/opencti-graphql/src/graphql/authDirective.js index da71a5d8e737..693b0482979e 100644 --- a/opencti-platform/opencti-graphql/src/graphql/authDirective.js +++ b/opencti-platform/opencti-graphql/src/graphql/authDirective.js @@ -3,7 +3,7 @@ import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils'; import { filter, includes, map } from 'ramda'; // eslint-disable-next-line import/extensions import { defaultFieldResolver } from 'graphql/index.js'; -import { AuthRequired, ForbiddenAccess, OtpRequired, OtpRequiredActivation } from '../config/errors'; +import { AuthRequired, ForbiddenAccess, OtpRequired, OtpRequiredActivation, UnsupportedError } from '../config/errors'; import { OPENCTI_ADMIN_UUID } from '../schema/general'; import { BYPASS, VIRTUAL_ORGANIZATION_ADMIN, SETTINGS_SET_ACCESSES } from '../utils/access'; @@ -26,7 +26,7 @@ export const authDirectiveBuilder = (directiveName) => { if (!authDirective && (typeName === 'Query' || typeName === 'Mutation')) { const publicDirective = getDirective(schema, fieldConfig, 'public')?.[0]; if (!publicDirective) { - throw new Error(`Unsecure schema: missing auth or public directive for ${_fieldName}`); + throw UnsupportedError('Unsecure schema: missing auth or public directive', { field: _fieldName }); } } if (authDirective) { diff --git a/opencti-platform/opencti-graphql/src/graphql/loggerPlugin.js b/opencti-platform/opencti-graphql/src/graphql/loggerPlugin.js index 80efc4b8481f..4b215d7a58c0 100644 --- a/opencti-platform/opencti-graphql/src/graphql/loggerPlugin.js +++ b/opencti-platform/opencti-graphql/src/graphql/loggerPlugin.js @@ -2,7 +2,7 @@ import { filter, head, isEmpty, isNil } from 'ramda'; import { stripIgnoredCharacters } from 'graphql/utilities'; import { ApolloServerErrorCode } from '@apollo/server/errors'; import conf, { appLogExtendedErrors, booleanConf, logApp } from '../config/conf'; -import { isEmptyField, isNotEmptyField } from '../database/utils'; +import { isNotEmptyField } from '../database/utils'; import { getMemoryStatistics } from '../domain/settings'; import { AUTH_ERRORS, FORBIDDEN_ACCESS, FUNCTIONAL_ERRORS, ValidationError } from '../config/errors'; import { publishUserAction } from '../listener/UserActionListener'; @@ -15,40 +15,6 @@ const API_CALL_MESSAGE = 'GRAPHQL_API'; // If you touch this, you need to change const perfLog = booleanConf('app:performance_logger', false); const LOGS_SENSITIVE_FIELDS = conf.get('app:app_logs:logs_redacted_inputs') ?? []; -const graphQLNodeParser = (node) => { - const result = []; - try { - if (node.kind === 'Field') { - const data = { name: node.name?.value, alias: node.alias?.value }; - data.arguments = (node.arguments ?? []).map((arg) => graphQLNodeParser(arg)); - result.push(data); - } - if (node.kind === 'Argument') { - const data = { name: node.name.value, alias: node.alias?.value }; - if (node.value.kind === 'ObjectValue' || node.value.kind === 'ObjectField') { - data.value = graphQLNodeParser(node.value); - } else { // Direct value - data.type = node.value.kind; - data.is_empty = data.type === 'ListValue' ? isEmptyField(node.value.values) : isEmptyField(node.value.value); - } - result.push(data); - } - if (node.kind === 'ObjectField') { - const data = { name: node.name.value, alias: node.alias?.value }; - data.type = node.value.kind; - data.is_empty = data.type === 'ListValue' ? isEmptyField(node.value.values) : isEmptyField(node.value.value); - result.push(data); - } - if (node.kind === 'ObjectValue') { - const data = { values: (node.fields ?? []).map((arg) => graphQLNodeParser(arg)) }; - result.push(data); - } - } catch { - // Node fail to be parsed - } - return result; -}; - const resolveKeyPromises = async (object) => { const resolvedObject = {}; const entries = Object.entries(object).filter(([, value]) => value && typeof value.then === 'function'); @@ -83,7 +49,6 @@ export default { const isWrite = context.operation && context.operation.operation === 'mutation'; const contextUser = context.contextValue.user; const origin = contextUser ? contextUser.origin : undefined; - const [variables] = await tryResolveKeyPromises(contextVariables); // Compute inner relations let innerRelationCount = 0; if (isWrite) { @@ -114,6 +79,12 @@ export default { time: elapsed, size, }; + // Handle extended error option + if (appLogExtendedErrors) { + const [variables] = await tryResolveKeyPromises(contextVariables); + callMetaData.variables = variables; + callMetaData.operation_query = stripIgnoredCharacters(context.request.query); + } if (isCallError) { let callError = head(context.errors); // For errors directly generated by appollo @@ -121,13 +92,8 @@ export default { const data = { source: callError.message, ...callError.extensions }; callError = ValidationError('[OPENCTI] API GraphQL framework error', callError.extensions?.field, data); } - // Handle extended error option - if (appLogExtendedErrors) { - callMetaData.variables = variables; - callMetaData.operation_query = stripIgnoredCharacters(context.request.query); - } else { - callMetaData.query_attributes = (callError.nodes ?? []).map((node) => graphQLNodeParser(node)); - } + callMetaData.cause = callError; + const errorMessage = callError.message; const errorCode = callError.extensions?.code ?? callError.name; // Don't error log for a simple missing authentication // Specific audit log for forbidden access @@ -152,10 +118,10 @@ export default { } // If functional error, log in warning if (FUNCTIONAL_ERRORS.includes(errorCode)) { - logApp.warn(callError, callMetaData); + logApp.warn(errorMessage, callMetaData); } else { // Every other uses cases are logged with error level - logApp.error(callError, callMetaData); + logApp.error(errorMessage, callMetaData); } } else if (perfLog) { logApp.info(API_CALL_MESSAGE, { ...callMetaData, memory: getMemoryStatistics() }); diff --git a/opencti-platform/opencti-graphql/src/http/httpServer.js b/opencti-platform/opencti-graphql/src/http/httpServer.js index b03d68532f3c..ab4510cabb23 100644 --- a/opencti-platform/opencti-graphql/src/http/httpServer.js +++ b/opencti-platform/opencti-graphql/src/http/httpServer.js @@ -18,6 +18,7 @@ import { isStrategyActivated, STRATEGY_CERT } from '../config/providers'; import { applicationSession } from '../database/session'; import { executionContext } from '../utils/access'; import { authenticateUserFromRequest, userWithOrigin } from '../domain/user'; +import { ForbiddenAccess } from '../config/errors'; const MIN_20 = 20 * 60 * 1000; const REQ_TIMEOUT = conf.get('app:request_timeout'); @@ -45,7 +46,7 @@ const createHttpServer = async () => { httpServer = https.createServer(options, app); logApp.info('[INIT] HTTPS server initialization done.'); } catch (e) { - logApp.error('[INIT] HTTPS server cannot start, please verify app.https_cert and other configurations', e); + logApp.error('[INIT] HTTPS server cannot start, please verify app.https_cert and other configurations', { cause: e }); } } else { httpServer = http.createServer(app); @@ -58,7 +59,7 @@ const createHttpServer = async () => { path: `${basePath}/graphql`, }); wsServer.on('error', (e) => { - throw new Error(e.message); + throw e; }); const serverCleanup = useServer({ schema, @@ -89,7 +90,7 @@ const createHttpServer = async () => { context.user = { ...wsSession.user, origin }; return context; } - throw new Error('User must be authenticated'); + throw ForbiddenAccess('User must be authenticated'); }, }, wsServer); apolloServer.addPlugin(ApolloServerPluginDrainHttpServer({ httpServer })); diff --git a/opencti-platform/opencti-graphql/src/instrumentation.js b/opencti-platform/opencti-graphql/src/instrumentation.js index 2c1e39186137..e79ee24f6b16 100644 --- a/opencti-platform/opencti-graphql/src/instrumentation.js +++ b/opencti-platform/opencti-graphql/src/instrumentation.js @@ -5,19 +5,16 @@ import { booleanConf, logApp } from './config/conf'; const isPyroscopeEnable = booleanConf('app:telemetry:pyroscope:enabled', false); if (isPyroscopeEnable) { - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires,global-require - const Pyroscope = require('@pyroscope/nodejs'); - const node = nconf.get('app:telemetry:pyroscope:identifier') ?? 'opencti'; - const exporter = nconf.get('app:telemetry:pyroscope:exporter'); - SourceMapper.create(['.']).then((sourceMapper) => { - Pyroscope.init({ serverAddress: exporter, appName: node, sourceMapper }); - Pyroscope.start(); - }).catch((err) => { - logApp.error('[OPENCTI] Error loading Pyroscope source mapper', { cause: err }); - }); - logApp.info('[OPENCTI] Pyroscope plugin successfully loaded.'); - } catch (err) { + // eslint-disable-next-line @typescript-eslint/no-var-requires,global-require + const Pyroscope = require('@pyroscope/nodejs'); + const node = nconf.get('app:telemetry:pyroscope:identifier') ?? 'opencti'; + const exporter = nconf.get('app:telemetry:pyroscope:exporter'); + SourceMapper.create(['.']).then((sourceMapper) => { + Pyroscope.init({ serverAddress: exporter, appName: node, sourceMapper }); + Pyroscope.start(); + }).catch((err) => { logApp.error('[OPENCTI] Error loading Pyroscope', { cause: err }); - } + }).then(() => { + logApp.info('[OPENCTI] Pyroscope plugin successfully loaded.'); + }); } diff --git a/opencti-platform/opencti-graphql/src/manager/connectorManager.js b/opencti-platform/opencti-graphql/src/manager/connectorManager.js index 33bdd707f4f1..199a4b1a1d78 100644 --- a/opencti-platform/opencti-graphql/src/manager/connectorManager.js +++ b/opencti-platform/opencti-graphql/src/manager/connectorManager.js @@ -59,7 +59,7 @@ const closeOldWorks = async (context, connector) => { // Delete redis tracking key await redisDeleteWorks(element.internal_id); } catch (e) { - logApp.error('[OPENCTI-MODULE] Connector manager error processing work closing', { error: e }); + logApp.error('[OPENCTI-MODULE] Connector manager error processing work closing', { cause: e }); } } }; diff --git a/opencti-platform/opencti-graphql/src/manager/indicatorDecayManager.ts b/opencti-platform/opencti-graphql/src/manager/indicatorDecayManager.ts index bd4dde1aaa60..469a81ab6381 100644 --- a/opencti-platform/opencti-graphql/src/manager/indicatorDecayManager.ts +++ b/opencti-platform/opencti-graphql/src/manager/indicatorDecayManager.ts @@ -22,7 +22,7 @@ export const indicatorDecayHandler = async () => { const indicator = indicatorsToUpdate[i]; await updateIndicatorDecayScore(context, DECAY_MANAGER_USER, indicator); } catch (e) { - logApp.warn('[OPENCTI-MODULE] Error when processing decay, skipping.', { cause: e, id: indicatorsToUpdate[i].id }); + logApp.error('[OPENCTI-MODULE] Error when processing decay, skipping.', { cause: e, id: indicatorsToUpdate[i].id }); errorCount += 1; } } diff --git a/opencti-platform/opencti-graphql/src/modules/draftWorkspace/draftWorkspace-domain.ts b/opencti-platform/opencti-graphql/src/modules/draftWorkspace/draftWorkspace-domain.ts index 5c0abe25a6b0..9e2116b669e1 100644 --- a/opencti-platform/opencti-graphql/src/modules/draftWorkspace/draftWorkspace-domain.ts +++ b/opencti-platform/opencti-graphql/src/modules/draftWorkspace/draftWorkspace-domain.ts @@ -71,7 +71,9 @@ export const listDraftRelations = (context: AuthContext, user: AuthUser, args: Q }; export const addDraftWorkspace = async (context: AuthContext, user: AuthUser, input: DraftWorkspaceAddInput) => { - if (!isFeatureEnabled('DRAFT_WORKSPACE')) throw new Error('Feature not yet available'); + if (!isFeatureEnabled('DRAFT_WORKSPACE')) { + throw UnsupportedError('Feature not yet available'); + } const defaultOps = { created_at: now(), }; diff --git a/opencti-platform/opencti-graphql/src/modules/exclusionList/exclusionList-domain.ts b/opencti-platform/opencti-graphql/src/modules/exclusionList/exclusionList-domain.ts index f9478d9f5ff1..22f3310118a9 100644 --- a/opencti-platform/opencti-graphql/src/modules/exclusionList/exclusionList-domain.ts +++ b/opencti-platform/opencti-graphql/src/modules/exclusionList/exclusionList-domain.ts @@ -7,6 +7,7 @@ import { listEntitiesPaginated, storeLoadById } from '../../database/middleware- import type { AuthContext, AuthUser } from '../../types/user'; import { type BasicStoreEntityExclusionList, ENTITY_TYPE_EXCLUSION_LIST, type StoreEntityExclusionList } from './exclusionList-types'; import type { ExclusionListContentAddInput, ExclusionListFileAddInput, QueryExclusionListsArgs } from '../../generated/graphql'; +import { UnsupportedError } from '../../config/errors'; const filePath = 'exclusionLists'; @@ -35,7 +36,9 @@ const storeAndCreateExclusionList = async (context: AuthContext, user: AuthUser, }; export const addExclusionListContent = async (context: AuthContext, user: AuthUser, input: ExclusionListContentAddInput) => { - if (!isExclusionListEnabled) throw new Error('Feature not yet available'); + if (!isExclusionListEnabled) { + throw UnsupportedError('Feature not yet available'); + } const file = { createReadStream: () => Readable.from(Buffer.from(input.content, 'utf-8')), filename: `${input.name}.txt`, @@ -45,12 +48,16 @@ export const addExclusionListContent = async (context: AuthContext, user: AuthUs return storeAndCreateExclusionList(context, user, input, file); }; export const addExclusionListFile = async (context: AuthContext, user: AuthUser, input: ExclusionListFileAddInput) => { - if (!isExclusionListEnabled) throw new Error('Feature not yet available'); + if (!isExclusionListEnabled) { + throw UnsupportedError('Feature not yet available'); + } return storeAndCreateExclusionList(context, user, input, input.file); }; export const deleteExclusionList = async (context: AuthContext, user: AuthUser, exclusionListId: string) => { - if (!isExclusionListEnabled) throw new Error('Feature not yet available'); + if (!isExclusionListEnabled) { + throw UnsupportedError('Feature not yet available'); + } const exclusionList = await findById(context, user, exclusionListId); await deleteFile(context, user, exclusionList.file_id); return deleteInternalObject(context, user, exclusionListId, ENTITY_TYPE_EXCLUSION_LIST); diff --git a/opencti-platform/opencti-graphql/src/modules/playbook/playbook-components.ts b/opencti-platform/opencti-graphql/src/modules/playbook/playbook-components.ts index 45f09edd1b2b..0e8cb0e963ed 100644 --- a/opencti-platform/opencti-graphql/src/modules/playbook/playbook-components.ts +++ b/opencti-platform/opencti-graphql/src/modules/playbook/playbook-components.ts @@ -126,7 +126,7 @@ const PLAYBOOK_LOGGER_COMPONENT: PlaybookComponent = { schema: async () => PLAYBOOK_LOGGER_COMPONENT_SCHEMA, executor: async ({ bundle, playbookNode }) => { if (playbookNode.configuration.level) { - logApp._log(playbookNode.configuration.level, '[PLAYBOOK MANAGER] Logger component output', null, { bundle }); + logApp._log(playbookNode.configuration.level, '[PLAYBOOK MANAGER] Logger component output', { bundle }); } return { output_port: 'out', bundle, forceBundleTracking: true }; } diff --git a/opencti-platform/opencti-graphql/src/modules/xtm/xtm-domain.js b/opencti-platform/opencti-graphql/src/modules/xtm/xtm-domain.js index 8bdd3c3c9fa5..468c4108130f 100644 --- a/opencti-platform/opencti-graphql/src/modules/xtm/xtm-domain.js +++ b/opencti-platform/opencti-graphql/src/modules/xtm/xtm-domain.js @@ -413,7 +413,9 @@ export const generateOpenBasScenario = async (context, user, stixCoreObject, att }; export const generateContainerScenario = async (context, user, args) => { - if (getDraftContext(context, user)) throw new Error('Cannot generate scenario in draft'); + if (getDraftContext(context, user)) { + throw UnsupportedError('Cannot generate scenario in draft'); + } const { id, interval, selection, simulationType = 'technical', useAI = false } = args; if (useAI || simulationType !== 'technical') { await checkEnterpriseEdition(context); @@ -426,7 +428,9 @@ export const generateContainerScenario = async (context, user, args) => { }; export const generateThreatScenario = async (context, user, args) => { - if (getDraftContext(context, user)) throw new Error('Cannot generate scenario in draft'); + if (getDraftContext(context, user)) { + throw UnsupportedError('Cannot generate scenario in draft'); + } const { id, interval, selection, simulationType = 'technical', useAI = false } = args; if (useAI || simulationType !== 'technical') { await checkEnterpriseEdition(context); @@ -439,7 +443,9 @@ export const generateThreatScenario = async (context, user, args) => { }; export const generateVictimScenario = async (context, user, args) => { - if (getDraftContext(context, user)) throw new Error('Cannot generate scenario in draft'); + if (getDraftContext(context, user)) { + throw UnsupportedError('Cannot generate scenario in draft'); + } const { id, interval, selection, simulationType = 'technical', useAI = false } = args; if (useAI || simulationType !== 'technical') { await checkEnterpriseEdition(context); diff --git a/opencti-platform/opencti-graphql/src/telemetry/BatchExportingMetricReader.ts b/opencti-platform/opencti-graphql/src/telemetry/BatchExportingMetricReader.ts index ac398098987d..3e7659f93260 100644 --- a/opencti-platform/opencti-graphql/src/telemetry/BatchExportingMetricReader.ts +++ b/opencti-platform/opencti-graphql/src/telemetry/BatchExportingMetricReader.ts @@ -5,6 +5,7 @@ import { type MetricProducer, MetricReader, type PushMetricExporter, TimeoutErro import { callWithTimeout } from '@opentelemetry/sdk-metrics/build/esnext/utils'; import type { DataPoint, ResourceMetrics } from '@opentelemetry/sdk-metrics/build/src/export/MetricData'; import { Resource } from '@opentelemetry/resources/build/src/Resource'; +import { UnknownError } from '../config/errors'; export type BatchExportingMetricReaderOptions = { exporter: PushMetricExporter; @@ -104,7 +105,7 @@ export class BatchExportingMetricReader extends MetricReader { const doExport = async () => { const result = await internal._export(this._exporter, this._resourceMetrics); if (result.code !== ExportResultCode.SUCCESS) { - throw new Error(`PeriodicExportingMetricReader: metrics export failed (error ${result.error})`); + throw UnknownError('PeriodicExportingMetricReader: metrics export failed', { cause: result.error }); } this._resourceMetrics.resource = Resource.EMPTY; }; diff --git a/opencti-platform/opencti-graphql/tests/01-unit/manager/indicatorDecayManager-test.ts b/opencti-platform/opencti-graphql/tests/01-unit/manager/indicatorDecayManager-test.ts index 7893fbc00d82..1acb83bb1dc0 100644 --- a/opencti-platform/opencti-graphql/tests/01-unit/manager/indicatorDecayManager-test.ts +++ b/opencti-platform/opencti-graphql/tests/01-unit/manager/indicatorDecayManager-test.ts @@ -33,8 +33,8 @@ describe('Testing indicatorDecayManager', () => { }); it('should manage error in findIndicatorsForDecay', async () => { - const logAppWarnSpy = vi.spyOn(logApp, 'warn'); + const logAppWarnSpy = vi.spyOn(logApp, 'error'); await indicatorDecayHandler(); - expect(logAppWarnSpy, 'Error should be managed and log as warn.').toHaveBeenCalledTimes(1); + expect(logAppWarnSpy, 'Error should be managed and log as error.').toHaveBeenCalledTimes(2); }); }); diff --git a/opencti-platform/opencti-graphql/tests/01-unit/utils/logger-test.ts b/opencti-platform/opencti-graphql/tests/01-unit/utils/logger-test.ts index 45accfc52726..a88f39294874 100644 --- a/opencti-platform/opencti-graphql/tests/01-unit/utils/logger-test.ts +++ b/opencti-platform/opencti-graphql/tests/01-unit/utils/logger-test.ts @@ -1,7 +1,9 @@ import * as R from 'ramda'; import { describe, expect, it } from 'vitest'; -import { appLogLevelMaxArraySize, appLogLevelMaxStringSize, limitMetaErrorComplexity } from '../../../src/config/conf'; +import { appLogLevelMaxArraySize, appLogLevelMaxStringSize, prepareLogMetadata } from '../../../src/config/conf'; +import { FunctionalError } from '../../../src/config/errors'; +// region objects definition const CLASSIC_OBJECT = { category: 'APP', errors: [ @@ -11,7 +13,7 @@ const CLASSIC_OBJECT = { { index: 'opencti_stix_domain_objects-000001', index_uuid: 'ntQ2slJaRmWphjOmcls3lA', - reason: '[b2349596-2fa3-4444-842d-effa8b34f267]: version conflict, required seqNo [51514085], primary term [64]. current document has seqNo [51514086] and primary term [64]', + reason: 'reason', shard: '0', type: 'version_conflict_engine_exception' } @@ -21,7 +23,7 @@ const CLASSIC_OBJECT = { }, message: 'Bulk indexing fail', name: 'DATABASE_ERROR', - stack: 'GraphQLError: Bulk indexing fail\\n at error (/opt/opencti/build/src/config/errors.js:7:10)\\n at DatabaseError (/opt/opencti/build/src/config/errors.js:61:48)\\n at /opt/opencti/build/src/database/engine.js:3425:13\\n at processTicksAndRejections (node:internal/process/task_queues:95:5)\\n at elRemoveRelationConnection (/opt/opencti/build/src/database/engine.js:3582:9)\\n at elDeleteElements (/opt/opencti/build/src/database/engine.js:3773:3)\\n at internalDeleteElementById (/opt/opencti/build/src/database/middleware.js:3253:7)\\n at deleteElementById (/opt/opencti/build/src/database/middleware.js:3275:32)\\n at deleteElement (/opt/opencti/build/src/manager/retentionManager.ts:37:5)\\n at deleteFn (/opt/opencti/build/src/manager/retentionManager.ts:83:11)' + stack: 'GraphQLError: Bulk indexing fail' } ], id: '3f001108-c42c-4131-b3a3-583a98043c15', @@ -32,7 +34,6 @@ const CLASSIC_OBJECT = { timestamp: '2025-01-09T20:57:05.422Z', version: '6.4.6' }; - const TOO_COMPLEX_OBJECT = { category: 'APP', errors: [ @@ -178,17 +179,31 @@ const TOO_COMPLEX_OBJECT = { source: R.range(1, 6000).map(() => 'A').join(''), timestamp: '2025-01-09T20:57:05.422Z' }; +const WITH_ERROR_OBJECT = { + level: 'error', + cause: FunctionalError('my error', { cause: new Error('embedded error') }), + timestamp: '2025-01-09T20:57:05.422Z' +}; +// endregion describe('Logger test suite', () => { it('Log object is correctly untouched', () => { - const cleanObject = limitMetaErrorComplexity(CLASSIC_OBJECT); - expect(JSON.stringify(cleanObject)).toEqual(JSON.stringify(CLASSIC_OBJECT)); + const cleanObject = prepareLogMetadata(CLASSIC_OBJECT); + const classicCompare = R.dissoc('version', CLASSIC_OBJECT); + const cleanCompare = R.dissoc('version', cleanObject); + expect(JSON.stringify(cleanCompare)).toEqual(JSON.stringify(classicCompare)); + }); + + it('Log object with error correctly formatted', () => { + const cleanObject = prepareLogMetadata(WITH_ERROR_OBJECT); + expect(cleanObject.cause.message).toBe('my error'); + expect(cleanObject.cause.attributes.cause.message).toBe('embedded error'); }); it('Log object is correctly limited', () => { let initialSize = TOO_COMPLEX_OBJECT.errors[0].attributes.category_to_limit.length; const start = new Date().getTime(); - const cleanObject = limitMetaErrorComplexity(TOO_COMPLEX_OBJECT); + const cleanObject = prepareLogMetadata(TOO_COMPLEX_OBJECT); const parsingTimeMs = new Date().getTime() - start; expect(parsingTimeMs).to.be.lt(2); let cleanedSize = cleanObject.errors[0].attributes.category_to_limit.length; diff --git a/opencti-platform/opencti-graphql/tests/02-integration/02-resolvers/publicDashboard-test.js b/opencti-platform/opencti-graphql/tests/02-integration/02-resolvers/publicDashboard-test.js index 6947934ea342..807459942690 100644 --- a/opencti-platform/opencti-graphql/tests/02-integration/02-resolvers/publicDashboard-test.js +++ b/opencti-platform/opencti-graphql/tests/02-integration/02-resolvers/publicDashboard-test.js @@ -358,7 +358,7 @@ describe('PublicDashboard resolver', () => { }); expect(publicDashboardQuery).not.toBeNull(); expect(publicDashboardQuery.errors.length).toEqual(1); - expect(publicDashboardQuery.errors.at(0).message).toEqual('You are not allowed to share these markings.'); + expect(publicDashboardQuery.errors.at(0).message).toEqual('You are not allowed to share these markings'); }); it('should publicDashboard created', async () => {