[{"data":1,"prerenderedAt":358},["ShallowReactive",2],{"doc-\u002Fsecurity\u002Faudit-trail":3},{"id":4,"title":5,"body":6,"description":348,"edit":349,"extension":350,"meta":351,"navigation":352,"path":353,"seo":354,"stem":355,"vertical":349,"weight":356,"__hash__":357},"content\u002Fsecurity\u002Faudit-trail.md","Audit trail",{"type":7,"value":8,"toc":338},"minimark",[9,18,23,26,53,68,72,83,174,177,181,199,214,220,224,251,259,262,282,286,289,309,312,316,334],[10,11,12,13,17],"p",{},"For HACCP and Legionella the audit trail is the ",[14,15,16],"strong",{},"product",". The PDF\nreport is the deliverable; the audit trail is the thing an inspector\ntrusts that the deliverable is not faked. This page describes how the\ntrail is constructed, what is guaranteed, and what is not.",[19,20,22],"h2",{"id":21},"what-is-recorded","What is recorded",[10,24,25],{},"The audit trail records three kinds of events:",[27,28,29,36,47],"ol",{},[30,31,32,35],"li",{},[14,33,34],{},"Configuration changes"," — every CRUD on sites, devices, sensors,\nrules, channel thresholds, recipient lists, integrations.",[30,37,38,41,42,46],{},[14,39,40],{},"Alarm lifecycle"," — ",[43,44,45],"code",{},"armed → grace → alarm → acknowledged → cleared",".\nIncludes the acknowledger's identity and free-text note.",[30,48,49,52],{},[14,50,51],{},"Access events"," — logins, magic-link issuance, support-team\naccess invocations, API token rotations.",[10,54,55,56,59,60,63,64,67],{},"Each event is a row in an append-only table. Rows are never ",[43,57,58],{},"UPDATE","d\nor ",[43,61,62],{},"DELETE","d; corrections are new rows with ",[43,65,66],{},"correction_of = \u003Cid>",". A\ncorrected row is rendered with a strike-through in the dashboard and\nthe PDF; the correction row appears immediately below.",[19,69,71],{"id":70},"measurement-audit","Measurement audit",[10,73,74,75,78,79,82],{},"Measurements themselves are also append-only. A measurement that\nturned out to be wrong (sensor fault, calibration issue) is not\ndeleted — it is ",[14,76,77],{},"annotated"," with a ",[43,80,81],{},"note"," row that an operator\nattaches:",[84,85,90],"pre",{"className":86,"code":87,"language":88,"meta":89,"style":89},"language-json shiki shiki-themes github-dark github-dark","{\n  \"channel_id\": \"chn_4f3c1a\",\n  \"ts\": \"2026-05-15T03:11:00Z\",\n  \"note\": \"Reading erroneous — probe was dislodged during cleaning. Discarded for compliance reasons; physical re-mount documented.\",\n  \"by\": \"ops@cafe-bratislava.sk\",\n  \"at\": \"2026-05-15T08:40:00Z\"\n}\n","json","",[43,91,92,101,118,131,144,157,168],{"__ignoreMap":89},[93,94,97],"span",{"class":95,"line":96},"line",1,[93,98,100],{"class":99},"suv1-","{\n",[93,102,104,108,111,115],{"class":95,"line":103},2,[93,105,107],{"class":106},"s8ozJ","  \"channel_id\"",[93,109,110],{"class":99},": ",[93,112,114],{"class":113},"s4wv1","\"chn_4f3c1a\"",[93,116,117],{"class":99},",\n",[93,119,121,124,126,129],{"class":95,"line":120},3,[93,122,123],{"class":106},"  \"ts\"",[93,125,110],{"class":99},[93,127,128],{"class":113},"\"2026-05-15T03:11:00Z\"",[93,130,117],{"class":99},[93,132,134,137,139,142],{"class":95,"line":133},4,[93,135,136],{"class":106},"  \"note\"",[93,138,110],{"class":99},[93,140,141],{"class":113},"\"Reading erroneous — probe was dislodged during cleaning. Discarded for compliance reasons; physical re-mount documented.\"",[93,143,117],{"class":99},[93,145,147,150,152,155],{"class":95,"line":146},5,[93,148,149],{"class":106},"  \"by\"",[93,151,110],{"class":99},[93,153,154],{"class":113},"\"ops@cafe-bratislava.sk\"",[93,156,117],{"class":99},[93,158,160,163,165],{"class":95,"line":159},6,[93,161,162],{"class":106},"  \"at\"",[93,164,110],{"class":99},[93,166,167],{"class":113},"\"2026-05-15T08:40:00Z\"\n",[93,169,171],{"class":95,"line":170},7,[93,172,173],{"class":99},"}\n",[10,175,176],{},"The PDF report renders the original reading with a footnote pointing\nto the annotation. The auditor sees both. We never edit history\nsilently — that is the entire point.",[19,178,180],{"id":179},"hash-chain","Hash chain",[10,182,183,184,187,188,191,192,194,195,198],{},"Each audit row carries a ",[43,185,186],{},"prev_hash"," and a ",[43,189,190],{},"hash",". ",[43,193,190],{}," is\n",[43,196,197],{},"SHA-256(prev_hash || canonical_json(row_payload))",". The chain is\nper-account. Any tampering with a past row invalidates every hash\nafter it.",[10,200,201,202,205,206,209,210,213],{},"The current head hash is signed nightly with a key whose ",[14,203,204],{},"public\nhalf"," is published at\n",[43,207,208],{},"https:\u002F\u002Fopensense.murzin.digital\u002Flegal\u002Faudit-pubkey.pem",". The signed\nhead is committed to a public log\n(",[43,211,212],{},"https:\u002F\u002Fopensense.murzin.digital\u002Flegal\u002Faudit-heads.txt",") along with\nthe day's date. The combination — publication + signature with a\npublished key — gives any auditor an external check on whether we\nsilently rewrote your history.",[215,216],"stoplight",{"alarm":217,"ok":218,"warn":219},"Mismatch + we can't explain it. Tell the inspector. This is on us.","Head hash today matches the row chain you can reproduce locally","Mismatch — open a ticket",[19,221,223],{"id":222},"what-we-can-prove","What we can prove",[225,226,227,233,239,245],"ul",{},[30,228,229,232],{},[14,230,231],{},"No reordering of past rows"," — the chain forbids it.",[30,234,235,238],{},[14,236,237],{},"No silent edits"," — corrections are new rows.",[30,240,241,244],{},[14,242,243],{},"No silent deletions"," — corrections of corrections still leave the\noriginal visible.",[30,246,247,250],{},[14,248,249],{},"Approximate clock honesty"," — server timestamps are NTP-synced to\na publicly verifiable pool. Drift > 50 ms triggers an internal alarm.",[19,252,254,255,258],{"id":253},"what-we-can-not-prove","What we can ",[14,256,257],{},"not"," prove",[10,260,261],{},"We are honest about the limits.",[225,263,264,270,276],{},[30,265,266,269],{},[14,267,268],{},"Pre-ingest data integrity."," OpenSense cannot tell whether a\nreading is the real fridge temperature or whether someone aimed a\nhair dryer at the sensor. Probe placement and physical security are\nthe customer's problem.",[30,271,272,275],{},[14,273,274],{},"Real-time append."," If an attacker drops the network for an hour\nand re-establishes it, our audit trail captures the gap honestly but\ncannot fill in what happened during it.",[30,277,278,281],{},[14,279,280],{},"Cryptographic non-repudiation"," of acknowledgements. The\n\"acknowledged by\" stamp is identity-asserted by our session, not by\nthe operator signing the acknowledgement with their own key. This is\ngood enough for HACCP and Legionella as practised; not good enough\nfor pharmaceutical GMP. For pharma, see the Enterprise tier roadmap.",[19,283,285],{"id":284},"how-an-inspector-verifies","How an inspector verifies",[10,287,288],{},"Three options, in increasing rigour:",[27,290,291,297,303],{},[30,292,293,296],{},[14,294,295],{},"The PDF, by itself."," Most common. The PDF cites the audit trail\nfor each event. Inspector trusts the PDF.",[30,298,299,302],{},[14,300,301],{},"The dashboard, on-site."," Open the audit log in the dashboard;\ninspector walks the chronology. Useful when the inspector is\nsceptical of the PDF.",[30,304,305,308],{},[14,306,307],{},"The hash-chain receipt."," Download the JSON-LD export. Inspector\n(or their auditor) verifies the chain locally against the daily\npublished head. Useful for high-stakes events.",[10,310,311],{},"We have not yet had option 3 invoked in a Slovak ŠVPS inspection. It\nexists because for legionella in care homes the question can be raised,\nand we want the answer to be \"yes\" not \"uh, let me ask\".",[19,313,315],{"id":314},"sources","Sources",[225,317,318,327],{},[30,319,320],{},[321,322,326],"a",{"href":323,"rel":324},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc6962",[325],"nofollow","RFC 6962 — Certificate Transparency (analogous hash-chain design)",[30,328,329],{},[321,330,333],{"href":331,"rel":332},"https:\u002F\u002Fgithub.com\u002Fgoogle\u002Ftrillian",[325],"Trillian — append-only log infrastructure (background reading)",[335,336,337],"style",{},"html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":89,"searchDepth":120,"depth":120,"links":339},[340,341,342,343,344,346,347],{"id":21,"depth":103,"text":22},{"id":70,"depth":103,"text":71},{"id":179,"depth":103,"text":180},{"id":222,"depth":103,"text":223},{"id":253,"depth":103,"text":345},"What we can not prove",{"id":284,"depth":103,"text":285},{"id":314,"depth":103,"text":315},"Append-only, hash-chained evidence",null,"md",{},true,"\u002Fsecurity\u002Faudit-trail",{"title":5,"description":348},"security\u002Faudit-trail",530,"wpuRRpHRuYcfedIAc9ORJndnZnPtNqxzYG0hr2QuZjc",1779022956075]