Auditing & Data History

Auditing

To comply with data protection regulation, we need to make sure that SORMAS provides an audit log trail which can be easily ingested by dedicated log processing systems and allows investigation by officials.

Use cases:

  • User opens case in UI -> call to backend method CaseFacade.getCaseDataByUuid needs to be logged

  • User edits/deletes case in UI -> call to backend method CaseFacade.save/deleteCase needs to be logged

  • User does export -> call to CaseFacade.getExportList needs to be logged

  • User opens case directory -> call to CaseFacade.getIndexList needs to be logged

High-Level Explanation

The audit trail gets populated by automatically logging every invocation of a facade/EJB method. By this, we can trace every interaction with the system (i.e., via Vaadin UI or REST). We output the collected logs to a user configurable log sink such that the logs can be easily ingested for further processing.

The most important module that is covered is the SORMAS backend.

Related epic: https://github.com/hzi-braunschweig/SORMAS-Project/issues/7904

Setup

Audit logging can be set up by setting the audit.logger.config property in the sormas.properties file to a path that points to a logback configuration file.

There is an example file in SORMAS source code in the sormas-base/setup folder. This writes audit logs to a file but Logback can be easily configured to write to other destinations like a database or a log processing system.

For example adding a Loki4jAppender appender to the logback configuration file will send the logs to a Loki instance.

e.g. The following configuration will send the logs to a Loki instance running on localhost:

<configuration> <appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender"> <http> <url>http://localhost:3100/loki/api/v1/push</url> </http> <format> <label> <pattern>app=my-app,host=localhost,level=%level</pattern> </label> <message> <pattern>l=%level h=localhost c=%logger{20} t=%thread | %msg %ex</pattern> </message> <sortByTime>true</sortByTime> </format> </appender> <root level="DEBUG"> <appender-ref ref="LOKI"/> </root> </configuration>

NOTE: in order to make the Loki appender work, you need to add the latest loki-logback-appender.jar as a dependency in the libs folder of the SORMAS domain in your payara

Log Format

For the general purpose of audit logging SORMAS logs access to the SORMAS backend with details on what exactly has been accessed, without including sensitive data (e.g. names).

For the logging output format, SORMAS is using the FHIR R4 AuditEvent resource, which is based on the IHE ATNA audit profile. FHIR is a well-known and established standard for exchange of medical related data.

A FHIR AuditEvent resource has a specified JSON representation which is compact and easy to digest by log processing systems like Loki or ELK.

These are the most important fields logged in the AuditEvent class:

Content

Description

Field in AuditEvent resource

Content

Description

Field in AuditEvent resource

Access type

Create/Read/Update/Delete/Execute

action

timestamp

Timestamp of the event

recorded

actor

Functional instance causing the event (e.g., user). Users should be identified with a human readable name besides using the UUID

agent

Executing instance

Identifier of the system(component) generating the audit event

source.site

Activity/Event

Description of the activity with unique reference(uuid) w.r.t. the accesses data

entitiy

Additionally, there is the field type, which will be populated according to the following table.

Code

System

Name

Description

Code

System

Name

Description

110100

http://dicom.nema.org/resources/ontology/DCM

Application Activity

Start, Stop

110106

http://dicom.nema.org/resources/ontology/DCM

Export

Database level export

110112

http://dicom.nema.org/resources/ontology/DCM

Query

A request for multiple entities

110110

http://dicom.nema.org/resources/ontology/DCM

Patient Record

Use for read/write/delete operations on patient related entities (case, contact, symptoms, samples, ...)

object

http://terminology.hl7.org/CodeSystem/audit-event-type

An Operation on other Objects

Use for read/write/delete operations on all other entities, most importantly users, infrastructure, configuration and tasks

Logged events

SORMAS logs the following events:

  • Application lifecycle events (start, stop)

  • Data reads

  • Creation and change of data

  • Deletion of data

  • REST api calls

  • Failed login attempts

External message adapters can also log events using the AuditLoggerFacade

Logged data

  • Under all circumstances the principle of data minimization is followed

  • Only the UUID of entities is logged, not the full entity

  • The timestamps are in ISO format.

In general the log contains only pseudonymized personal data, the only exception being the name of the active user.


Log examples

  • Failed login

    { "resourceType": "AuditEvent", "type": { "system": "https://hl7.org/fhir/R4/valueset-audit-event-type.html", "code": "110114", "display": "User Authentication" }, "subtype": [ { "system": "https://hl7.org/fhir/R4/valueset-audit-event-sub-type.html", "code": "110122", "display": "Login" } ], "action": "E", "recorded": "2024-03-07T12:38:17.803+02:00", "outcome": "4", "outcomeDesc": "Authentication failed", "agent": [ { "name": "Username cannot be determined without ID token. Check Keycloak logs for details." } ], "source": { "site": "sormas.lu - UI MultiAuthenticationMechanism" }, "entity": [ { "what": { "reference": "sormas-ui/Callback" } } ] }
  • Load case directory

    { "resourceType": "AuditEvent", "action": "R", "period": { "start": "2024-03-07T12:38:34+02:00", "end": "2024-03-07T12:38:34+02:00" }, "recorded": "2024-03-07T12:38:34.912+02:00", "outcomeDesc": "[CaseIndexDto(uuid=SYQMFJ-N2YXQ7-PMHXUE-SAQU2FIE),CaseIndexDto(uuid=RAG4WE-2BUPEN-3OXQW5-TB7OKE4Y),CaseIndexDto(uuid=WILIOE-FFX3L7-C46FKD-PCN6CJQU),CaseIndexDto(uuid=SCYB5H-MLZJJB-PFAVRD-6IQQ2FMU),CaseIndexDto(uuid=S7DQVF-4XCHEO-YA57KV-HHAHKF5Q),CaseIndexDto(uuid=XFLEBJ-A5FAY7-DGVLX7-XZ4VKP6U),CaseIndexDto(uuid=TUWWHZ-E5LQPN-GCVYDQ-VYTVKMAY),CaseIndexDto(uuid=RH7FHT-BMD6BV-T7V7VY-LIC4CHCM),CaseIndexDto(uuid=Q2YYLE-TXYILI-X5Q7FE-BJHL2IG4),CaseIndexDto(uuid=SF2Y72-YYG335-XI3ZFX-VTMBSMT4),CaseIndexDto(uuid=VORYUS-BB3TN5-EWQ6CB-TLM7KI4E),CaseIndexDto(uuid=UI47KF-UCZMA4-DHEHXU-MLMKKE5A),CaseIndexDto(uuid=TCO4DA-2GI64Y-D7IK2G-KBKACNXM),CaseIndexDto(uuid=T62OHY-7ID2Z2-LTVWSR-VCZVKMQE),CaseIndexDto(uuid=VCGLLU-REDG56-BVEULC-WZKASOOU),CaseIndexDto(uuid=T4ZZBB-2UHGS2-2TLMTH-26FOSBPQ),CaseIndexDto(uuid=SAGWGH-JS2LQA-WOV3F5-VAQICK5Y),CaseIndexDto(uuid=VZ4SMM-OBWY72-7DVLC4-6FZL2LFY),CaseIndexDto(uuid=V3ND6S-66VQIV-S642FO-5F42KIQ4),CaseIndexDto(uuid=VQFIG5-QDTIK7-UC2UAX-KD52CAHI),CaseIndexDto(uuid=QYL2XE-KU6BKX-CO7JXV-35PN2F4E),CaseIndexDto(uuid=VT3ADI-TYG6PJ-YYOHCT-UVWUKMPQ),CaseIndexDto(uuid=RVXCET-Y35DWT-3DYA34-4MXX2HLI),CaseIndexDto(uuid=RKU56K-HGXFNO-AFXOCX-YWH5KJZQ),CaseIndexDto(uuid=XABT2R-MVHQA4-PN3BNS-DWKKCCY4),CaseIndexDto(uuid=TDPU3J-ZSKZBD-BLRHEV-7PC2KELY),CaseIndexDto(uuid=X2TFPZ-BULBSB-7NVZIP-4K3D2ICA),CaseIndexDto(uuid=WKWEG3-EGYL2J-GKUHDC-F22CSBRU),CaseIndexDto(uuid=TOAQVR-IU3CKF-LXYJKL-IT6VKKOI),CaseIndexDto(uuid=QR2PLT-JVRN5H-BIMWJO-H3MJKAN4),CaseIndexDto(uuid=QS3UDV-QFVBMM-LGDB2F-LNCLSIRI),CaseIndexDto(uuid=WLJVI5-GNOLVN-D335JX-Q3HBKE7I),CaseIndexDto(uuid=UJLPOL-LSJ7ZS-MUWCCO-3FGQCICI),CaseIndexDto(uuid=XMXHHD-NE526P-EMUVCU-NGPDCFQ4),CaseIndexDto(uuid=UNPGCP-7HRLLF-JCRCZ6-HMAG2E6E),CaseIndexDto(uuid=Q44DQL-ECYSWP-XHRB5G-FNHXKEOA),CaseIndexDto(uuid=XZN26C-NZJXPS-3BLLPD-GDSD2GIQ),CaseIndexDto(uuid=RPZS4S-MIWQY6-7HZ6A5-SG7T2NRY),CaseIndexDto(uuid=RGJQ3S-2OBPHZ-AJY7CM-C6M3CNWU),CaseIndexDto(uuid=RXZ3FB-XNNB2M-TJZX2I-4C3EKAXI),CaseIndexDto(uuid=STPE5C-JV62ST-P7NTB4-5JMZCOQE),CaseIndexDto(uuid=WTP4W2-3DP76T-N4E5EV-YZESSKXM),CaseIndexDto(uuid=QUNZWX-AAKOXA-PEHSCB-DU6HKHSI),CaseIndexDto(uuid=WJOFHO-2X5NIA-VKPJGT-FON52IMU),CaseIndexDto(uuid=QGHGY4-B3RUOC-TX27YD-JUSQ2DPI),CaseIndexDto(uuid=RT4XRJ-VM74IK-5TYURK-B5NGKO2Y),CaseIndexDto(uuid=V6XOJS-SX4ESD-XQQSCS-JJXOCBQA),CaseIndexDto(uuid=RPVLIR-R7YK3E-QAXKU3-QVRFKLZA),CaseIndexDto(uuid=XSQTVG-4IXW7A-LAHYTW-2D5CKMDY),CaseIndexDto(uuid=W7UK6H-KZSPPD-PGHWIO-JI4MCCGU),CaseIndexDto(uuid=UMVX63-P6R4I3-SI7IK5-ZNEMSLC4),CaseIndexDto(uuid=VZKFEJ-Q5V2XU-5SQPRM-ZZACSDMQ),CaseIndexDto(uuid=RGH5HS-A3TSNU-5LZXIR-OOCICME4),CaseIndexDto(uuid=SYXQBI-UOXGW7-KPNDZU-ZCJNSDYU),CaseIndexDto(uuid=TJL5UI-QFCEOX-4NA4WA-UTVRCPJQ),CaseIndexDto(uuid=RDEGZP-OEFQZK-E3ZC5C-MYG2SHWY),CaseIndexDto(uuid=Q4ICNF-Y5R56L-NPSAXN-O52WCOUU),CaseIndexDto(uuid=USASG5-3HG5QV-GUZBS6-VKLOKF3A),CaseIndexDto(uuid=SRYNG6-7BL73Q-LAC6TK-V2H62NKI),CaseIndexDto(uuid=V6WVV4-MFF6G7-CBTDMQ-VNX7KG34),CaseIndexDto(uuid=UEQ4TR-QXWOXK-IERBBB-LE5SSNEA),CaseIndexDto(uuid=U2W5EI-34BFKM-FMCGON-4COT2F3M),CaseIndexDto(uuid=UFKBX6-WLAQRE-DOJNWN-2AFWSEYA),CaseIndexDto(uuid=XAOL6R-S7BFCM-6BDOLB-SZOXKGIY),CaseIndexDto(uuid=SFPALU-Z4QORK-ZYPOEK-2QK32DUE),CaseIndexDto(uuid=RBMPYH-VBOOBC-BSEJKD-7KL7CMQQ),CaseIndexDto(uuid=V26N2S-5XKDTB-BMTJBS-HS7S2LAU)]", "agent": [ { "type": { "coding": [ { "system": "https://www.hl7.org/fhir/valueset-participation-role-type.html", "code": "humanuser", "display": "human user" } ] }, "who": { "identifier": { "value": "UOSUJW-BRSDJL-ZGSXEW-3XCUCLHI" } }, "name": "NatUser" } ], "source": { "site": "sormas.lu", "type": [ { "system": "http://terminology.hl7.org/CodeSystem/security-source-type", "code": "4", "display": "Application Server" } ] }, "entity": [ { "what": { "reference": "public java.util.List de.symeda.sormas.backend.caze.CaseFacadeEjb.getIndexList(de.symeda.sormas.api.utils.criteria.BaseCriteria,java.lang.Integer,java.lang.Integer,java.util.List)" }, "detail": [ { "type": "param", "valueString": "CaseCriteria(birthdateDD=null,birthdateMM=null,birthdateYYYY=null,caseClassification=null,caseLike=null,caseOrigin=null,caseUuidsForMerge=null,community=null,creationDateFrom=null,creationDateTo=null,dateFilterOption=By Date,dateTypeCalss=class de.symeda.sormas.api.caze.NewCaseDateType,disease=null,diseaseVariant=null,district=null,eventLike=null,facilityType=null,facilityTypeGroup=null,followUpStatus=null,followUpUntilFrom=null,followUpUntilTo=null,followUpVisitsFrom=null,followUpVisitsInterval=null,followUpVisitsTo=null,healthFacility=null,includeCasesFromOtherJurisdictions=false,investigationStatus=null,jurisdictionType=null,mustBePortHealthCaseWithoutFacility=null,mustHaveCaseManagementData=null,mustHaveNoGeoCoordinates=null,newCaseDateFrom=null,newCaseDateTo=null,newCaseDateType=null,onlyCasesWithDontShareWithExternalSurvTool=null,onlyCasesWithEvents=false,onlyCasesWithReinfection=null,onlyContactsFromOtherInstances=null,onlyEntitiesChangedSinceLastSharedWithExternalSurvTool=null,onlyEntitiesNotSharedWithExternalSurvTool=null,onlyEntitiesSharedWithExternalSurvTool=null,onlyQuarantineHelpNeeded=null,onlyShowCasesWithFulfilledReferenceDefinition=null,outcome=null,person=null,personLike=null,pointOfEntry=null,presentCondition=null,quarantineTo=null,quarantineType=null,region=null,reinfectionStatus=null,relevanceStatus=Active,reportDateTo=null,reportingUserLike=null,reportingUserRole=null,sourceCaseInfoLike=null,surveillanceOfficer=null,symptomJournalStatus=null,vaccinationStatus=null,withExtendedQuarantine=null,withOwnership=true,withReducedQuarantine=null,withoutResponsibleOfficer=null)" }, { "type": "param", "valueString": "0" }, { "type": "param", "valueString": "100" }, { "type": "param", "valueString": "[]" } ] } ] }
  • Load case data

  • Update a case

  • Archive a case

  • Delete a case

Data History

Use case: A user observes incorrect data in a few cases. To understand how exactly this came to be it should be possible to extract the change history of the cases, including what exactly changed, at what point in time and by which user the change was made.

Goals:

  1. Provide the information when and by whom a change was made

  2. Provide what was changed / what the data looked like before and after the change

SORMAS uses temporal tables to provide a history of all data changes. These automatically create a copy of the previous status in a history table each time a database entry is changed and provide it with a validity period. This also makes it possible to query the status of the data at any time in the past with simple SQL queries.