openhim-core
Version:
The OpenHIM core application that provides logging and routing of http requests
350 lines (297 loc) • 16.4 kB
JavaScript
/* eslint-env mocha */
/* eslint no-unused-expressions:0 */
import fs from 'fs'
import * as auditing from '../../src/auditing'
import { AuditModel, AuditMetaModel } from '../../src/model/audits'
import { config } from '../../src/config'
import * as utils from '../utils'
import { promisify } from 'util'
import * as sinon from 'sinon'
import * as constants from '../constants'
import {testAuditIHEDICOM, testAuditIHERFC3881, testAuditMessage, testAuditParticipantQuery} from '../fixtures'
config.auditing = config.get('auditing')
describe('Auditing', () => {
beforeEach(async () => {
await Promise.all([
AuditModel.deleteMany({}),
AuditMetaModel.deleteMany({})
])
})
describe('.processAudit', () => {
const validateSyslog = function (syslog) {
syslog.should.exist
syslog.msgID.should.be.equal('IHE+RFC-3881')
syslog.pid.should.be.equal('9293')
syslog.appName.should.be.equal('java')
syslog.host.should.be.equal('Hanness-MBP.jembi.local')
syslog.time.should.exist
syslog.type.should.be.equal('RFC5424')
syslog.severity.should.be.equal('notice')
syslog.facility.should.be.equal('sec')
syslog.severityID.should.be.equal(5)
syslog.facilityID.should.be.equal(10)
return syslog.prival.should.be.equal(85)
}
it('should parse audit message and persist it to the database', done =>
auditing.processAudit(testAuditMessage, () =>
AuditModel.find({}, (err, audits) => {
if (err) { return done(err) }
audits.length.should.be.exactly(1)
audits[0].rawMessage.should.be.exactly(testAuditMessage)
validateSyslog(audits[0].syslog)
audits[0].eventIdentification.should.exist
audits[0].eventIdentification.eventDateTime.should.exist
audits[0].eventIdentification.eventOutcomeIndicator.should.be.equal('0')
audits[0].eventIdentification.eventActionCode.should.be.equal('E')
audits[0].eventIdentification.eventID.code.should.be.equal('110112')
audits[0].eventIdentification.eventID.displayName.should.be.equal('Query')
audits[0].eventIdentification.eventID.codeSystemName.should.be.equal('DCM')
audits[0].eventIdentification.eventTypeCode.code.should.be.equal('ITI-9')
audits[0].eventIdentification.eventTypeCode.displayName.should.be.equal('PIX Query')
audits[0].eventIdentification.eventTypeCode.codeSystemName.should.be.equal('IHE Transactions')
audits[0].activeParticipant.length.should.be.exactly(2)
audits[0].activeParticipant[0].userID.should.be.equal('openhim-mediator-ohie-xds|openhim')
audits[0].activeParticipant[0].alternativeUserID.should.be.equal('9293')
audits[0].activeParticipant[0].userIsRequestor.should.be.equal('true')
audits[0].activeParticipant[0].networkAccessPointID.should.be.equal('192.168.1.111')
audits[0].activeParticipant[0].networkAccessPointTypeCode.should.be.equal('2')
audits[0].activeParticipant[0].roleIDCode.code.should.be.equal('110153')
audits[0].activeParticipant[0].roleIDCode.displayName.should.be.equal('Source')
audits[0].activeParticipant[0].roleIDCode.codeSystemName.should.be.equal('DCM')
audits[0].activeParticipant[1].userID.should.be.equal('pix|pix')
audits[0].activeParticipant[1].alternativeUserID.should.be.equal('2100')
audits[0].activeParticipant[1].userIsRequestor.should.be.equal('false')
audits[0].activeParticipant[1].networkAccessPointID.should.be.equal('localhost')
audits[0].activeParticipant[1].networkAccessPointTypeCode.should.be.equal('1')
audits[0].activeParticipant[1].roleIDCode.code.should.be.equal('110152')
audits[0].activeParticipant[1].roleIDCode.displayName.should.be.equal('Destination')
audits[0].activeParticipant[1].roleIDCode.codeSystemName.should.be.equal('DCM')
audits[0].auditSourceIdentification.should.exist
audits[0].auditSourceIdentification.auditSourceID.should.be.equal('openhim')
audits[0].participantObjectIdentification.length.should.be.exactly(2)
audits[0].participantObjectIdentification[0].participantObjectID.should.be.equal('fc133984036647e^^^&1.3.6.1.4.1.21367.2005.13.20.3000&ISO')
audits[0].participantObjectIdentification[0].participantObjectTypeCode.should.be.equal('1')
audits[0].participantObjectIdentification[0].participantObjectTypeCodeRole.should.be.equal('1')
audits[0].participantObjectIdentification[0].participantObjectIDTypeCode.code.should.be.equal('2')
audits[0].participantObjectIdentification[0].participantObjectIDTypeCode.displayName.should.be.equal('PatientNumber')
audits[0].participantObjectIdentification[0].participantObjectIDTypeCode.codeSystemName.should.be.equal('RFC-3881')
audits[0].participantObjectIdentification[1].participantObjectID.should.be.equal('c7bd7244-29bc-4ab5-80ee-74b56eed9db0')
audits[0].participantObjectIdentification[1].participantObjectTypeCode.should.be.equal('2')
audits[0].participantObjectIdentification[1].participantObjectTypeCodeRole.should.be.equal('24')
audits[0].participantObjectIdentification[1].participantObjectIDTypeCode.code.should.be.equal('ITI-9')
audits[0].participantObjectIdentification[1].participantObjectIDTypeCode.displayName.should.be.equal('PIX Query')
audits[0].participantObjectIdentification[1].participantObjectIDTypeCode.codeSystemName.should.be.equal('IHE Transactions')
audits[0].participantObjectIdentification[1].participantObjectQuery.should.be.equal(testAuditParticipantQuery)
audits[0].participantObjectIdentification[1].participantObjectDetail.should.exist
audits[0].participantObjectIdentification[1].participantObjectDetail.type.should.be.equal('MSH-10')
audits[0].participantObjectIdentification[1].participantObjectDetail.value.should.be.equal('YmIwNzNiODUtNTdhOS00MGJhLTkyOTEtMTVkMjExOGQ0OGYz')
return done()
})
)
)
it('should still persist to the database even if the audit includes a non-xml message', (done) => {
const nonXmlAudit = '<85>1 2015-03-05T12:52:31.358+02:00 Hanness-MBP.jembi.local java 9293 IHE+RFC-3881 - this is a message?>'
return auditing.processAudit(nonXmlAudit, () =>
AuditModel.find({}, (err, audits) => {
if (err) { return done(err) }
audits.length.should.be.exactly(1)
audits[0].rawMessage.should.be.exactly(nonXmlAudit)
validateSyslog(audits[0].syslog)
return done()
})
)
})
it('should still persist to the database even if the audit includes an unexpected type of xml message', (done) => {
const nonXmlAudit = '<85>1 2015-03-05T12:52:31.358+02:00 Hanness-MBP.jembi.local java 9293 IHE+RFC-3881 - <data>data</data>?>'
return auditing.processAudit(nonXmlAudit, () =>
AuditModel.find({}, (err, audits) => {
if (err) { return done(err) }
audits.length.should.be.exactly(1)
audits[0].rawMessage.should.be.exactly(nonXmlAudit)
validateSyslog(audits[0].syslog)
return done()
})
)
})
it('should reject bad messages', (done) => {
const badAudit = 'this message is a garbage message'
auditing.processAudit(badAudit, () =>
AuditModel.find({}, (err, audits) => {
if (err) { return done(err) }
audits.length.should.be.exactly(0)
return done()
})
)
})
it('should populate audit meta collection with filter fields', done =>
auditing.processAudit(testAuditMessage, () =>
AuditMetaModel.findOne({}, (err, auditMeta) => {
if (err) { return done(err) }
auditMeta.eventID.should.exist
auditMeta.eventID.length.should.be.exactly(1)
auditMeta.eventID[0].code.should.be.equal('110112')
auditMeta.eventID[0].displayName.should.be.equal('Query')
auditMeta.eventID[0].codeSystemName.should.be.equal('DCM')
auditMeta.eventType.should.exist
auditMeta.eventType.length.should.be.exactly(1)
auditMeta.eventType[0].code.should.be.equal('ITI-9')
auditMeta.eventType[0].displayName.should.be.equal('PIX Query')
auditMeta.eventType[0].codeSystemName.should.be.equal('IHE Transactions')
auditMeta.activeParticipantRoleID.should.exist
auditMeta.activeParticipantRoleID.length.should.be.exactly(2)
auditMeta.activeParticipantRoleID[0].code.should.be.equal('110153')
auditMeta.activeParticipantRoleID[0].displayName.should.be.equal('Source')
auditMeta.activeParticipantRoleID[0].codeSystemName.should.be.equal('DCM')
auditMeta.activeParticipantRoleID[1].code.should.be.equal('110152')
auditMeta.activeParticipantRoleID[1].displayName.should.be.equal('Destination')
auditMeta.activeParticipantRoleID[1].codeSystemName.should.be.equal('DCM')
auditMeta.participantObjectIDTypeCode.should.exist
auditMeta.participantObjectIDTypeCode.length.should.be.exactly(2)
auditMeta.participantObjectIDTypeCode[0].code.should.be.equal('2')
auditMeta.participantObjectIDTypeCode[0].displayName.should.be.equal('PatientNumber')
auditMeta.participantObjectIDTypeCode[0].codeSystemName.should.be.equal('RFC-3881')
auditMeta.participantObjectIDTypeCode[1].code.should.be.equal('ITI-9')
auditMeta.participantObjectIDTypeCode[1].displayName.should.be.equal('PIX Query')
auditMeta.participantObjectIDTypeCode[1].codeSystemName.should.be.equal('IHE Transactions')
auditMeta.auditSourceID.should.exist
auditMeta.auditSourceID.length.should.be.exactly(1)
auditMeta.auditSourceID[0].should.be.equal('openhim')
return done()
})
)
)
it('should not duplicate filter fields in audit meta collection', done =>
auditing.processAudit(testAuditMessage, () =>
auditing.processAudit(testAuditMessage, () =>
AuditMetaModel.findOne({}, (err, auditMeta) => {
if (err) { return done(err) }
auditMeta.eventID.length.should.be.exactly(1)
auditMeta.eventType.length.should.be.exactly(1)
auditMeta.activeParticipantRoleID.length.should.be.exactly(2)
auditMeta.participantObjectIDTypeCode.length.should.be.exactly(2)
auditMeta.auditSourceID.length.should.be.exactly(1)
return done()
})
)
)
)
})
describe('IHE Samples', () => {
const validateIHEAudit = function (type, audit) {
audit.syslog.should.exist
audit.syslog.msgID.should.be.equal(type)
audit.syslog.pid.should.be.equal('521')
audit.syslog.appName.should.be.equal('OHT')
audit.syslog.host.should.be.equal('cabig-h1')
audit.syslog.time.should.exist
audit.syslog.type.should.be.equal('RFC5424')
audit.syslog.severity.should.be.equal('notice')
audit.syslog.facility.should.be.equal('sec')
audit.syslog.severityID.should.be.equal(5)
audit.syslog.facilityID.should.be.equal(10)
audit.syslog.prival.should.be.equal(85)
audit.eventIdentification.should.exist
audit.eventIdentification.eventDateTime.should.exist
audit.eventIdentification.eventOutcomeIndicator.should.be.equal('0')
audit.eventIdentification.eventActionCode.should.be.equal('E')
audit.eventIdentification.eventID.code.should.be.equal('110114')
audit.eventIdentification.eventID.displayName.should.be.equal('UserAuthenticated')
audit.eventIdentification.eventID.codeSystemName.should.be.equal('DCM')
audit.eventIdentification.eventTypeCode.code.should.be.equal('110122')
audit.eventIdentification.eventTypeCode.displayName.should.be.equal('Login')
audit.eventIdentification.eventTypeCode.codeSystemName.should.be.equal('DCM')
audit.activeParticipant.length.should.be.exactly(2)
audit.activeParticipant[0].userID.should.be.equal('fe80::5999:d1ef:63de:a8bb%11')
audit.activeParticipant[0].userIsRequestor.should.be.equal('true')
audit.activeParticipant[0].networkAccessPointID.should.be.equal('125.20.175.12')
audit.activeParticipant[0].networkAccessPointTypeCode.should.be.equal('1')
audit.activeParticipant[0].roleIDCode.code.should.be.equal('110150')
audit.activeParticipant[0].roleIDCode.displayName.should.be.equal('Application')
audit.activeParticipant[0].roleIDCode.codeSystemName.should.be.equal('DCM')
audit.activeParticipant[1].userID.should.be.equal('farley.granger@wb.com')
audit.activeParticipant[1].userIsRequestor.should.be.equal('true')
audit.auditSourceIdentification.should.exist
audit.auditSourceIdentification.auditSourceID.should.be.equal('farley.granger@wb.com')
return audit.auditSourceIdentification.auditEnterpriseSiteID.should.be.equal('End User')
}
it('should parse IHE sample RFC3881 audit message and persist it to the database', done =>
auditing.processAudit(testAuditIHERFC3881, () =>
AuditModel.find({}, (err, audits) => {
if (err) { return done(err) }
audits.length.should.be.exactly(1)
audits[0].rawMessage.should.be.exactly(testAuditIHERFC3881)
validateIHEAudit('IHE+RFC-3881', audits[0])
return done()
})
)
)
it('should parse IHE sample DICOM audit message and persist it to the database', done =>
auditing.processAudit(testAuditIHEDICOM, () =>
AuditModel.find({}, (err, audits) => {
if (err) { return done(err) }
audits.length.should.be.exactly(1)
audits[0].rawMessage.should.be.exactly(testAuditIHEDICOM)
validateIHEAudit('IHE+DICOM', audits[0])
return done()
})
)
)
})
describe('.sendAuditEvent', () => {
const testString = 'hello - this is a test'
let _restore = null
const ca = [fs.readFileSync('test/resources/server-tls/cert.pem')]
let servers
let spy
before(async () => {
_restore = JSON.stringify(config.auditing.auditEvents)
spy = sinon.spy(data => data)
servers = await Promise.all([
utils.createMockUdpServer(spy),
utils.createMockTCPServer(spy),
utils.createMockTLSServerWithMutualAuth(spy)
])
await utils.setupTestKeystore(undefined, undefined, ca)
})
afterEach(() => {
spy.resetHistory()
config.auditing.auditEvents.interface = undefined
config.auditing.auditEvents.port = undefined
})
after(async () => {
config.auditing.auditEvents = JSON.parse(_restore)
await Promise.all(servers.map(s => s.close()))
await utils.cleanupTestKeystore()
})
it('should process audit internally', async () => {
config.auditing.auditEvents.interface = 'internal'
await promisify(auditing.sendAuditEvent)(testAuditMessage)
const audits = await AuditModel.find({})
audits.length.should.be.exactly(1)
audits[0].rawMessage.should.be.exactly(testAuditMessage)
})
it('should send an audit event via UDP', async () => {
config.auditing.auditEvents.interface = 'udp'
config.auditing.auditEvents.port = constants.UDP_PORT
await promisify(auditing.sendAuditEvent)(testString)
// Needs to wait for event loop to catch up
await promisify(setImmediate)()
spy.callCount.should.equal(1)
spy.calledWith(`${testString.length} ${testString}`)
})
it('should send an audit event via TLS', async () => {
config.auditing.auditEvents.interface = 'tls'
config.auditing.auditEvents.port = constants.TLS_PORT
await promisify(auditing.sendAuditEvent)(testString)
spy.callCount.should.equal(1)
spy.calledWith(`${testString.length} ${testString}`)
})
it('should send an audit event via TCP', async () => {
config.auditing.auditEvents.interface = 'tcp'
config.auditing.auditEvents.port = constants.TCP_PORT
await promisify(auditing.sendAuditEvent)(testString)
spy.callCount.should.equal(1)
spy.calledWith(`${testString.length} ${testString}`)
})
})
})