Tamper-Evident Audit Logging for Regulated Environments
Every analytical action taken in a consequential R environment should be documented — who did it, what they did, when, and why. In practice, almost none of it is.
regulog fills that gap. It records every action, change, note, and decision into a tamper-evident, hash-chained audit trail stored as newline-delimited JSON. Every entry is attributed to a named user, time-stamped in UTC, and linked to the previous entry via SHA-256 — so any modification after the fact, however subtle, is detectable by verify_log().
Works for regulated pharmaceutical environments (21 CFR Part 11, EU Annex 11), internal data pipelines, multi-user Shiny applications, and any context where accountability and traceability matter.
Installation
# Install from GitHub
pak::pak("repro-stats/regulog")Quick start
library(regulog)
# Initialise a session
log <- regulog_init(
app = "primary-analysis",
version = "1.0.0",
user = "analyst",
path = "logs/audit.rlog"
)
# Log actions, changes, and decisions
log_action(log,
action = "data_read",
object = "adsl.sas7bdat",
reason = "Reading ADSL for primary efficacy analysis"
)
log_change(log,
object = "alpha",
field = "value",
before = "0.05",
after = "0.025",
reason = "Updated per protocol amendment 2"
)
log_note(log,
"Outlier in subject 01-042 retained per SAP section 8.3 —
discussed with medical monitor 2026-06-20"
)
# Log data reads, scoped to a block — read() is logged automatically
with_log(log, {
adsl <- read(haven::read_sas, "data/adsl.sas7bdat")
adae <- read(haven::read_sas, "data/adae.sas7bdat")
})
# Apply an electronic signature
log_signature(log,
"I certify this analysis is accurate and complete per SAP version 2.0")
# Verify tamper integrity
verify_log(log)
#> regulog: Log intact: 5 entries, chain unbroken
# Query the log
filter_log(log, type = "SIGNATURE")
filter_log(log, action = "data_read", from = "2026-06-01")
# Export for submission
export_audit_trail(log, format = "csv", signed = TRUE,
path = "outputs/audit_trail.csv")Key functions
| Function | Purpose |
|---|---|
regulog_init() |
Initialise an audit logging session |
log_action() |
Log a discrete action |
log_change() |
Log a before/after field change |
log_note() |
Log a free-text annotation or analytical decision |
log_signature() |
Apply an electronic signature |
rl_read() |
Explicit, logged read of any data source |
with_log() |
Scoped convenience: read() calls inside the block log automatically |
verify_log() |
Verify SHA-256 hash chain integrity |
filter_log() |
Query entries by type, user, action, or date |
export_audit_trail() |
Export to CSV or JSON, with optional signing |
regulog_shiny_init() |
Initialise inside a Shiny server function |
regulog_observer() |
Auto-log Shiny reactive input events |
The hash chain
Each entry hash is SHA-256 of all entry fields plus the prior hash:
h_0 = SHA256("GENESIS" | app | version | timestamp)
h_n = SHA256(entry_id | timestamp | app | version | user | type |
<payload fields> | h_{n-1})
Any modification to any field in any entry breaks the chain from that point forward. verify_log() recomputes every hash and reports the first broken link — and works offline from the .rlog file, without an active R session.
Entry types
| Type | Created by | Purpose |
|---|---|---|
ACTION |
log_action() |
Discrete events: reads, runs, approvals |
CHANGE |
log_change() |
Before/after field modifications |
NOTE |
log_note() |
Decisions and free-text rationale |
SIGNATURE |
log_signature() |
Named, dated, meaningful sign-off |
Validation
Deploying any software in a regulated environment requires documented evidence that it is installed correctly, operates as specified, and performs reliably under real-world conditions. This is the IQ/OQ/PQ qualification process required under 21 CFR Part 11, EU Annex 11, and GAMP 5 before a tool can be used in GxP workflows.
regulog ships pre-written, executable qualification protocols. Instead of authoring validation documents from scratch — a process that typically takes weeks of internal effort — your team runs three commands and receives a complete, signed qualification record:
source(system.file("validation/IQ_regulog.R", package = "regulog"))
source(system.file("validation/OQ_regulog.R", package = "regulog"))
source(system.file("validation/PQ_regulog.R", package = "regulog"))Each protocol is self-contained and produces a pass/fail summary against explicit acceptance criteria:
| Protocol | What it covers | Tests |
|---|---|---|
| IQ — Installation Qualification | R version, package installation, dependency integrity, file system access | 10 |
| OQ — Operational Qualification | All 21 CFR §11.10 requirements: hash chain, tamper detection, user attribution, timestamps, export, signatures | 26 |
| PQ — Performance Qualification | End-to-end clinical workflows: data review, regulatory export, multi-user sessions, 500-entry load test, inspector query simulation | 7 |
Protocols are version-controlled alongside the package and updated with every release that affects qualified behaviour.
The qualification record produced by each run — including the platform, R version, date, and pass/fail status — can be retained as documented evidence of system qualification in your validated environment.
Regulatory coverage
| Regulation | Clause | Coverage |
|---|---|---|
| 21 CFR Part 11 | §11.10(e) | Hash-chained, time-stamped, user-attributed entries |
| 21 CFR Part 11 | §11.10(b) |
export_audit_trail() CSV and JSON |
| 21 CFR Part 11 | §11.100 |
log_signature() signer identity |
| 21 CFR Part 11 | §11.200 | Signature components: identity, timestamp, meaning |
| EU Annex 11 | Clause 9 | Date, time, user, action on every entry |
| EU Annex 11 | Clause 11 |
verify_log() periodic integrity evaluation |
