diff --git a/.env.sample b/.env.sample index 5d6b3eba..b3f44894 100644 --- a/.env.sample +++ b/.env.sample @@ -2,6 +2,7 @@ PORT= LOG_LEVEL=debug +REDACT_LOGS=false # Swagger SWAGGER_USER= diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index d72d1f8d..850f8489 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -123,6 +123,7 @@ jobs: "PORT=${{ secrets.PORT }}" \ "NODE_ENV=${{ secrets.NODE_ENV }}" \ "LOG_LEVEL=${{ secrets.LOG_LEVEL }}" \ + "REDACT_LOGS=${{ secrets.REDACT_LOGS }}" \ "TZ=${{ secrets.TZ }}" \ "SWAGGER_USER=${{ secrets.SWAGGER_USER }}" \ "SWAGGER_PASSWORD=${{ secrets.SWAGGER_PASSWORD }}" \ diff --git a/docker-compose.yml b/docker-compose.yml index ef7022d9..4dc0aa9e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,7 @@ services: PORT: NODE_ENV: LOG_LEVEL: + REDACT_LOGS: TZ: SWAGGER_USER: SWAGGER_PASSWORD: diff --git a/package-lock.json b/package-lock.json index 34662e30..a0da7f30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "date-fns": "^2.30.0", "dotenv": "^16.3.1", "express-basic-auth": "^1.2.1", + "lodash": "^4.17.21", "mssql": "^9.1.1", "nestjs-pino": "^3.3.0", "passport": "^0.6.0", @@ -50,6 +51,7 @@ "@types/compression": "^1.7.2", "@types/express": "^4.17.17", "@types/jest": "^29.5.3", + "@types/lodash": "^4.14.195", "@types/node": "^20.4.4", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^6.2.0", @@ -1777,7 +1779,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "devOptional": true, + "dev": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -1789,7 +1791,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "devOptional": true, + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -2406,7 +2408,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6.0.0" } @@ -2434,7 +2436,7 @@ "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "devOptional": true + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.18", @@ -3014,25 +3016,25 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node20": { "version": "20.1.0", @@ -3232,6 +3234,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.195", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", + "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==", + "dev": true + }, "node_modules/@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -3248,7 +3256,7 @@ "version": "20.4.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.4.tgz", "integrity": "sha512-CukZhumInROvLq3+b5gLev+vgpsIqC2D0deQr/yS1WnxvmYLlJXZpaQrQiseMY+6xusl79E04UjWoqyr+t1/Ew==", - "devOptional": true + "dev": true }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -3826,7 +3834,7 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "devOptional": true, + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -3856,7 +3864,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "devOptional": true, + "dev": true, "engines": { "node": ">=0.4.0" } @@ -4050,7 +4058,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "devOptional": true + "dev": true }, "node_modules/argparse": { "version": "2.0.1", @@ -5419,7 +5427,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "devOptional": true + "dev": true }, "node_modules/cross-spawn": { "version": "7.0.3", @@ -5984,7 +5992,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "devOptional": true, + "dev": true, "engines": { "node": ">=0.3.1" } @@ -10921,7 +10929,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true + "dev": true }, "node_modules/makeerror": { "version": "1.0.12", @@ -14199,7 +14207,7 @@ "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "devOptional": true, + "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -14769,7 +14777,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "devOptional": true + "dev": true }, "node_modules/v8-to-istanbul": { "version": "9.1.0", @@ -15200,7 +15208,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6" } diff --git a/package.json b/package.json index 284aeaf2..a97c4936 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "date-fns": "^2.30.0", "dotenv": "^16.3.1", "express-basic-auth": "^1.2.1", + "lodash": "^4.17.21", "mssql": "^9.1.1", "nestjs-pino": "^3.3.0", "passport": "^0.6.0", @@ -61,6 +62,7 @@ "@types/compression": "^1.7.2", "@types/express": "^4.17.17", "@types/jest": "^29.5.3", + "@types/lodash": "^4.14.195", "@types/node": "^20.4.4", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^6.2.0", diff --git a/src/config/app.config.test.ts b/src/config/app.config.test.ts new file mode 100644 index 00000000..4197317d --- /dev/null +++ b/src/config/app.config.test.ts @@ -0,0 +1,135 @@ +import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; + +import appConfig from './app.config'; +import { InvalidConfigException } from './invalid-config.exception'; + +describe('appConfig', () => { + const valueGenerator = new RandomValueGenerator(); + + let originalProcessEnv: NodeJS.ProcessEnv; + + beforeEach(() => { + originalProcessEnv = process.env; + }); + + afterEach(() => { + process.env = originalProcessEnv; + }); + + describe('parsing LOG_LEVEL', () => { + it('throws an InvalidConfigException if LOG_LEVEL is specified but is not a valid log level', () => { + replaceEnvironmentVariables({ + LOG_LEVEL: 'not-a-real-log-level', + }); + + const gettingTheAppConfig = () => appConfig(); + + expect(gettingTheAppConfig).toThrow(InvalidConfigException); + expect(gettingTheAppConfig).toThrow(`LOG_LEVEL must be one of fatal,error,warn,info,debug,trace,silent or not specified.`); + }); + + it('uses info as the logLevel if LOG_LEVEL is not specified', () => { + replaceEnvironmentVariables({}); + + const config = appConfig(); + + expect(config.logLevel).toBe('info'); + }); + + it('uses info as the logLevel if LOG_LEVEL is empty', () => { + replaceEnvironmentVariables({ + LOG_LEVEL: '', + }); + + const config = appConfig(); + + expect(config.logLevel).toBe('info'); + }); + + it.each([ + { + LOG_LEVEL: 'fatal', + }, + { + LOG_LEVEL: 'error', + }, + { + LOG_LEVEL: 'warn', + }, + { + LOG_LEVEL: 'info', + }, + { + LOG_LEVEL: 'debug', + }, + { + LOG_LEVEL: 'trace', + }, + { + LOG_LEVEL: 'silent', + }, + ])('uses LOG_LEVEL as the logLevel if LOG_LEVEL is valid ($LOG_LEVEL)', ({ LOG_LEVEL }) => { + replaceEnvironmentVariables({ + LOG_LEVEL, + }); + + const config = appConfig(); + + expect(config.logLevel).toBe(LOG_LEVEL); + }); + }); + + describe('parsing REDACT_LOGS', () => { + it('sets redactLogs to true if REDACT_LOGS is true', () => { + replaceEnvironmentVariables({ + REDACT_LOGS: 'true', + }); + + const config = appConfig(); + + expect(config.redactLogs).toBe(true); + }); + + it('sets redactLogs to false if REDACT_LOGS is false', () => { + replaceEnvironmentVariables({ + REDACT_LOGS: 'false', + }); + + const config = appConfig(); + + expect(config.redactLogs).toBe(false); + }); + + it('sets redactLogs to true if REDACT_LOGS is not specified', () => { + replaceEnvironmentVariables({}); + + const config = appConfig(); + + expect(config.redactLogs).toBe(true); + }); + + it('sets redactLogs to true if REDACT_LOGS is the empty string', () => { + replaceEnvironmentVariables({ + REDACT_LOGS: '', + }); + + const config = appConfig(); + + expect(config.redactLogs).toBe(true); + }); + + it('sets redactLogs to true if REDACT_LOGS is any string other than true or false', () => { + replaceEnvironmentVariables({ + REDACT_LOGS: valueGenerator.string(), + }); + + const config = appConfig(); + + expect(config.redactLogs).toBe(true); + }); + }); + + const replaceEnvironmentVariables = (newEnvVariables: Record): void => { + process.env = newEnvVariables; + }; +}); diff --git a/src/config/app.config.ts b/src/config/app.config.ts index 003f4824..f38e82c5 100644 --- a/src/config/app.config.ts +++ b/src/config/app.config.ts @@ -14,7 +14,7 @@ export default registerAs('app', (): Record => { env: process.env.APP_ENV || 'development', versioning: { - enable: process.env.HTTP_VERSIONING_ENABLE === 'true' || false, + enable: process.env.HTTP_VERSIONING_ENABLE === 'true', prefix: 'v', version: process.env.HTTP_VERSION || '1', }, @@ -23,5 +23,6 @@ export default registerAs('app', (): Record => { port: process.env.HTTP_PORT ? Number.parseInt(process.env.HTTP_PORT, 10) : 3003, apiKey: process.env.API_KEY, logLevel: process.env.LOG_LEVEL || 'info', + redactLogs: process.env.REDACT_LOGS !== 'false', }; }); diff --git a/src/constants/index.ts b/src/constants/index.ts index 3ea8323d..7bd70773 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -7,6 +7,8 @@ * 3. Auth strategy * 4. Products * 5. Customers + * 6. Strings to redact + * 7. Strings locations to redact */ export * from './auth.constant'; @@ -15,4 +17,6 @@ export * from './database-name.constant'; export * from './date.constant'; export * from './enums'; export * from './products.constant'; +export * from './redact-strings.constant'; +export * from './redact-strings-paths.constant'; export * from './ukef-id.constant'; diff --git a/src/constants/redact-strings-paths.constant.ts b/src/constants/redact-strings-paths.constant.ts new file mode 100644 index 00000000..648fd4e5 --- /dev/null +++ b/src/constants/redact-strings-paths.constant.ts @@ -0,0 +1,12 @@ +export const REDACT_STRING_PATHS = [ + 'driverError.originalError.message', + 'driverError.originalError.stack', + 'driverError.message', + 'driverError.stack', + 'message', + 'stack', + 'originalError.message', + 'originalError.stack', + 'err.message', + 'err.stack', +]; diff --git a/src/constants/redact-strings.constant.ts b/src/constants/redact-strings.constant.ts new file mode 100644 index 00000000..a58a6b33 --- /dev/null +++ b/src/constants/redact-strings.constant.ts @@ -0,0 +1,9 @@ +export const REDACT_STRINGS = [ + { searchValue: /(Login failed for user ').*(')/g, replaceValue: '$1[Redacted]$2' }, + { searchValue: process.env.DATABASE_USERNAME, replaceValue: '[Redacted]' }, + { searchValue: process.env.DATABASE_PASSWORD, replaceValue: '[Redacted]' }, + { searchValue: process.env.DATABASE_MDM_HOST, replaceValue: '[RedactedDomain]' }, + { searchValue: process.env.DATABASE_CEDAR_HOST, replaceValue: '[RedactedDomain]' }, + { searchValue: process.env.DATABASE_NUMBER_GENERATOR_HOST, replaceValue: '[RedactedDomain]' }, + { searchValue: process.env.DATABASE_CIS_HOST, replaceValue: '[RedactedDomain]' }, +]; diff --git a/src/helpers/redact-strings-in-log-args.helper.test.ts b/src/helpers/redact-strings-in-log-args.helper.test.ts new file mode 100644 index 00000000..375f8529 --- /dev/null +++ b/src/helpers/redact-strings-in-log-args.helper.test.ts @@ -0,0 +1,125 @@ +import { REDACT_STRING_PATHS } from '@ukef/constants'; +import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; + +import { redactStringsInLogArgs } from './redact-strings-in-log-args.helper'; + +describe('Redact errors helper', () => { + const valueGenerator = new RandomValueGenerator(); + + describe('redactStringsInLogArgs', () => { + const domain = valueGenerator.httpsUrl(); + const otherSensitiveField = valueGenerator.word(); + const message = `ConnectionError: Failed to connect to ${domain}, ${otherSensitiveField}`; + const redactedMessage = `ConnectionError: Failed to connect to [RedactedDomain], [Redacted]`; + const redactStrings = [ + { searchValue: domain, replaceValue: '[RedactedDomain]' }, + { searchValue: otherSensitiveField, replaceValue: '[Redacted]' }, + ]; + const args = [ + { + message: message, + stack: message, + originalError: { + message: message, + stack: message, + safe: 'Nothing sensitive', + }, + driverError: { + message: message, + stack: message, + originalError: { + message: message, + stack: message, + safe: 'Nothing sensitive', + }, + }, + }, + ]; + const expectedResult = [ + { + message: redactedMessage, + stack: redactedMessage, + originalError: { + message: redactedMessage, + stack: redactedMessage, + safe: 'Nothing sensitive', + }, + driverError: { + message: redactedMessage, + stack: redactedMessage, + originalError: { + message: redactedMessage, + stack: redactedMessage, + safe: 'Nothing sensitive', + }, + }, + }, + ]; + + it('replaces sensitive data in input object', () => { + const redacted = redactStringsInLogArgs(true, REDACT_STRING_PATHS, redactStrings, args); + + expect(redacted).toStrictEqual(expectedResult); + }); + + it('returns original input if redactLogs is set to false', () => { + const redacted = redactStringsInLogArgs(false, REDACT_STRING_PATHS, redactStrings, args); + + expect(redacted).toStrictEqual(args); + }); + + it('replaces sensitive data in input object using regex', () => { + const redactStrings = [{ searchValue: /(Login failed for user ').*(')/g, replaceValue: '$1[Redacted]$2' }]; + const otherSensitiveValue = valueGenerator.word(); + const messageforRegex = `Connection error: Login failed for user '${otherSensitiveValue}'`; + const redactedMessage = `Connection error: Login failed for user '[Redacted]'`; + const args = [ + { + message: messageforRegex, + stack: messageforRegex, + originalError: { + message: messageforRegex, + }, + }, + ]; + const expectedResult = [ + { + message: redactedMessage, + stack: redactedMessage, + originalError: { + message: redactedMessage, + }, + }, + ]; + + const redacted = redactStringsInLogArgs(true, REDACT_STRING_PATHS, redactStrings, args); + + expect(redacted).toStrictEqual(expectedResult); + }); + + it('replaces sensitive data in different input object', () => { + const args = [ + { + field1: message, + field2: { + field3: message, + safe: 'Nothing sensitive', + }, + }, + ]; + const expectedResult = [ + { + field1: redactedMessage, + field2: { + field3: redactedMessage, + safe: 'Nothing sensitive', + }, + }, + ]; + const redactPaths = ['field1', 'field2.field3']; + const redacted = redactStringsInLogArgs(true, redactPaths, redactStrings, args); + + expect(redacted).toStrictEqual(expectedResult); + }); + }); +}); diff --git a/src/helpers/redact-strings-in-log-args.helper.ts b/src/helpers/redact-strings-in-log-args.helper.ts new file mode 100644 index 00000000..d50e76c8 --- /dev/null +++ b/src/helpers/redact-strings-in-log-args.helper.ts @@ -0,0 +1,31 @@ +import { get, set } from 'lodash'; + +// This helper function is used to redact sensitive data in Error object strings. +export const redactStringsInLogArgs = ( + redactLogs: boolean, + redactPaths: string[], + redactStrings: { searchValue: string | RegExp; replaceValue: string }[], + args: any[], +): any => { + if (!redactLogs) { + return args; + } + args.forEach((arg, index) => { + redactPaths.forEach((path) => { + const value: string = get(arg, path); + if (value) { + const safeValue = redactString(redactStrings, value); + set(args[index], path, safeValue); + } + }); + }); + return args; +}; + +const redactString = (redactStrings: { searchValue: string | RegExp; replaceValue: string }[], string: string): string => { + let safeString: string = string; + redactStrings.forEach((redact) => { + safeString = safeString.replaceAll(redact.searchValue, redact.replaceValue); + }); + return safeString; +}; diff --git a/src/logging/build-key-to-redact.test.ts b/src/logging/build-key-to-redact.test.ts new file mode 100644 index 00000000..49df4053 --- /dev/null +++ b/src/logging/build-key-to-redact.test.ts @@ -0,0 +1,34 @@ +import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; + +import { buildKeyToRedact } from './build-key-to-redact'; + +describe('buildKeyToRedact', () => { + const valueGenerator = new RandomValueGenerator(); + const part1 = valueGenerator.string(); + const part2 = valueGenerator.string(); + const part3 = valueGenerator.string(); + + it('returns an empty string if there are no parts to build', () => { + const keyToRedact = buildKeyToRedact([]); + + expect(keyToRedact).toBe(''); + }); + + it('returns the string in the format [""] if there is only 1 part to build', () => { + const keyToRedact = buildKeyToRedact([part1]); + + expect(keyToRedact).toBe(`["${part1}"]`); + }); + + it('returns the parts joined in the format [""][""] if there are 2 parts', () => { + const keyToRedact = buildKeyToRedact([part1, part2]); + + expect(keyToRedact).toBe(`["${part1}"]["${part2}"]`); + }); + + it('returns the parts joined in the format [""][""] if there are more than 2 parts', () => { + const keyToRedact = buildKeyToRedact([part1, part2, part3]); + + expect(keyToRedact).toBe(`["${part1}"]["${part2}"]["${part3}"]`); + }); +}); diff --git a/src/logging/build-key-to-redact.ts b/src/logging/build-key-to-redact.ts new file mode 100644 index 00000000..ca17309f --- /dev/null +++ b/src/logging/build-key-to-redact.ts @@ -0,0 +1,10 @@ +const prefix = `["`; +const suffix = `"]`; +const joinSeparator = `${suffix}${prefix}`; + +export const buildKeyToRedact = (parts: string[]): string => { + if (!parts.length) { + return ''; + } + return `${prefix}${parts.join(joinSeparator)}${suffix}`; +}; diff --git a/src/logging/console-logger-with-redact.test.ts b/src/logging/console-logger-with-redact.test.ts new file mode 100644 index 00000000..e3ff05a7 --- /dev/null +++ b/src/logging/console-logger-with-redact.test.ts @@ -0,0 +1,42 @@ +import { ConsoleLogger } from '@nestjs/common'; +import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; + +import { ConsoleLoggerWithRedact } from './console-logger-with-redact'; + +const mockedSuperError = jest.spyOn(ConsoleLogger.prototype, 'error'); + +describe('ConsoleLoggerWithRedact', () => { + const valueGenerator = new RandomValueGenerator(); + const domain = valueGenerator.httpsUrl(); + const otherSensitiveField = valueGenerator.word(); + const message = `ConnectionError: Failed to connect to ${domain}, ${otherSensitiveField}`; + const redactedMessage = `ConnectionError: Failed to connect to [RedactedDomain], [Redacted]`; + const redactStrings = [ + { searchValue: domain, replaceValue: '[RedactedDomain]' }, + { searchValue: otherSensitiveField, replaceValue: '[Redacted]' }, + ]; + const logger = new ConsoleLoggerWithRedact(redactStrings); + + beforeEach(() => { + mockedSuperError.mockClear(); + }); + + it('redacts sensitive data in `message`', () => { + logger.error(message); + + expect(mockedSuperError).toHaveBeenCalledWith(redactedMessage, undefined, undefined); + }); + + it('redacts sensitive data in `message` and second parameter `stack`', () => { + logger.error(message, message); + + expect(mockedSuperError).toHaveBeenCalledWith(redactedMessage, redactedMessage, undefined); + }); + + it('redacts sensitive data in `message` and second parameter `stack`, also passes context', () => { + const context = valueGenerator.word(); + logger.error(message, message, context); + + expect(mockedSuperError).toHaveBeenCalledWith(redactedMessage, redactedMessage, context); + }); +}); diff --git a/src/logging/console-logger-with-redact.ts b/src/logging/console-logger-with-redact.ts new file mode 100644 index 00000000..9637cc27 --- /dev/null +++ b/src/logging/console-logger-with-redact.ts @@ -0,0 +1,27 @@ +import { ConsoleLogger } from '@nestjs/common'; + +export class ConsoleLoggerWithRedact extends ConsoleLogger { + private stringPatternsToRedact: [{ searchValue: string | RegExp; replaceValue; string }]; + + constructor(stringPatternsToRedact) { + super(); + this.stringPatternsToRedact = stringPatternsToRedact; + } + + // Simplified, because has just single signature, function from ConsoleLogger. + error(message: any, stack?: string, context?: string) { + let cleanMessage = message; + let cleanStack = stack; + if (typeof message == 'string') { + this.stringPatternsToRedact.forEach((redact) => { + cleanMessage = cleanMessage.replace(redact.searchValue, redact.replaceValue); + }); + } + if (typeof stack == 'string') { + this.stringPatternsToRedact.forEach((redact) => { + cleanStack = cleanStack.replace(redact.searchValue, redact.replaceValue); + }); + } + super.error(cleanMessage, cleanStack, context); + } +} diff --git a/src/logging/log-keys-to-redact.test.ts b/src/logging/log-keys-to-redact.test.ts new file mode 100644 index 00000000..155171f8 --- /dev/null +++ b/src/logging/log-keys-to-redact.test.ts @@ -0,0 +1,108 @@ +import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; + +import { buildKeyToRedact } from './build-key-to-redact'; +import { logKeysToRedact, LogKeysToRedactOptions } from './log-keys-to-redact'; + +describe('logKeysToRedact', () => { + const valueGenerator = new RandomValueGenerator(); + const options: Omit = { + clientRequest: { + logKey: valueGenerator.string(), + headersLogKey: valueGenerator.string(), + }, + outgoingRequest: { + logKey: valueGenerator.string(), + headersLogKey: valueGenerator.string(), + }, + error: { + logKey: valueGenerator.string(), + sensitiveChildKeys: [valueGenerator.string(), valueGenerator.string()], + }, + dbError: { + logKey: valueGenerator.string(), + sensitiveChildKeys: [valueGenerator.string(), valueGenerator.string()], + }, + }; + + describe('when redactLogs is false', () => { + it('returns an empty array', () => { + expect(logKeysToRedact({ redactLogs: false, ...options })).toStrictEqual([]); + }); + }); + + describe('when redactLogs is true', () => { + let result: string[]; + + beforeEach(() => { + result = logKeysToRedact({ redactLogs: true, ...options }); + }); + + it('includes the headers of a client request', () => { + const { logKey, headersLogKey } = options.clientRequest; + + expect(result).toContain(buildKeyToRedact([logKey, headersLogKey])); + }); + + it('includes the headers of an outgoing request', () => { + const { logKey, headersLogKey } = options.outgoingRequest; + + expect(result).toContain(buildKeyToRedact([logKey, headersLogKey])); + }); + + it('includes all sensitive child keys of an error', () => { + const { logKey, sensitiveChildKeys } = options.error; + + expect(result).toContain(buildKeyToRedact([logKey, sensitiveChildKeys[0]])); + expect(result).toContain(buildKeyToRedact([logKey, sensitiveChildKeys[1]])); + }); + + it('includes all sensitive child keys of an inner error', () => { + const { logKey, sensitiveChildKeys } = options.error; + + expect(result).toContain(buildKeyToRedact([logKey, 'innerError', sensitiveChildKeys[0]])); + expect(result).toContain(buildKeyToRedact([logKey, 'innerError', sensitiveChildKeys[1]])); + }); + + it('includes all sensitive child keys of an error cause', () => { + const { logKey, sensitiveChildKeys } = options.error; + + expect(result).toContain(buildKeyToRedact([logKey, 'options', 'cause', sensitiveChildKeys[0]])); + expect(result).toContain(buildKeyToRedact([logKey, 'options', 'cause', sensitiveChildKeys[1]])); + }); + + it(`includes all sensitive child keys of an error cause's inner error`, () => { + const { logKey, sensitiveChildKeys } = options.error; + + expect(result).toContain(buildKeyToRedact([logKey, 'options', 'cause', 'innerError', sensitiveChildKeys[0]])); + expect(result).toContain(buildKeyToRedact([logKey, 'options', 'cause', 'innerError', sensitiveChildKeys[1]])); + }); + + it('includes all sensitive child keys of an dbError', () => { + const { logKey, sensitiveChildKeys } = options.dbError; + + expect(result).toContain(buildKeyToRedact([logKey, sensitiveChildKeys[0]])); + expect(result).toContain(buildKeyToRedact([logKey, sensitiveChildKeys[1]])); + }); + + it('includes all sensitive child keys of an original dbError', () => { + const { logKey, sensitiveChildKeys } = options.dbError; + + expect(result).toContain(buildKeyToRedact([logKey, 'originalError', 'info', sensitiveChildKeys[0]])); + expect(result).toContain(buildKeyToRedact([logKey, 'originalError', 'info', sensitiveChildKeys[1]])); + }); + + it('includes all sensitive child keys of an dbError driverError', () => { + const { logKey, sensitiveChildKeys } = options.dbError; + + expect(result).toContain(buildKeyToRedact([logKey, 'driverError', sensitiveChildKeys[0]])); + expect(result).toContain(buildKeyToRedact([logKey, 'driverError', sensitiveChildKeys[1]])); + }); + + it(`includes all sensitive child keys of an dbError driverError's original error`, () => { + const { logKey, sensitiveChildKeys } = options.dbError; + + expect(result).toContain(buildKeyToRedact([logKey, 'driverError', 'originalError', 'info', sensitiveChildKeys[0]])); + expect(result).toContain(buildKeyToRedact([logKey, 'driverError', 'originalError', 'info', sensitiveChildKeys[1]])); + }); + }); +}); diff --git a/src/logging/log-keys-to-redact.ts b/src/logging/log-keys-to-redact.ts new file mode 100644 index 00000000..cf7d3c42 --- /dev/null +++ b/src/logging/log-keys-to-redact.ts @@ -0,0 +1,76 @@ +import { buildKeyToRedact } from './build-key-to-redact'; + +export interface LogKeysToRedactOptions { + redactLogs: boolean; + clientRequest: { + logKey: string; + headersLogKey: string; + }; + outgoingRequest: { + logKey: string; + headersLogKey: string; + }; + error: { + logKey: string; + sensitiveChildKeys: string[]; + }; + dbError: { + logKey: string; + sensitiveChildKeys: string[]; + }; +} + +export const logKeysToRedact = ({ redactLogs, clientRequest, outgoingRequest, error, dbError }: LogKeysToRedactOptions): string[] => { + if (!redactLogs) { + return []; + } + const keys = [ + ...getClientRequestLogKeysToRedact(clientRequest), + ...getOutgoingRequestLogKeysToRedact(outgoingRequest), + ...getErrorLogKeysToRedact(error), + ...getDbErrorLogKeysToRedact(dbError), + ]; + + return keys; +}; + +const getClientRequestLogKeysToRedact = ({ logKey, headersLogKey }: LogKeysToRedactOptions['clientRequest']): string[] => [ + // We redact the client request headers as they contain the secret API key that the client uses to authenticate with our API. + buildKeyToRedact([logKey, headersLogKey]), +]; + +const getOutgoingRequestLogKeysToRedact = ({ logKey, headersLogKey }: LogKeysToRedactOptions['outgoingRequest']): string[] => { + return [ + // We redact the outgoing request headers as they contain: + // - our Basic auth details for Informatica + buildKeyToRedact([logKey, headersLogKey]), + ]; +}; + +const getErrorLogKeysToRedact = ({ logKey, sensitiveChildKeys }: LogKeysToRedactOptions['error']): string[] => { + const innerErrorKey = 'innerError'; + const causeNestedErrorKey = ['options', 'cause']; + return sensitiveChildKeys.flatMap((childKey) => [ + buildKeyToRedact([logKey, childKey]), + // Some errors are wrapped in a new error and logged as the `innerError` field on the new error. + // Some errors also contain a `cause` field containing a wrapped error. + // We need to make sure the sensitive child keys are still redacted in these cases. + buildKeyToRedact([logKey, innerErrorKey, childKey]), + buildKeyToRedact([logKey, ...causeNestedErrorKey, childKey]), + buildKeyToRedact([logKey, ...causeNestedErrorKey, innerErrorKey, childKey]), + ]); +}; + +const getDbErrorLogKeysToRedact = ({ logKey, sensitiveChildKeys }: LogKeysToRedactOptions['dbError']): string[] => { + const innerErrorKey = ['originalError', 'info']; + const driverNestedErrorKey = ['driverError']; + return sensitiveChildKeys.flatMap((childKey) => [ + // Some errors are wrapped in a new error and logged as the `originalError` field on the new error. + // Some errors also contain a `driverError` field containing a wrapped error. + // We need to make sure the sensitive child keys are still redacted in these cases. + buildKeyToRedact([logKey, childKey]), + buildKeyToRedact([logKey, ...driverNestedErrorKey, childKey]), + buildKeyToRedact([logKey, ...innerErrorKey, childKey]), + buildKeyToRedact([logKey, ...driverNestedErrorKey, ...innerErrorKey, childKey]), + ]); +}; diff --git a/src/logging/logging-interceptor.helper.ts b/src/logging/logging-interceptor.helper.ts new file mode 100644 index 00000000..c2655c63 --- /dev/null +++ b/src/logging/logging-interceptor.helper.ts @@ -0,0 +1,20 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; +import { PinoLogger } from 'nestjs-pino'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + constructor(private readonly logger: PinoLogger) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const requestBody = context.switchToHttp().getRequest().body; + this.logger.debug({ requestBody }, 'Handling the following request from the client.'); + + return next.handle().pipe( + tap((responseBody) => { + this.logger.debug({ responseBody }, 'Returning the following response to the client.'); + }), + ); + } +} diff --git a/src/main.module.ts b/src/main.module.ts index fbc6c02f..6fb41e02 100644 --- a/src/main.module.ts +++ b/src/main.module.ts @@ -1,9 +1,16 @@ import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; +import { APP_INTERCEPTOR } from '@nestjs/core'; import config from '@ukef/config'; +import { HEADERS_LOG_KEY, OUTGOING_REQUEST_LOG_KEY } from '@ukef/modules/http/http.constants'; import { MdmModule } from '@ukef/modules/mdm.module'; import { LoggerModule } from 'nestjs-pino'; +import { REDACT_STRING_PATHS, REDACT_STRINGS } from './constants'; +import { redactStringsInLogArgs } from './helpers/redact-strings-in-log-args.helper'; +import { logKeysToRedact } from './logging/log-keys-to-redact'; +import { LoggingInterceptor } from './logging/logging-interceptor.helper'; + @Module({ imports: [ ConfigModule.forRoot({ @@ -25,10 +32,41 @@ import { LoggerModule } from 'nestjs-pino'; singleLine: true, }, }, + hooks: { + logMethod(inputArgs: any[], method) { + return method.apply(this, redactStringsInLogArgs(config.get('app.redactLogs'), REDACT_STRING_PATHS, REDACT_STRINGS, inputArgs)); + }, + }, + redact: logKeysToRedact({ + redactLogs: config.get('app.redactLogs'), + clientRequest: { + logKey: 'req', + headersLogKey: 'headers', + }, + outgoingRequest: { + logKey: OUTGOING_REQUEST_LOG_KEY, + headersLogKey: HEADERS_LOG_KEY, + }, + error: { + logKey: 'err', + sensitiveChildKeys: [ + // The `config` has basic authentication details for Informatica. + 'config', + ], + }, + dbError: { + logKey: 'err', + sensitiveChildKeys: [ + // The `serverName` has db server address. + 'serverName', + ], + }, + }), }, }), }), MdmModule, ], + providers: [{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }], }) export class MainModule {} diff --git a/src/main.ts b/src/main.ts index ac024a74..bbace709 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,12 +3,16 @@ import { NestApplication, NestFactory } from '@nestjs/core'; import { MainModule } from '@ukef/main.module'; import { App } from './app'; +import { REDACT_STRINGS } from './constants'; +import { ConsoleLoggerWithRedact } from './logging/console-logger-with-redact'; const main = async () => { - const nestApp: NestApplication = await NestFactory.create(MainModule, { bufferLogs: true }); + // If REDACT_LOGS is true use ConsoleLoggerWithRedact. ConsoleLoggerWithRedact is used just if `NestFactory.create` fails completely. + // If `NestFactory.create` doesn't fail completely, then buffered logs are passed to PinoLogger. NestLogger and ConsoleLoggerWithRedact are not used. + const logger = process.env.REDACT_LOGS !== 'false' ? new ConsoleLoggerWithRedact(REDACT_STRINGS) : new NestLogger(); + const nestApp: NestApplication = await NestFactory.create(MainModule, { logger, bufferLogs: true }); const app = new App(nestApp); - const logger = new NestLogger(); logger.log(`==========================================================`); logger.log(`Main app will serve on PORT ${app.port}`, 'MainApplication'); logger.log(`==========================================================`); diff --git a/src/modules/auth/strategy/api-key.strategy.ts b/src/modules/auth/strategy/api-key.strategy.ts index 07753717..b1d8ab88 100644 --- a/src/modules/auth/strategy/api-key.strategy.ts +++ b/src/modules/auth/strategy/api-key.strategy.ts @@ -8,7 +8,10 @@ import { AuthService } from '../auth.service'; @Injectable() export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy, AUTH.STRATEGY) { - constructor(private readonly authService: AuthService, private readonly configService: ConfigService) { + constructor( + private readonly authService: AuthService, + private readonly configService: ConfigService, + ) { const headerKeyApiKey: string = configService.get('app.apiKeyStrategy'); super({ header: headerKeyApiKey, prefix: '' }, true, (apiKey: string, done: (arg0: UnauthorizedException, arg1: boolean) => void) => { const hasValidKey = this.authService.validateApiKey(apiKey); diff --git a/src/modules/constants/constants.service.ts b/src/modules/constants/constants.service.ts index 038b0243..1c81761a 100644 --- a/src/modules/constants/constants.service.ts +++ b/src/modules/constants/constants.service.ts @@ -1,18 +1,18 @@ -import { Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; +import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DATABASE } from '@ukef/constants'; import { DbResponseHelper } from '@ukef/helpers/db-response.helper'; +import { PinoLogger } from 'nestjs-pino'; import { Repository } from 'typeorm'; import { ConstantSpiEntity } from './entities/constants-spi.entity'; @Injectable() export class ConstantsService { - private readonly logger = new Logger(); - constructor( @InjectRepository(ConstantSpiEntity, DATABASE.CIS) private readonly constantsCisRepository: Repository, + private readonly logger: PinoLogger, ) {} async find(oecdRiskCategory: number, category: string): Promise { diff --git a/src/modules/currencies/currencies.service.ts b/src/modules/currencies/currencies.service.ts index 151c6d18..2b60217d 100644 --- a/src/modules/currencies/currencies.service.ts +++ b/src/modules/currencies/currencies.service.ts @@ -1,7 +1,8 @@ -import { Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; +import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DATABASE, DATE } from '@ukef/constants'; import { DbResponseHelper } from '@ukef/helpers/db-response.helper'; +import { PinoLogger } from 'nestjs-pino'; import { DataSource, Equal, Repository } from 'typeorm'; import { CurrencyEntity } from './entities/currency.entity'; @@ -9,7 +10,6 @@ import { CurrencyExchangeEntity } from './entities/currency-exchange.entity'; @Injectable() export class CurrenciesService { - private readonly logger = new Logger(); constructor( @InjectRepository(CurrencyEntity, DATABASE.MDM) private readonly currency: Repository, @@ -17,6 +17,7 @@ export class CurrenciesService { private readonly currencyExchange: DataSource, @InjectRepository(CurrencyExchangeEntity, DATABASE.CEDAR) private readonly currencyExchangeRepository: Repository, + private readonly logger: PinoLogger, ) {} async findAll(): Promise { diff --git a/src/modules/exposure-period/exposure-period.service.ts b/src/modules/exposure-period/exposure-period.service.ts index 0a71556a..2519b45e 100644 --- a/src/modules/exposure-period/exposure-period.service.ts +++ b/src/modules/exposure-period/exposure-period.service.ts @@ -1,17 +1,17 @@ -import { Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; +import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import { DATABASE } from '@ukef/constants'; +import { PinoLogger } from 'nestjs-pino'; import { DataSource } from 'typeorm'; import { ExposurePeriodDto } from './dto/exposure-period.dto'; @Injectable() export class ExposurePeriodService { - private readonly logger = new Logger(); - constructor( @InjectDataSource(DATABASE.MDM) private readonly mdmDataSource: DataSource, + private readonly logger: PinoLogger, ) {} async calculate(startDate: string, endDate: string, productGroup: string): Promise { diff --git a/src/modules/http/http.constants.ts b/src/modules/http/http.constants.ts index 88119275..e0cf5df5 100644 --- a/src/modules/http/http.constants.ts +++ b/src/modules/http/http.constants.ts @@ -2,3 +2,5 @@ export const OUTGOING_REQUEST_LOG_KEY = 'outgoingRequest'; export const INCOMING_RESPONSE_LOG_KEY = 'incomingResponse'; export const BODY_LOG_KEY = 'data'; export const HEADERS_LOG_KEY = 'headers'; +export const SENSITIVE_REQUEST_HEADER_NAMES = 'headers'; +export const SENSITIVE_RESPONSE_HEADER_NAMES = 'headers'; diff --git a/src/modules/informatica/exception/informatica.exception.ts b/src/modules/informatica/exception/informatica.exception.ts index 47e3b67e..668edf31 100644 --- a/src/modules/informatica/exception/informatica.exception.ts +++ b/src/modules/informatica/exception/informatica.exception.ts @@ -1,5 +1,8 @@ export class InformaticaException extends Error { - constructor(message: string, public readonly innerError?: Error) { + constructor( + message: string, + public readonly innerError?: Error, + ) { super(message); this.name = this.constructor.name; } diff --git a/src/modules/informatica/informatica.module.ts b/src/modules/informatica/informatica.module.ts index 64eb4890..b51ca005 100644 --- a/src/modules/informatica/informatica.module.ts +++ b/src/modules/informatica/informatica.module.ts @@ -4,6 +4,7 @@ import { InformaticaConfig, KEY as INFORMATICA_CONFIG_KEY } from '@ukef/config/i import { HttpModule } from '@ukef/modules/http/http.module'; import { InformaticaService } from './informatica.service'; + @Module({ imports: [ HttpModule.registerAsync({ diff --git a/src/modules/interest-rates/interest-rates.service.ts b/src/modules/interest-rates/interest-rates.service.ts index f2f5cf38..124b9bfa 100644 --- a/src/modules/interest-rates/interest-rates.service.ts +++ b/src/modules/interest-rates/interest-rates.service.ts @@ -1,16 +1,17 @@ -import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DATABASE, DATE } from '@ukef/constants'; +import { PinoLogger } from 'nestjs-pino'; import { Equal, Repository } from 'typeorm'; import { InterestRatesEntity } from './entities/interest-rate.entity'; @Injectable() export class InterestRatesService { - private readonly logger = new Logger(); constructor( @InjectRepository(InterestRatesEntity, DATABASE.CEDAR) private readonly interestRates: Repository, + private readonly logger: PinoLogger, ) {} findAll(): Promise { diff --git a/src/modules/markets/markets.service.ts b/src/modules/markets/markets.service.ts index 04739520..68fb6406 100644 --- a/src/modules/markets/markets.service.ts +++ b/src/modules/markets/markets.service.ts @@ -1,18 +1,18 @@ -import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DATABASE } from '@ukef/constants'; import { DbResponseHelper } from '@ukef/helpers/db-response.helper'; +import { PinoLogger } from 'nestjs-pino'; import { Repository } from 'typeorm'; import { MarketEntity } from './entities/market.entity'; @Injectable() export class MarketsService { - private readonly logger = new Logger(); - constructor( @InjectRepository(MarketEntity, DATABASE.CIS) private readonly marketsRepository: Repository, + private readonly logger: PinoLogger, ) {} async find(active?: string, search?: string): Promise { diff --git a/src/modules/numbers/numbers.service.test.ts b/src/modules/numbers/numbers.service.test.ts index e37b549e..2479c62a 100644 --- a/src/modules/numbers/numbers.service.test.ts +++ b/src/modules/numbers/numbers.service.test.ts @@ -1,11 +1,12 @@ +import { ConfigService } from '@nestjs/config'; import { Test } from '@nestjs/testing'; +import { PinoLogger } from 'nestjs-pino'; import { Repository } from 'typeorm'; import { NumbersService } from './numbers.service'; describe('NumbersService', () => { let service: NumbersService; - const unSortedUkefIds = [ { id: 201, @@ -129,9 +130,14 @@ describe('NumbersService', () => { beforeAll(async () => { const module = await Test.createTestingModule({ - providers: [NumbersService, { provide: 'mssql-number-generator_UkefIdRepository', useFactory: repositoryMockFactory }], + providers: [ + NumbersService, + PinoLogger, + { provide: 'pino-params', useValue: {} }, + ConfigService, + { provide: 'mssql-number-generator_UkefIdRepository', useFactory: repositoryMockFactory }, + ], }).compile(); - service = module.get(NumbersService); }); diff --git a/src/modules/numbers/numbers.service.ts b/src/modules/numbers/numbers.service.ts index 97f9ad51..66d38d32 100644 --- a/src/modules/numbers/numbers.service.ts +++ b/src/modules/numbers/numbers.service.ts @@ -1,6 +1,7 @@ -import { BadRequestException, Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; +import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DATABASE } from '@ukef/constants'; +import { PinoLogger } from 'nestjs-pino'; import { Repository } from 'typeorm'; import { CreateUkefIdDto } from './dto/create-ukef-id.dto'; @@ -8,15 +9,13 @@ import { UkefId } from './entities/ukef-id.entity'; @Injectable() export class NumbersService { - private readonly logger = new Logger(); - constructor( @InjectRepository(UkefId, DATABASE.NUMBER_GENERATOR) private readonly numberRepository: Repository, + private readonly logger: PinoLogger, ) {} async create(createUkefIdDto: CreateUkefIdDto[]): Promise { - // TODO: DB calls are async and will generate IDs that are not in order. Extra code to order ids is required, or calls need to be made in async order. // TODO: new IDs of type 1 and 2 could be checked if they are used in ACBS. ACBS might be down, but generation still should work. const activeRequests = createUkefIdDto.map((createNumber) => { return this.numberRepository diff --git a/src/modules/premium-schedules/premium-schedules.service.ts b/src/modules/premium-schedules/premium-schedules.service.ts index a069cb5e..eb13247a 100644 --- a/src/modules/premium-schedules/premium-schedules.service.ts +++ b/src/modules/premium-schedules/premium-schedules.service.ts @@ -1,19 +1,20 @@ -import { Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; +import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DATABASE } from '@ukef/constants'; +import { DbResponseHelper } from '@ukef/helpers/db-response.helper'; import { Response } from 'express'; +import { PinoLogger } from 'nestjs-pino'; import { Equal, Repository } from 'typeorm'; -import { DbResponseHelper } from '../../helpers/db-response.helper'; import { CreatePremiumScheduleDto } from './dto/create-premium-schedule.dto'; import { PremiumScheduleEntity } from './entities/premium-schedule.entity'; @Injectable() export class PremiumSchedulesService { - private readonly logger = new Logger(); constructor( @InjectRepository(PremiumScheduleEntity, DATABASE.MDM) private readonly premiumSchedulesRepository: Repository, + private readonly logger: PinoLogger, ) {} async find(facilityId: string): Promise { diff --git a/src/modules/sector-industries/sector-industries.service.ts b/src/modules/sector-industries/sector-industries.service.ts index a1437db6..df3aa89a 100644 --- a/src/modules/sector-industries/sector-industries.service.ts +++ b/src/modules/sector-industries/sector-industries.service.ts @@ -1,16 +1,17 @@ -import { Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; +import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DATABASE, DATE } from '@ukef/constants'; +import { PinoLogger } from 'nestjs-pino'; import { Equal, Repository } from 'typeorm'; import { SectorIndustryEntity } from './entities/sector-industry.entity'; @Injectable() export class SectorIndustriesService { - private readonly logger = new Logger(); constructor( @InjectRepository(SectorIndustryEntity, DATABASE.MDM) private readonly sectorIndustries: Repository, + private readonly logger: PinoLogger, ) {} async find(ukefSectorId: string, ukefIndustryId: string): Promise { diff --git a/src/modules/yield-rates/yield-rates.service.ts b/src/modules/yield-rates/yield-rates.service.ts index 4e2d56b8..a2c4a2ff 100644 --- a/src/modules/yield-rates/yield-rates.service.ts +++ b/src/modules/yield-rates/yield-rates.service.ts @@ -1,16 +1,17 @@ -import { Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; +import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DATABASE, DATE } from '@ukef/constants'; +import { PinoLogger } from 'nestjs-pino'; import { Equal, LessThanOrEqual, MoreThan, Repository } from 'typeorm'; import { YieldRateEntity } from './entities/yield-rate.entity'; @Injectable() export class YieldRatesService { - private readonly logger = new Logger(); constructor( @InjectRepository(YieldRateEntity, DATABASE.CEDAR) private readonly yieldRateRepository: Repository, + private readonly logger: PinoLogger, ) {} async find(searchDate: string): Promise { diff --git a/test/docs/__snapshots__/get-docs-yaml.api-test.ts.snap b/test/docs/__snapshots__/get-docs-yaml.api-test.ts.snap index 0e5b0b8c..3307631b 100644 --- a/test/docs/__snapshots__/get-docs-yaml.api-test.ts.snap +++ b/test/docs/__snapshots__/get-docs-yaml.api-test.ts.snap @@ -135,7 +135,7 @@ paths: example: '00302069' description: The unique UKEF id of the customer to search for. schema: - pattern: /^\\d{8}$/ + pattern: ^\\d{8}$ type: string - name: name required: false