diff --git a/README.md b/README.md index 117c0828..bdaa1853 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ In order to send an email, users must specify the hostname or IP address of an S - `port`: (Optional) The port to connect to (defaults to 587) - `to`: Comma separated list or an array of recipients email addresses that will appear on the _To:_ field - `from`: (Optional) The email address of the sender. All email addresses can be plain `'sender@server.com'` or formatted `'"Sender Name" sender@server.com'` (defaults to mcode-extraction-errors@mitre.org, which cannot receive reply emails) +- `tlsRejectUnauthorized`: (Optional) A boolean value to set the [node.js TLSSocket option](https://nodejs.org/api/tls.html#tls_class_tls_tlssocket) for rejecting any unauthorized connections, `tls.rejectUnauthorized`. (defaults to `true`) An example of this object can be found in [`config/csv.config.example.json`](config/csv.config.example.json). @@ -126,6 +127,23 @@ Users can specify a different location for the file by using the `--run-log-file node src/cli/cli.js --run-log-filepath path/to/file.json ``` +### Masking Patient Data + +Currently, patient data can be masked within the extracted `Patient` resource. When masked, the value of the field will be replaced with a [Data Absent Reason extension](https://www.hl7.org/fhir/extension-data-absent-reason.html) with the code `masked`. +Patient properties that can be masked are: `gender`, `mrn`, `name`, `address`, `birthDate`, `language`, `ethnicity`, `birthsex`, and `race`. +To mask a property, provide an array of the properties to mask in the `constructorArgs` of the Patient extractor. For example, the following configuration can be used to mask `address` and `birthDate`: + +```bash +{ + "label": "patient", + "type": "CSVPatientExtractor", + "constructorArgs": { + "filePath": "./data/patient-information.csv" + "mask": ["address", "birthDate"] + } +} +``` + ### Extraction Date Range The mCODE Extraction Client will extract all data that is provided in the CSV files by default, regardless of any dates associated with each row of data. It is recommended that any required date filtering is performed outside of the scope of this client. diff --git a/config/csv.config.example.json b/config/csv.config.example.json index a29fa771..9c936e9f 100644 --- a/config/csv.config.example.json +++ b/config/csv.config.example.json @@ -8,7 +8,8 @@ "to": [ "demo@example.com", "test@example.com" - ] + ], + "tlsRejectUnauthorized": true }, "extractors": [ { diff --git a/src/cli/emailNotifications.js b/src/cli/emailNotifications.js index eed8c112..edb5dc41 100644 --- a/src/cli/emailNotifications.js +++ b/src/cli/emailNotifications.js @@ -57,9 +57,15 @@ async function sendEmailNotification(notificationInfo, errors, debug = false) { emailBody += 'The stack trace information can be seen in the terminal as well as in the notification email.'; } + // Ensure that the tlsRejectUnauthorized property is a boolean + if (notificationInfo.tlsRejectUnauthorized && (notificationInfo.tlsRejectUnauthorized !== true || notificationInfo.tlsRejectUnauthorized !== false)) { + logger.warn('The notificationInfo.tlsRejectUnauthorized should be a boolean value. The value provided will not be used.'); + } + const transporter = nodemailer.createTransport({ host: notificationInfo.host, ...(notificationInfo.port && { port: notificationInfo.port }), + ...((notificationInfo.tlsRejectUnauthorized === true || notificationInfo.tlsRejectUnauthorized === false) && { tls: { rejectUnauthorized: notificationInfo.tlsRejectUnauthorized } }), }); logger.debug('Sending email with error information'); diff --git a/test/cli/emailNotifications.test.js b/test/cli/emailNotifications.test.js index cc556220..e201ce82 100644 --- a/test/cli/emailNotifications.test.js +++ b/test/cli/emailNotifications.test.js @@ -71,6 +71,7 @@ describe('sendEmailNotification', () => { port: 123, to: ['something@example.com', 'someone@example.com'], from: 'other@example.com', + tlsRejectUnauthorized: false, }; const errors = { 0: [], @@ -79,7 +80,7 @@ describe('sendEmailNotification', () => { }; await expect(sendEmailNotification(notificationInfo, errors, false)).resolves.not.toThrow(); - expect(createTransportSpy).toBeCalledWith({ host: notificationInfo.host, port: notificationInfo.port }); + expect(createTransportSpy).toBeCalledWith({ host: notificationInfo.host, port: notificationInfo.port, tls: { rejectUnauthorized: notificationInfo.tlsRejectUnauthorized } }); expect(sendMailMock).toBeCalled(); const sendMailMockArgs = sendMailMock.mock.calls[0][0]; expect(sendMailMockArgs.to).toEqual(notificationInfo.to); @@ -93,6 +94,48 @@ describe('sendEmailNotification', () => { expect(sendMailMockArgs.text).not.toMatch(/Error at line 4`/i); }); + it('should send an email with tlsRejectUnauthorized set to true, false, and not set', async () => { + const notificationInfoTLSFalse = { + host: 'my.host.com', + to: ['something@example.com', 'someone@example.com'], + tlsRejectUnauthorized: false, + }; + const notificationInfoTLSTrue = { + host: 'my.host.com', + to: ['something@example.com', 'someone@example.com'], + tlsRejectUnauthorized: true, + }; + const notificationInfoNoTLS = { + host: 'my.host.com', + to: ['something@example.com', 'someone@example.com'], + }; + const notificationInfoUnexpectedTLS = { + host: 'my.host.com', + to: ['something@example.com', 'someone@example.com'], + tlsRejectUnauthorized: 'true', // Any value that is not true or false will log a warning and not be used + }; + const errors = { + 0: [], + 1: [{ message: 'something bad', stack: 'Error at line 1' }], + }; + createTransportSpy.mockReturnValueOnce({ sendMail: sendMailMock }); + await expect(sendEmailNotification(notificationInfoTLSFalse, errors, false)).resolves.not.toThrow(); + expect(createTransportSpy).toBeCalledWith({ host: notificationInfoTLSFalse.host, port: notificationInfoTLSFalse.port, tls: { rejectUnauthorized: false } }); + + createTransportSpy.mockReturnValueOnce({ sendMail: sendMailMock }); + await expect(sendEmailNotification(notificationInfoTLSTrue, errors, false)).resolves.not.toThrow(); + expect(createTransportSpy).toBeCalledWith({ host: notificationInfoTLSTrue.host, port: notificationInfoTLSTrue.port, tls: { rejectUnauthorized: true } }); + + createTransportSpy.mockReturnValueOnce({ sendMail: sendMailMock }); + await expect(sendEmailNotification(notificationInfoNoTLS, errors, false)).resolves.not.toThrow(); + expect(createTransportSpy).toBeCalledWith({ host: notificationInfoNoTLS.host, port: notificationInfoNoTLS.port }); // No tls object set + + createTransportSpy.mockReturnValueOnce({ sendMail: sendMailMock }); + await expect(sendEmailNotification(notificationInfoUnexpectedTLS, errors, false)).resolves.not.toThrow(); + // A warning will be logged and the unexpected value will not be used + expect(createTransportSpy).toBeCalledWith({ host: notificationInfoUnexpectedTLS.host, port: notificationInfoUnexpectedTLS.port }); + }); + it('should send an email with stack traces if debug flag was used', async () => { createTransportSpy.mockReturnValueOnce({ sendMail: sendMailMock }); const notificationInfo = {