From 80d595fcc57fac86cc7a9c9b574b2f0844e27407 Mon Sep 17 00:00:00 2001 From: Dylan Mendelowitz Date: Fri, 26 Mar 2021 15:46:56 -0400 Subject: [PATCH 1/5] mask MRN when referenced outside Patient --- src/cli/app.js | 12 ++++++++ src/helpers/patientUtils.js | 24 +++++++++++++++ test/helpers/fixtures/bundle-with-mrn-id.json | 30 +++++++++++++++++++ test/helpers/patientUtils.test.js | 18 ++++++++++- 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 test/helpers/fixtures/bundle-with-mrn-id.json diff --git a/src/cli/app.js b/src/cli/app.js index 77cbd0b7..c4fc3033 100644 --- a/src/cli/app.js +++ b/src/cli/app.js @@ -6,6 +6,7 @@ const logger = require('../helpers/logger'); const { RunInstanceLogger } = require('./RunInstanceLogger'); const { sendEmailNotification, zipErrors } = require('./emailNotifications'); const { extractDataForPatients } = require('./mcodeExtraction'); +const { maskMRN } = require('../helpers/patientUtils'); function getConfig(pathToConfig) { // Checks pathToConfig points to valid JSON file @@ -109,6 +110,17 @@ async function mcodeApp(Client, fromDate, toDate, pathToConfig, pathToRunLogs, d } } + // check if config specifies that MRN needs to be masked + // if it does need to be masked, mask all references to MRN outside of the patient resource + const patientConfig = config.extractors.filter((e) => e.type === 'CSVPatientExtractor')[0]; + if ('constructorArgs' in patientConfig && 'mask' in patientConfig.constructorArgs) { + if (patientConfig.constructorArgs.mask.includes('mrn')) { + extractedData.forEach((bundle) => { + maskMRN(bundle); + }); + } + } + // Finally, save the data to disk const outputPath = './output'; if (!fs.existsSync(outputPath)) { diff --git a/src/helpers/patientUtils.js b/src/helpers/patientUtils.js index b9a1212a..c0fb92c7 100644 --- a/src/helpers/patientUtils.js +++ b/src/helpers/patientUtils.js @@ -1,5 +1,6 @@ /* eslint-disable no-underscore-dangle */ const fhirpath = require('fhirpath'); +const shajs = require('sha.js'); const { extensionArr, dataAbsentReasonExtension } = require('../templates/snippets/extension.js'); // Based on the OMB Ethnicity table found here:http://hl7.org/fhir/us/core/STU3.1/ValueSet-omb-ethnicity-category.html @@ -147,10 +148,33 @@ function maskPatientData(bundle, mask) { }); } +/** + * Mask all references to the MRN used as an id + * Currently, the MRN appears as an id in 'subject' and 'individual' objects in other resources + * and in the 'id' and 'fullUrl' fields of the Patient resource. + * Replaces the MRN with a hash of the MRN + * @param {Object} bundle a FHIR bundle with a Patient resource and other resources + */ +function maskMRN(bundle) { + const patient = fhirpath.evaluate(bundle, 'Bundle.entry.where(resource.resourceType=\'Patient\')')[0]; + if (patient === undefined) throw Error('No Patient resource in bundle. Could not mask MRN.'); + const mrn = patient.resource.id; + const masked = shajs('sha256').update(mrn).digest('hex'); + patient.fullUrl = `urn:uuid:${masked}`; + patient.resource.id = masked; + const subjects = fhirpath.evaluate(bundle, `Bundle.entry.resource.subject.where(reference='urn:uuid:${mrn}')`); + const individuals = fhirpath.evaluate(bundle, `Bundle.entry.resource.individual.where(reference='urn:uuid:${mrn}')`); + const mrnOccurances = subjects.concat(individuals); + for (let i = 0; i < mrnOccurances.length; i += 1) { + mrnOccurances[i].reference = `urn:uuid:${masked}`; + } +} + module.exports = { getEthnicityDisplay, getRaceCodesystem, getRaceDisplay, getPatientName, maskPatientData, + maskMRN, }; diff --git a/test/helpers/fixtures/bundle-with-mrn-id.json b/test/helpers/fixtures/bundle-with-mrn-id.json new file mode 100644 index 00000000..e4622a62 --- /dev/null +++ b/test/helpers/fixtures/bundle-with-mrn-id.json @@ -0,0 +1,30 @@ +{ + "resourceType": "Bundle", + "type": "collection", + "entry": [ + { + "fullUrl": "urn:uuid:123", + "resource": { + "resourceType": "Patient", + "id": "123" + + } + }, + { + "resource": { + "subject": { + "reference": "urn:uuid:123", + "type": "Patient" + } + } + }, + { + "resource": { + "individual": { + "reference": "urn:uuid:123", + "type": "Patient" + } + } + } + ] +} \ No newline at end of file diff --git a/test/helpers/patientUtils.test.js b/test/helpers/patientUtils.test.js index 467ee9b6..201dc325 100644 --- a/test/helpers/patientUtils.test.js +++ b/test/helpers/patientUtils.test.js @@ -1,9 +1,11 @@ const _ = require('lodash'); +const shajs = require('sha.js'); const { - getEthnicityDisplay, getRaceCodesystem, getRaceDisplay, getPatientName, maskPatientData, + getEthnicityDisplay, getRaceCodesystem, getRaceDisplay, getPatientName, maskPatientData, maskMRN, } = require('../../src/helpers/patientUtils'); const examplePatient = require('../extractors/fixtures/csv-patient-bundle.json'); const exampleMaskedPatient = require('./fixtures/masked-patient-bundle.json'); +const exampleBundleWithMRN = require('./fixtures/bundle-with-mrn-id.json'); describe('PatientUtils', () => { describe('getEthnicityDisplay', () => { @@ -101,4 +103,18 @@ describe('PatientUtils', () => { expect(() => maskPatientData(bundle, ['this is an invalid field', 'mrn'])).toThrowError(); }); }); + describe('maskMRN', () => { + test('all occurances of the MRN as an id should be masked by a hashed version', () => { + const bundle = _.cloneDeep(exampleBundleWithMRN); + const hashedMRN = shajs('sha256').update(bundle.entry[0].resource.id).digest('hex'); + maskMRN(bundle); + expect(bundle.entry[0].resource.id).toEqual(hashedMRN); + expect(bundle.entry[0].fullUrl).toEqual(`urn:uuid:${hashedMRN}`); + expect(bundle.entry[1].resource.subject.reference).toEqual(`urn:uuid:${hashedMRN}`); + expect(bundle.entry[2].resource.individual.reference).toEqual(`urn:uuid:${hashedMRN}`); + }); + test('should throw error when there is no Patient resource in bundle', () => { + expect(() => maskMRN({})).toThrowError(); + }); + }); }); From 40d798adb2c2f8fc2e81ffc5a1ae8605d6ea1e85 Mon Sep 17 00:00:00 2001 From: dmendelowitz <79325127+dmendelowitz@users.noreply.github.com> Date: Mon, 29 Mar 2021 12:21:44 -0400 Subject: [PATCH 2/5] Update src/cli/app.js Co-authored-by: Matthew Gramigna --- src/cli/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/app.js b/src/cli/app.js index c4fc3033..de1374d1 100644 --- a/src/cli/app.js +++ b/src/cli/app.js @@ -112,7 +112,7 @@ async function mcodeApp(Client, fromDate, toDate, pathToConfig, pathToRunLogs, d // check if config specifies that MRN needs to be masked // if it does need to be masked, mask all references to MRN outside of the patient resource - const patientConfig = config.extractors.filter((e) => e.type === 'CSVPatientExtractor')[0]; + const patientConfig = config.extractors.find((e) => e.type === 'CSVPatientExtractor'); if ('constructorArgs' in patientConfig && 'mask' in patientConfig.constructorArgs) { if (patientConfig.constructorArgs.mask.includes('mrn')) { extractedData.forEach((bundle) => { From 859a83347b1120076f8bfc2b536b5f34b99d61c8 Mon Sep 17 00:00:00 2001 From: dmendelowitz <79325127+dmendelowitz@users.noreply.github.com> Date: Mon, 29 Mar 2021 12:21:58 -0400 Subject: [PATCH 3/5] Update src/cli/app.js Co-authored-by: Matthew Gramigna --- src/cli/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/app.js b/src/cli/app.js index de1374d1..236ecc6e 100644 --- a/src/cli/app.js +++ b/src/cli/app.js @@ -113,7 +113,7 @@ async function mcodeApp(Client, fromDate, toDate, pathToConfig, pathToRunLogs, d // check if config specifies that MRN needs to be masked // if it does need to be masked, mask all references to MRN outside of the patient resource const patientConfig = config.extractors.find((e) => e.type === 'CSVPatientExtractor'); - if ('constructorArgs' in patientConfig && 'mask' in patientConfig.constructorArgs) { + if (patientConfig && ('constructorArgs' in patientConfig && 'mask' in patientConfig.constructorArgs)) { if (patientConfig.constructorArgs.mask.includes('mrn')) { extractedData.forEach((bundle) => { maskMRN(bundle); From f31590aae6545c1ff9cee672bcb703beae5d3b73 Mon Sep 17 00:00:00 2001 From: Dylan Mendelowitz Date: Mon, 29 Mar 2021 12:23:41 -0400 Subject: [PATCH 4/5] fixing type in variable name --- src/helpers/patientUtils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/helpers/patientUtils.js b/src/helpers/patientUtils.js index c0fb92c7..f9c48531 100644 --- a/src/helpers/patientUtils.js +++ b/src/helpers/patientUtils.js @@ -164,9 +164,9 @@ function maskMRN(bundle) { patient.resource.id = masked; const subjects = fhirpath.evaluate(bundle, `Bundle.entry.resource.subject.where(reference='urn:uuid:${mrn}')`); const individuals = fhirpath.evaluate(bundle, `Bundle.entry.resource.individual.where(reference='urn:uuid:${mrn}')`); - const mrnOccurances = subjects.concat(individuals); - for (let i = 0; i < mrnOccurances.length; i += 1) { - mrnOccurances[i].reference = `urn:uuid:${masked}`; + const mrnOccurrances = subjects.concat(individuals); + for (let i = 0; i < mrnOccurrances.length; i += 1) { + mrnOccurrances[i].reference = `urn:uuid:${masked}`; } } From 71eb8039d37696f0681ae1b5ba83c9145bb21e7b Mon Sep 17 00:00:00 2001 From: Dylan Mendelowitz Date: Mon, 29 Mar 2021 12:26:48 -0400 Subject: [PATCH 5/5] actually fixing typo in variable name --- src/helpers/patientUtils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/helpers/patientUtils.js b/src/helpers/patientUtils.js index f9c48531..47eaded2 100644 --- a/src/helpers/patientUtils.js +++ b/src/helpers/patientUtils.js @@ -164,9 +164,9 @@ function maskMRN(bundle) { patient.resource.id = masked; const subjects = fhirpath.evaluate(bundle, `Bundle.entry.resource.subject.where(reference='urn:uuid:${mrn}')`); const individuals = fhirpath.evaluate(bundle, `Bundle.entry.resource.individual.where(reference='urn:uuid:${mrn}')`); - const mrnOccurrances = subjects.concat(individuals); - for (let i = 0; i < mrnOccurrances.length; i += 1) { - mrnOccurrances[i].reference = `urn:uuid:${masked}`; + const mrnOccurrences = subjects.concat(individuals); + for (let i = 0; i < mrnOccurrences.length; i += 1) { + mrnOccurrences[i].reference = `urn:uuid:${masked}`; } }