Gateway Auditing Hooks

Gateway Auditing Hooks

Auditing in the Gateway is done in the same manner as Connexion - via a custom code API to allow the integration of auditing data into your primary auditing infrastructure. These hooks work in the same manner as Alerting and Monitoring - we expose functions within custom code which can execute your code as audit event objects are generated.

Operations are also logged to the events/alerting. You can track basic audit information just by querying for events.

Two categories of auditing have been added to the Gateway:

  1. PHI User action auditing: Whenever a user interacts with, or alters, message data. Currently this is limited to users interacting with messages via the queue device UI. Actions such as querying, viewing, copying/pasting, deleting, altering, queuing etc. are filed here.

  2. Non-PHI User action auditing: User actions which are not related to messages. These are configuration changes and state changes (start/stop/pause etc.).

Unlike Connexion, individual messages are not audited, as they exist only on the customer site.

By default, all auditing is disabled. You can enable each type of auditing (non-phi user interactions and phi user actions) via check boxes.

The Gateway utilizes several buffering mechanisms (memory, disk) in order to minimize the impact of high-volume auditing.

Audit settings are accessed via the Auditing tab on the System Configuration screen:

image-20251006-230536.png

The code shown above is a boilerplate example and it is expected that you will replace this with your own implementation.

The main methods are:

Start: called when the auditing sub-system starts (at process launch, or when the audit configuration changes). This is where you can initialize any class-level variables.

Stop: called immediately before the auditing sub-system stops (at process stop, or, when the audit configuration changes).

ProcessAuditRecordsAsync: called when one or more non-phi audit records are available for processing. The boilerplate example code shows the expected access pattern.

ProcessPhiAuditRecordsAsync: called when one or more phi audit records are available for processing. The boilerplate example code shows the expected access pattern.

There are two separate methods called depending on whether the audit record contains phi or not. This makes it easy to treat phi audit data distinctly from non-phi audit data (for example, you may be forwarding phi audit data to a different repository than non-phi data).

Notes:

Audit data is buffered both in memory and on disk. Audit data is written into files which are then consumed on a separate thread. These files are read once they contain a maximum number of audit records, or, they are at least 2 minutes old. This means that there can be a delay of up to 2 minutes and 30 seconds between an audit record being buffered to disk and the custom auditing hook being called.

Note that it is possible to consume the cached audit files via an entirely different application. If you are operating a very busy Gateway system and cannot spare the bandwidth to also process audit data, you can grab the cache files directly (the filename is exposed in the method arguments) and forward them to another location.

(Contrived) example:

 

using System; using System.Linq; using System.Text; using System.IO; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Connexion.Core; using Connexion.Share.Threading; namespace Connexion.Device { public class CustomAuditing : BaseCustomAuditing { /// <summary> /// This class allows you to provide custom workflow for your audit data. For example, you may wish to save your audit data into specific files, or, send your audit data to a /// service over the network. Use the Start() and Stop() methods to initialize and tear down class-level variables. Use the ProcessAuditRecordsAsync method to process non-phi /// audit data, and the ProcessPhiAuditRecordAsync method to process phi data. /// *** /// *** The ProcessAuditRecordsAsync and ProcessPhiAuditRecordAsync methods run in separate threads, which means you *must synchronize* these methods if you are writing to a /// *** single non-thread-safe resource (such as a file). Use of lock(...) is acceptable. /// *** /// If you only wish to copy the raw audit data to another location (and skip processing), you can use the reader.FilePath property. In this case you would need to deploy your /// own audit parsing and processing application. /// *** /// The below sample code is a non-production sample. /// </summary> private StreamWriter m_Writer; private readonly AsyncLockEx m_AsyncLock = new AsyncLockEx(); public override void Start() { // initialize any class-level resources } public override void Stop() { // tear-down any class-level resources CloseStream(); } private void CloseStream() { var temp = m_Writer; m_Writer = null; temp?.BaseStream?.Close(); } private void CreateStream() { if(m_Writer?.BaseStream?.Length < 10 * 1024 * 1024) return; CloseStream(); var targetPath = "c:\\temp\\audit\\"; Directory.CreateDirectory(targetPath); var filePath = $"{targetPath}audit_{DateTime.Now.ToString("yyyyMMddHHmmss")}.txt"; var stream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite); m_Writer = new StreamWriter(stream, Encoding.UTF8, 8 * 1024, false); } public override async Task ProcessAuditRecordAsync(IAuditReader<AuditRecord> reader, CancellationToken cancellationToken) { // Non-PHI data (your logic here, do something better than below) foreach(var batch in reader.EnumerateBatches(100)) { var builder = new StringBuilder(); foreach(var record in batch) { builder.AppendLine(record.ToString()); } await WriteToAuditRecord(builder.ToString()); } } public override async Task ProcessPhiAuditRecordAsync(IAuditReader<PhiAuditRecord> reader, CancellationToken cancellationToken) { // Phi data (your logic here, do something better than below) foreach(var batch in reader.EnumerateBatches(100)) { var builder = new StringBuilder(); foreach(var record in batch) { builder.Append(record.ToStringWithPhi()); } await WriteToAuditRecord(builder.ToString()); } } private async Task WriteToAuditRecord(string toWrite) { using(await m_AsyncLock.LockAsync()) { CreateStream(); await m_Writer.WriteAsync(toWrite); await m_Writer.FlushAsync(); } } } }

Device/Plugin developers can write audit events from within their own code by grabbing a handle to an AuditProvider.

var auditProvider = ServiceProvider.GetInstance<IAuditProvider>(); if(auditProvider != null) auditProvider.AuditNonPhi("UserDeletedTemporaryCache", $"The user cleared the temporary cache.");