From d509ed9bdea63e3c0a74a9ddc205540280f7cc54 Mon Sep 17 00:00:00 2001 From: Matt Lavin Date: Thu, 15 Feb 2024 13:30:16 -0500 Subject: [PATCH 1/4] feat: Allow message body redaction --- src/sqs.test.ts | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ src/sqs.ts | 43 ++++++++++++++++++++++++------- 2 files changed, 102 insertions(+), 9 deletions(-) diff --git a/src/sqs.test.ts b/src/sqs.test.ts index 291c260..ee6b72b 100644 --- a/src/sqs.test.ts +++ b/src/sqs.test.ts @@ -79,6 +79,74 @@ describe('SQSMessageHandler', () => { ); }); + test('allows body redaction', async () => { + expect.assertions(2); + + const lambda = new SQSMessageHandler({ + logger, + redactMessageBody: () => 'REDACTED', + parseMessage: testSerializer.parseMessage, + createRunContext: (ctx) => { + expect(typeof ctx.correlationId === 'string').toBe(true); + return {}; + }, + }).lambda(); + + await lambda( + { + Records: [ + { attributes: {}, body: JSON.stringify({ data: 'test-event-1' }) }, + ], + } as any, + {} as any, + ); + + // Assert that the message body was redacted. + expect(logger.info).toHaveBeenCalledWith({ + event: { Records: [{ attributes: {}, body: 'REDACTED' }] }, + }, "Processing SQS topic message") + }); + + test('if redaction fails log the message', async () => { + expect.assertions(4); + + const error = new Error('Failed to redact message') + const lambda = new SQSMessageHandler({ + logger, + redactMessageBody: () => { throw error }, + parseMessage: testSerializer.parseMessage, + createRunContext: (ctx) => { + expect(typeof ctx.correlationId === 'string').toBe(true); + return {}; + }, + }).lambda(); + + const body = JSON.stringify({ data: 'test-event-1' }) + const event = { + Records: [ + { attributes: {}, body }, + ], + } as any + const response = await lambda( + event, + {} as any, + ); + + // Expect no failure + expect(response).toBeUndefined(); + + // Assert that the message body was shown unredacted. Logging the + // unredacted error allows for easier debugging. Leaking a small amount of + // PHI to Sumo is better than having a system that cannot be debugged. + expect(logger.error).toHaveBeenCalledWith({ + error, + body, + }, "Failed to redact message body") + expect(logger.info).toHaveBeenCalledWith({ + event, + }, "Processing SQS topic message") + }); + describe('error handling', () => { const records = [ { diff --git a/src/sqs.ts b/src/sqs.ts index d1f3bc5..057a76b 100644 --- a/src/sqs.ts +++ b/src/sqs.ts @@ -23,6 +23,13 @@ export type SQSMessageHandlerConfig = * https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#services-sqs-batchfailurereporting */ usePartialBatchResponses?: boolean; + + /** + * This will be called to redact the message body before logging it. By + * default, the full message body is logged. + */ + redactMessageBody?: (body: string) => string; + }; export type SQSMessageAction = ( @@ -58,13 +65,23 @@ export type SQSPartialBatchResponse = { }[]; }; +const safeRedactor = (logger: LoggerInterface, redactor: (body: string) => string) => + (body: string) => { + try { + return redactor(body); + } catch (error) { + logger.error({ error, body }, 'Failed to redact message body'); + return body; + } + } + /** * An abstraction for an SQS message handler. */ export class SQSMessageHandler { private messageActions: SQSMessageAction[] = []; - constructor(readonly config: SQSMessageHandlerConfig) {} + constructor(readonly config: SQSMessageHandlerConfig) { } /** * Adds a message action to the handler. @@ -96,7 +113,15 @@ export class SQSMessageHandler { Object.assign(context, await this.config.createRunContext(context)); // 2. Process all the records. - context.logger.info({ event }, 'Processing SQS topic message'); + const redactor = this.config.redactMessageBody ? safeRedactor(context.logger, this.config.redactMessageBody) : undefined; + const redactedEvent = redactor ? { + ...event, + Records: event.Records.map((record) => ({ + ...record, + body: redactor(record.body), + })), + } : event; + context.logger.info({ event: redactedEvent }, 'Processing SQS topic message'); const processingResult = await processWithOrdering( { @@ -183,13 +208,13 @@ export class SQSMessageHandler { const event: SQSEvent = { Records: messages.map( (msg) => - // We don't need to mock every field on this event -- there are lots. - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - ({ - attributes: {}, - messageId: uuid(), - body: stringifyMessage(msg), - } as any), + // We don't need to mock every field on this event -- there are lots. + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + ({ + attributes: {}, + messageId: uuid(), + body: stringifyMessage(msg), + } as any), ), }; From 8aa33030bbe54dcc76cc7b91124b4f79467e27aa Mon Sep 17 00:00:00 2001 From: Matt Lavin Date: Thu, 15 Feb 2024 13:35:26 -0500 Subject: [PATCH 2/4] chore: formatting fixes --- src/sqs.test.ts | 50 +++++++++++++++++++++++++++---------------------- src/sqs.ts | 47 ++++++++++++++++++++++++++-------------------- 2 files changed, 55 insertions(+), 42 deletions(-) diff --git a/src/sqs.test.ts b/src/sqs.test.ts index ee6b72b..63bd16c 100644 --- a/src/sqs.test.ts +++ b/src/sqs.test.ts @@ -102,18 +102,23 @@ describe('SQSMessageHandler', () => { ); // Assert that the message body was redacted. - expect(logger.info).toHaveBeenCalledWith({ - event: { Records: [{ attributes: {}, body: 'REDACTED' }] }, - }, "Processing SQS topic message") + expect(logger.info).toHaveBeenCalledWith( + { + event: { Records: [{ attributes: {}, body: 'REDACTED' }] }, + }, + 'Processing SQS topic message', + ); }); test('if redaction fails log the message', async () => { expect.assertions(4); - const error = new Error('Failed to redact message') + const error = new Error('Failed to redact message'); const lambda = new SQSMessageHandler({ logger, - redactMessageBody: () => { throw error }, + redactMessageBody: () => { + throw error; + }, parseMessage: testSerializer.parseMessage, createRunContext: (ctx) => { expect(typeof ctx.correlationId === 'string').toBe(true); @@ -121,30 +126,31 @@ describe('SQSMessageHandler', () => { }, }).lambda(); - const body = JSON.stringify({ data: 'test-event-1' }) + const body = JSON.stringify({ data: 'test-event-1' }); const event = { - Records: [ - { attributes: {}, body }, - ], - } as any - const response = await lambda( - event, - {} as any, - ); + Records: [{ attributes: {}, body }], + } as any; + const response = await lambda(event, {} as any); // Expect no failure expect(response).toBeUndefined(); - // Assert that the message body was shown unredacted. Logging the + // Assert that the message body was shown unredacted. Logging the // unredacted error allows for easier debugging. Leaking a small amount of // PHI to Sumo is better than having a system that cannot be debugged. - expect(logger.error).toHaveBeenCalledWith({ - error, - body, - }, "Failed to redact message body") - expect(logger.info).toHaveBeenCalledWith({ - event, - }, "Processing SQS topic message") + expect(logger.error).toHaveBeenCalledWith( + { + error, + body, + }, + 'Failed to redact message body', + ); + expect(logger.info).toHaveBeenCalledWith( + { + event, + }, + 'Processing SQS topic message', + ); }); describe('error handling', () => { diff --git a/src/sqs.ts b/src/sqs.ts index 057a76b..2b3e003 100644 --- a/src/sqs.ts +++ b/src/sqs.ts @@ -29,7 +29,6 @@ export type SQSMessageHandlerConfig = * default, the full message body is logged. */ redactMessageBody?: (body: string) => string; - }; export type SQSMessageAction = ( @@ -65,7 +64,8 @@ export type SQSPartialBatchResponse = { }[]; }; -const safeRedactor = (logger: LoggerInterface, redactor: (body: string) => string) => +const safeRedactor = + (logger: LoggerInterface, redactor: (body: string) => string) => (body: string) => { try { return redactor(body); @@ -73,7 +73,7 @@ const safeRedactor = (logger: LoggerInterface, redactor: (body: string) => strin logger.error({ error, body }, 'Failed to redact message body'); return body; } - } + }; /** * An abstraction for an SQS message handler. @@ -81,7 +81,7 @@ const safeRedactor = (logger: LoggerInterface, redactor: (body: string) => strin export class SQSMessageHandler { private messageActions: SQSMessageAction[] = []; - constructor(readonly config: SQSMessageHandlerConfig) { } + constructor(readonly config: SQSMessageHandlerConfig) {} /** * Adds a message action to the handler. @@ -113,15 +113,22 @@ export class SQSMessageHandler { Object.assign(context, await this.config.createRunContext(context)); // 2. Process all the records. - const redactor = this.config.redactMessageBody ? safeRedactor(context.logger, this.config.redactMessageBody) : undefined; - const redactedEvent = redactor ? { - ...event, - Records: event.Records.map((record) => ({ - ...record, - body: redactor(record.body), - })), - } : event; - context.logger.info({ event: redactedEvent }, 'Processing SQS topic message'); + const redactor = this.config.redactMessageBody + ? safeRedactor(context.logger, this.config.redactMessageBody) + : undefined; + const redactedEvent = redactor + ? { + ...event, + Records: event.Records.map((record) => ({ + ...record, + body: redactor(record.body), + })), + } + : event; + context.logger.info( + { event: redactedEvent }, + 'Processing SQS topic message', + ); const processingResult = await processWithOrdering( { @@ -208,13 +215,13 @@ export class SQSMessageHandler { const event: SQSEvent = { Records: messages.map( (msg) => - // We don't need to mock every field on this event -- there are lots. - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - ({ - attributes: {}, - messageId: uuid(), - body: stringifyMessage(msg), - } as any), + // We don't need to mock every field on this event -- there are lots. + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + ({ + attributes: {}, + messageId: uuid(), + body: stringifyMessage(msg), + } as any), ), }; From d0cd009d7397478819138348c9794e3333bded06 Mon Sep 17 00:00:00 2001 From: Matt Lavin Date: Tue, 20 Feb 2024 14:57:42 -0500 Subject: [PATCH 3/4] Encrypt senstive data with a public key when redaction fails --- src/__fixtures__/private-key.pem | 27 ++++++++ src/__fixtures__/public-key.pem | 9 +++ src/sqs.test.ts | 115 ++++++++++++++++++++++++++++--- src/sqs.ts | 90 ++++++++++++++++-------- 4 files changed, 201 insertions(+), 40 deletions(-) create mode 100644 src/__fixtures__/private-key.pem create mode 100644 src/__fixtures__/public-key.pem diff --git a/src/__fixtures__/private-key.pem b/src/__fixtures__/private-key.pem new file mode 100644 index 0000000..ffc6bc2 --- /dev/null +++ b/src/__fixtures__/private-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAqJACmpNdviOCUFgig7+BwYqVaNX9BelsCt9PymUv4NmSBwzb +QgxTIPT8jzEapcFtHWqz+ahH3OKizLglyCeqIC6bFSn8CWsm6u/g6d9kFk4Uz9Dw +1zuG31n/LYA1ebPOHZud9UiwTFJFvKPQPv8fKV73E/18swT+gLIBpgij83olb75N ++DpLpzAsKOjNJKWPOLzQPMYnd4XAEWMZpzt2Nb5VNcKU26sg7IEoj3LioiokWOX+ ++pPn9u/3KRkEYBH7qFXHozY2vhG5qqOu0+ImuXtZ6r0aI8CpXkKGSqg2KqNrhN2v +NL0N3onetPl/k2RCPZ/g08OHVFF9OOcytRuX3wIDAQABAoIBAHaEy0/kTgVi8j2L +urjn7lQnHOaZj06Y0V7TpUap3wA5+nL6ly/Zepmxp+MGo7XoStBkNidUKzMkJ1PK +JsaVHQmDu4cl/hChRrvp7jqC19zXCcsVHkI3mJ1yqflULEVmJ4ap5GaStWL0dhQt +Gj8xIrf0DcYAda1p1YinoIEdkimelI6trdGlQDF+BsKrrnal+K6cRKdMcAQWvgt0 +UKCw5wkcpMTxaN2pKsQtHu3GTOjP8uRoL7C6V0tCYo7VZO9+ZmMN+mpaO6LA+Rfj +wXr4itY9PtooLTrwAdP6WFIPwWsgNYqc5eVY9DvSeFH1saK3nguS2nTIr+DtY1H+ +D61PYAECgYEA0nEjteKzpM7gqLp8pcEicWY+d7j5GjvzUwfl6UJ2i5XbfbyC/khU +sz905rpc5OnVlxqodoew73v5gH3uvIcwhOXdcaIrwH1pMiFaV/TAeQjttmoVL0bJ +UOlpyWs8Tf04907l5fqtxz6izUk90nWK+wxrM7HdarxeRXsQz4aLIW0CgYEAzQ3h +Xnzi/0B/jnMeua/X1NGqgWJIp868A5pGog4BUWnSOzK5pRHQsVXN1wQq/5d1duhm +qnGWIEV8m+gChsDcuFoAWZorrFBSVI2/mTCWRLf5nELtkLZnXQ53x9N3LS8BvTek +rA8LOP+Cgv8IdM2em3DvIbZTa0uy1q2m6VdH2vsCgYBsrXsosmPdx+zjljNLEpur +/oZiI8eZUb6OcbS9KtK3sXOB0rm/gjEjxLClezcADPZ+K4k2dUrd0qN+RQrml9Zp +u6AJ0BtSNDIAbpMOe1pu5zqECvLX0HGk9HXqTBP/nrctmLRHeZcHH4TKCXoA1y0o +CzjNoJxdQ9xXe3+p/KybXQKBgC+tWIdltknvLzlp3u0By8c58NEgjxAla2XTCzVG +2FubpTwKcUvGNqXk83VZDL5c8vzw0F41Btj+DxkY+u1mDmv20ToENL9d9aafRrtR +pr7Xn/wLO714C9SBNqyJqJ4i3d6m/2zaGpvoHOpkbgzqekReH9vQztiVw0FTIwoC +NzzdAoGASTbIMn4+Syy1qF6uxB8PBlM3u9kutNgtQImEXgmjVO/46pzSPIyfijXu +z49EtzCqXuLM8eXGolX4CRyxe3zxLoq3BaS9aFDnKVfHsFoMsXpvLBxW9fTCmEhf +7oOqnKi1b6Y75/aM2yt9Eg8dTu8+619dPcX1EF17HVQQAWQe6HA= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/src/__fixtures__/public-key.pem b/src/__fixtures__/public-key.pem new file mode 100644 index 0000000..7a80980 --- /dev/null +++ b/src/__fixtures__/public-key.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqJACmpNdviOCUFgig7+B +wYqVaNX9BelsCt9PymUv4NmSBwzbQgxTIPT8jzEapcFtHWqz+ahH3OKizLglyCeq +IC6bFSn8CWsm6u/g6d9kFk4Uz9Dw1zuG31n/LYA1ebPOHZud9UiwTFJFvKPQPv8f +KV73E/18swT+gLIBpgij83olb75N+DpLpzAsKOjNJKWPOLzQPMYnd4XAEWMZpzt2 +Nb5VNcKU26sg7IEoj3LioiokWOX++pPn9u/3KRkEYBH7qFXHozY2vhG5qqOu0+Im +uXtZ6r0aI8CpXkKGSqg2KqNrhN2vNL0N3onetPl/k2RCPZ/g08OHVFF9OOcytRuX +3wIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/src/sqs.test.ts b/src/sqs.test.ts index 63bd16c..5ff54db 100644 --- a/src/sqs.test.ts +++ b/src/sqs.test.ts @@ -1,6 +1,8 @@ import { v4 as uuid } from 'uuid'; import { LoggerInterface } from '@lifeomic/logging'; import { SQSMessageAction, SQSMessageHandler } from './sqs'; +import { promises as fs } from 'fs' +import { privateDecrypt } from 'crypto' const logger: jest.Mocked = { info: jest.fn(), @@ -8,8 +10,14 @@ const logger: jest.Mocked = { child: jest.fn(), } as any; +let publicKey: string; +beforeAll(async () => { + publicKey = await fs.readFile(__dirname + '/__fixtures__/public-key.pem', 'utf8') +}) + beforeEach(() => { logger.info.mockReset(); + logger.error.mockReset(); logger.child.mockReset(); logger.child.mockImplementation(() => logger); }); @@ -84,7 +92,11 @@ describe('SQSMessageHandler', () => { const lambda = new SQSMessageHandler({ logger, - redactMessageBody: () => 'REDACTED', + redactionConfig: { + redactMessageBody: () => 'REDACTED', + publicEncryptionKey: publicKey, + publicKeyDescription: 'test-public-key', + }, parseMessage: testSerializer.parseMessage, createRunContext: (ctx) => { expect(typeof ctx.correlationId === 'string').toBe(true); @@ -110,14 +122,80 @@ describe('SQSMessageHandler', () => { ); }); - test('if redaction fails log the message', async () => { - expect.assertions(4); + test('if redaction fails, a redacted body is logged with an encrypted body', async () => { + expect.assertions(5); + + const error = new Error('Failed to redact message'); + const lambda = new SQSMessageHandler({ + logger, + redactionConfig: { + redactMessageBody: () => { + throw error; + }, + publicEncryptionKey: publicKey, + publicKeyDescription: 'test-public-key', + }, + parseMessage: testSerializer.parseMessage, + createRunContext: (ctx) => { + expect(typeof ctx.correlationId === 'string').toBe(true); + return {}; + }, + }).lambda(); + + const body = JSON.stringify({ data: 'test-event-1' }); + const event = { + Records: [{ attributes: {}, body }], + } as any; + const response = await lambda(event, {} as any); + + // Expect no failure + expect(response).toBeUndefined(); + + // Assert that the message body was shown redacted. Along with the + // redacted body, an encrypted body is also logged with the redaction + // error to help debugging. + expect(logger.error).toHaveBeenCalledWith( + { + error, + encryptedBody: expect.any(String), + publicKeyDescription: 'test-public-key' + }, + 'Failed to redact message body', + ); + + // Verify that the encrypted body can be decrypted. + const privateKey = await fs.readFile(__dirname + '/__fixtures__/private-key.pem', 'utf8') + const encryptedBody = logger.error.mock.calls[0][0].encryptedBody + const decrypted = privateDecrypt(privateKey, Buffer.from(encryptedBody, 'base64')).toString('utf8'); + expect(decrypted).toEqual(body); + + // Verify the the body was redacted. + expect(logger.info).toHaveBeenCalledWith( + { + event: { + ...event, + Records: event.Records.map((record: any) => ({ + ...record, + body: "[REDACTION FAILED]" + })) + } + }, + 'Processing SQS topic message', + ); + }); + + test('if redaction fails, and encryption fails, a redacted body is logged with the encryption failure', async () => { + expect.assertions(5); const error = new Error('Failed to redact message'); const lambda = new SQSMessageHandler({ logger, - redactMessageBody: () => { - throw error; + redactionConfig: { + redactMessageBody: () => { + throw error; + }, + publicEncryptionKey: 'not-a-valid-key', + publicKeyDescription: 'test-public-key', }, parseMessage: testSerializer.parseMessage, createRunContext: (ctx) => { @@ -135,19 +213,36 @@ describe('SQSMessageHandler', () => { // Expect no failure expect(response).toBeUndefined(); - // Assert that the message body was shown unredacted. Logging the - // unredacted error allows for easier debugging. Leaking a small amount of - // PHI to Sumo is better than having a system that cannot be debugged. + // Assert that the message body was shown redacted. Along with the + // redacted body, an encrypted body is also logged with the redaction + // error to help debugging. expect(logger.error).toHaveBeenCalledWith( { error, - body, + encryptedBody: "[ENCRYPTION FAILED]", // Signals that encryption failed + publicKeyDescription: 'test-public-key' }, 'Failed to redact message body', ); + + // When encryption fails, the failure is logged. + expect(logger.error).toHaveBeenCalledWith( + { + error: expect.anything() + }, + 'Failed to encrypt message body', + ); + + // Verify the the body was redacted. expect(logger.info).toHaveBeenCalledWith( { - event, + event: { + ...event, + Records: event.Records.map((record: any) => ({ + ...record, + body: "[REDACTION FAILED]" + })) + } }, 'Processing SQS topic message', ); diff --git a/src/sqs.ts b/src/sqs.ts index 2b3e003..77f3448 100644 --- a/src/sqs.ts +++ b/src/sqs.ts @@ -7,6 +7,7 @@ import { processWithOrdering, withHealthCheckHandling, } from './utils'; +import { publicEncrypt } from 'crypto' export type SQSMessageHandlerConfig = BaseHandlerConfig & { @@ -24,11 +25,25 @@ export type SQSMessageHandlerConfig = */ usePartialBatchResponses?: boolean; - /** - * This will be called to redact the message body before logging it. By - * default, the full message body is logged. - */ - redactMessageBody?: (body: string) => string; + redactionConfig?: { + /** + * This will be called to redact the message body before logging it. By + * default, the full message body is logged. + */ + redactMessageBody: (body: string) => string; + + /** + * The public encryption key used for writing messages that contain + * sensitive information but failed to be redacted. + */ + publicEncryptionKey: string; + + /** + * Logged with the encypted message to help identify the key used. For + * example, this could explain who has access to the key or how to get it. + */ + publicKeyDescription: string; + } }; export type SQSMessageAction = ( @@ -65,15 +80,30 @@ export type SQSPartialBatchResponse = { }; const safeRedactor = - (logger: LoggerInterface, redactor: (body: string) => string) => - (body: string) => { - try { - return redactor(body); - } catch (error) { - logger.error({ error, body }, 'Failed to redact message body'); - return body; - } - }; + (logger: LoggerInterface, redactionConfig: NonNullable['redactionConfig']>) => + (body: string) => { + try { + return redactionConfig.redactMessageBody(body); + } catch (error) { + let encryptedBody; + + // If redaction fails, then encrypt the message body and log it. + // Encryption allows for developers to decrypt the message if needed + // but does not log sensitive inforation the the log stream. + try { + encryptedBody = publicEncrypt(redactionConfig.publicEncryptionKey, Buffer.from(body)).toString('base64'); + } catch (error) { + // If encryption fails, then log the encryption error and replace + // the body with dummy text. + logger.error({ error }, 'Failed to encrypt message body'); + encryptedBody = '[ENCRYPTION FAILED]'; + } + + // Log the redaction error + logger.error({ error, encryptedBody, publicKeyDescription: redactionConfig.publicKeyDescription }, 'Failed to redact message body'); + return '[REDACTION FAILED]'; + } + }; /** * An abstraction for an SQS message handler. @@ -81,7 +111,7 @@ const safeRedactor = export class SQSMessageHandler { private messageActions: SQSMessageAction[] = []; - constructor(readonly config: SQSMessageHandlerConfig) {} + constructor(readonly config: SQSMessageHandlerConfig) { } /** * Adds a message action to the handler. @@ -113,17 +143,17 @@ export class SQSMessageHandler { Object.assign(context, await this.config.createRunContext(context)); // 2. Process all the records. - const redactor = this.config.redactMessageBody - ? safeRedactor(context.logger, this.config.redactMessageBody) + const redactor = this.config.redactionConfig + ? safeRedactor(context.logger, this.config.redactionConfig) : undefined; const redactedEvent = redactor ? { - ...event, - Records: event.Records.map((record) => ({ - ...record, - body: redactor(record.body), - })), - } + ...event, + Records: event.Records.map((record) => ({ + ...record, + body: redactor(record.body), + })), + } : event; context.logger.info( { event: redactedEvent }, @@ -215,13 +245,13 @@ export class SQSMessageHandler { const event: SQSEvent = { Records: messages.map( (msg) => - // We don't need to mock every field on this event -- there are lots. - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - ({ - attributes: {}, - messageId: uuid(), - body: stringifyMessage(msg), - } as any), + // We don't need to mock every field on this event -- there are lots. + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + ({ + attributes: {}, + messageId: uuid(), + body: stringifyMessage(msg), + } as any), ), }; From d812412da6f5492c38fb65efc27bd4b2100b16ca Mon Sep 17 00:00:00 2001 From: Matt Lavin Date: Tue, 20 Feb 2024 15:09:42 -0500 Subject: [PATCH 4/4] Formatting fixes --- src/sqs.test.ts | 43 ++++++++++++++---------- src/sqs.ts | 89 +++++++++++++++++++++++++++++-------------------- 2 files changed, 78 insertions(+), 54 deletions(-) diff --git a/src/sqs.test.ts b/src/sqs.test.ts index 5ff54db..a9946f9 100644 --- a/src/sqs.test.ts +++ b/src/sqs.test.ts @@ -1,8 +1,8 @@ import { v4 as uuid } from 'uuid'; import { LoggerInterface } from '@lifeomic/logging'; import { SQSMessageAction, SQSMessageHandler } from './sqs'; -import { promises as fs } from 'fs' -import { privateDecrypt } from 'crypto' +import { promises as fs } from 'fs'; +import { privateDecrypt } from 'crypto'; const logger: jest.Mocked = { info: jest.fn(), @@ -12,8 +12,11 @@ const logger: jest.Mocked = { let publicKey: string; beforeAll(async () => { - publicKey = await fs.readFile(__dirname + '/__fixtures__/public-key.pem', 'utf8') -}) + publicKey = await fs.readFile( + __dirname + '/__fixtures__/public-key.pem', + 'utf8', + ); +}); beforeEach(() => { logger.info.mockReset(); @@ -158,15 +161,21 @@ describe('SQSMessageHandler', () => { { error, encryptedBody: expect.any(String), - publicKeyDescription: 'test-public-key' + publicKeyDescription: 'test-public-key', }, 'Failed to redact message body', ); // Verify that the encrypted body can be decrypted. - const privateKey = await fs.readFile(__dirname + '/__fixtures__/private-key.pem', 'utf8') - const encryptedBody = logger.error.mock.calls[0][0].encryptedBody - const decrypted = privateDecrypt(privateKey, Buffer.from(encryptedBody, 'base64')).toString('utf8'); + const privateKey = await fs.readFile( + __dirname + '/__fixtures__/private-key.pem', + 'utf8', + ); + const encryptedBody = logger.error.mock.calls[0][0].encryptedBody; + const decrypted = privateDecrypt( + privateKey, + Buffer.from(encryptedBody, 'base64'), + ).toString('utf8'); expect(decrypted).toEqual(body); // Verify the the body was redacted. @@ -176,9 +185,9 @@ describe('SQSMessageHandler', () => { ...event, Records: event.Records.map((record: any) => ({ ...record, - body: "[REDACTION FAILED]" - })) - } + body: '[REDACTION FAILED]', + })), + }, }, 'Processing SQS topic message', ); @@ -219,8 +228,8 @@ describe('SQSMessageHandler', () => { expect(logger.error).toHaveBeenCalledWith( { error, - encryptedBody: "[ENCRYPTION FAILED]", // Signals that encryption failed - publicKeyDescription: 'test-public-key' + encryptedBody: '[ENCRYPTION FAILED]', // Signals that encryption failed + publicKeyDescription: 'test-public-key', }, 'Failed to redact message body', ); @@ -228,7 +237,7 @@ describe('SQSMessageHandler', () => { // When encryption fails, the failure is logged. expect(logger.error).toHaveBeenCalledWith( { - error: expect.anything() + error: expect.anything(), }, 'Failed to encrypt message body', ); @@ -240,9 +249,9 @@ describe('SQSMessageHandler', () => { ...event, Records: event.Records.map((record: any) => ({ ...record, - body: "[REDACTION FAILED]" - })) - } + body: '[REDACTION FAILED]', + })), + }, }, 'Processing SQS topic message', ); diff --git a/src/sqs.ts b/src/sqs.ts index 77f3448..a7a8d83 100644 --- a/src/sqs.ts +++ b/src/sqs.ts @@ -7,7 +7,7 @@ import { processWithOrdering, withHealthCheckHandling, } from './utils'; -import { publicEncrypt } from 'crypto' +import { publicEncrypt } from 'crypto'; export type SQSMessageHandlerConfig = BaseHandlerConfig & { @@ -43,7 +43,7 @@ export type SQSMessageHandlerConfig = * example, this could explain who has access to the key or how to get it. */ publicKeyDescription: string; - } + }; }; export type SQSMessageAction = ( @@ -80,30 +80,45 @@ export type SQSPartialBatchResponse = { }; const safeRedactor = - (logger: LoggerInterface, redactionConfig: NonNullable['redactionConfig']>) => - (body: string) => { + ( + logger: LoggerInterface, + redactionConfig: NonNullable< + SQSMessageHandlerConfig['redactionConfig'] + >, + ) => + (body: string) => { + try { + return redactionConfig.redactMessageBody(body); + } catch (error) { + let encryptedBody; + + // If redaction fails, then encrypt the message body and log it. + // Encryption allows for developers to decrypt the message if needed + // but does not log sensitive inforation the the log stream. try { - return redactionConfig.redactMessageBody(body); + encryptedBody = publicEncrypt( + redactionConfig.publicEncryptionKey, + Buffer.from(body), + ).toString('base64'); } catch (error) { - let encryptedBody; - - // If redaction fails, then encrypt the message body and log it. - // Encryption allows for developers to decrypt the message if needed - // but does not log sensitive inforation the the log stream. - try { - encryptedBody = publicEncrypt(redactionConfig.publicEncryptionKey, Buffer.from(body)).toString('base64'); - } catch (error) { - // If encryption fails, then log the encryption error and replace - // the body with dummy text. - logger.error({ error }, 'Failed to encrypt message body'); - encryptedBody = '[ENCRYPTION FAILED]'; - } - - // Log the redaction error - logger.error({ error, encryptedBody, publicKeyDescription: redactionConfig.publicKeyDescription }, 'Failed to redact message body'); - return '[REDACTION FAILED]'; + // If encryption fails, then log the encryption error and replace + // the body with dummy text. + logger.error({ error }, 'Failed to encrypt message body'); + encryptedBody = '[ENCRYPTION FAILED]'; } - }; + + // Log the redaction error + logger.error( + { + error, + encryptedBody, + publicKeyDescription: redactionConfig.publicKeyDescription, + }, + 'Failed to redact message body', + ); + return '[REDACTION FAILED]'; + } + }; /** * An abstraction for an SQS message handler. @@ -111,7 +126,7 @@ const safeRedactor = export class SQSMessageHandler { private messageActions: SQSMessageAction[] = []; - constructor(readonly config: SQSMessageHandlerConfig) { } + constructor(readonly config: SQSMessageHandlerConfig) {} /** * Adds a message action to the handler. @@ -148,12 +163,12 @@ export class SQSMessageHandler { : undefined; const redactedEvent = redactor ? { - ...event, - Records: event.Records.map((record) => ({ - ...record, - body: redactor(record.body), - })), - } + ...event, + Records: event.Records.map((record) => ({ + ...record, + body: redactor(record.body), + })), + } : event; context.logger.info( { event: redactedEvent }, @@ -245,13 +260,13 @@ export class SQSMessageHandler { const event: SQSEvent = { Records: messages.map( (msg) => - // We don't need to mock every field on this event -- there are lots. - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - ({ - attributes: {}, - messageId: uuid(), - body: stringifyMessage(msg), - } as any), + // We don't need to mock every field on this event -- there are lots. + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + ({ + attributes: {}, + messageId: uuid(), + body: stringifyMessage(msg), + } as any), ), };