Clinical safety

Does obfuscation break clinical alarm thresholds?

No, not on its own. We protected a patient-monitor alarm module on five presets and every alarm fired identically. Then we renamed one field on the reading rows, and a sustained heart rate of 165 and an oxygen saturation of 84 percent both reported as within limits.

The module, and the baseline

A monitoring dashboard folds a window of samples into one assessable row and decides whether to alarm. Ours has upper and lower limits per vital sign, a debounce so that a four-second artefact does not wake anybody, a suppression list for known technical artefacts such as a lead falling off, and an escalation to the central station for alarms that persist.

The engine is an installed library, copied in unprotected, and the alarm test is a comparison whose true branch raises the alarm. That detail decides everything that follows.

Baseline: five presets, every reading, twenty-five comparisons, all identical. Protection did not change a single alarm.

The reading that stopped existing

The reading value is a field on the row. We renamed it, and the engine could no longer find it. Comparisons against an undefined value are false in both directions, and both of those false results skip the alarm.

tachycardia-five-minutes ALARMING=false escalated=false within-limits(hr undefined in 40-130)

desaturation ALARMING=false escalated=false within-limits(spo2 undefined in 90-100)

Every alarm in the file went silent. Not an error, not a gap in the trace, not a device fault: an affirmative statement that the patient is within limits.

The message even says so. A monitor reporting a value as within limits while printing that value as undefined is the same self-refuting signature this series keeps finding, and it is the fastest triage available if this reaches you as a ticket.

Which way a threshold fails is decided by the branch it guards

It is tempting to reason that a floor and a ceiling fail in opposite directions. They do not, and this file shows why cleanly.

Both limits here are used the same way: the comparison being true raises the alarm. So skipping the comparison skips the alarm, whichever side it is on. Renaming the upper limit silenced the tachycardia alarms and left the desaturation alarm working perfectly. Renaming the lower limit did the reverse:

desaturation ALARMING=false escalated=false within-limits(spo2 84 in undefined-100)

while the tachycardia alarm kept firing and kept reporting correctly.

The two halves guard disjoint situations, so there is no redundancy to notice the loss and no anomaly to detect. Each half simply has its own patient. Two comparisons on one line look like two guards and behave like two independent single points of failure.

One field, one safe half and one open half

The most instructive arm was the duration a breach has persisted for, because two different rules read it.

The debounce compares it against a delay before allowing an alarm. Lose the value and that comparison is false, so the alarm is not suppressed. The four-second artefact now alarms. That is noisier, and noisier is the safe direction.

The escalation compares the same value against a longer threshold before paging the central station. Lose the value and that comparison is false too, so nothing is escalated. The tachycardia and the desaturation both still alarmed at the bedside and neither reached anybody outside the room.

One field, one edit, and the half that fails safe is loudly visible while the half that fails open is not. If you only looked at whether alarms were still firing, everything was fine.

What failed safe, reported honestly

A series that only ever finds silent disasters is confirming rather than measuring, so it is worth saying plainly which arms were harmless.

Renaming the debounce delay made the library substitute its default of zero, and the four-second spike alarmed. Renaming the artefact suppression list, or the code field it matches against, made the lead-off artefact alarm and page the central station. All three are false alarms rather than missed ones.

That is not nothing, because alarm fatigue is a real clinical hazard and a monitor that cries wolf gets ignored. But it is recoverable, it is visible within one shift, and nobody deteriorates unobserved because of it. It is the direction you want a failure to take.

The page that never went out

The escalation route behaved the way a caller-supplied implementation with a builtin behind it always does. Renaming the sink meant the library used its own, which writes to a buffer nobody reads:

paged=(NOBODY)

The alarms still fired. The rows still reported escalated=true. The configuration still described the sink as caller-supplied and paging the central station. That description was accurate: the sink was present, correctly written, and never used.

Renaming the enabling flag in front of escalation, or the threshold it compares against, both reached the same place, and the union was identical to the flag alone.

The most dangerous arm of all was the limit table itself. Renaming it made the library substitute its default, which is no limits at all, and every reading in the file reported not-monitored. A dashboard that is monitoring nothing looks exactly like a ward where everybody is stable.

What to do about it

Scope the rename. Fields on the rows you hand to an installed engine cross a boundary as surely as an option key does, and a pattern written around the names your own code invented will not touch them.

Then think about which direction each comparison fails in, because that is a design decision you can make for free. A comparison whose true branch raises an alarm fails silent when the value it reads goes missing. If you want it to fail loud instead, require the value to be a finite number before comparing it and treat a non-finite reading as a fault worth alarming on. That one line converts every arm on this page from a silent monitor into a device fault, which is a thing wards already know how to respond to.

And be careful with the reasoning that says you would notice. In this file the alarms that stopped were also the alarms that would have told you.

Frequently asked questions

Does obfuscating an alarm module change when it alarms?

Not by itself. We protected the module on five presets covering the ES5 target, the modern target and the string transforms, and every alarm and escalation matched the unprotected run. Twenty-five comparisons, all identical. Alarms moved only when member renaming reached the fields the installed engine reads.

Why does losing a reading silence the alarm rather than trigger it?

Because the alarm is raised when the comparison is true, and a comparison against an undefined value is false in both directions. Skipping a comparison whose true branch is the restrictive one skips the restriction. The engine then reports the patient as within limits, which is an affirmative statement rather than an error.

Do a floor and a ceiling fail in opposite directions?

Not when both are used the same way. Here both limits raise an alarm when true, so losing either one silences the alarms on its own side while the other keeps working. They guard disjoint situations, so neither can cover for the other and neither loss produces an anomaly anybody could detect.

Which arms failed safely?

Three of them. Losing the debounce delay, the artefact suppression list, or the code field it matches against all made the monitor noisier rather than quieter. Those produce false alarms, which matter because alarm fatigue is real, but they are visible within a shift and nobody goes unobserved.

Our dashboard says escalation is configured. Does that mean pages are going out?

Not necessarily. In our measurement the configuration accurately reported a caller-supplied escalation sink while the library was using its own builtin, and the rows still reported themselves as escalated. Confirm that somebody received a page rather than that a sink is configured.

What single change makes this class of failure loud?

Require the value to be a finite number before comparing it, and treat a non-finite reading as a device fault worth alarming on. That converts a silent monitor into a technical alarm, which wards already have a procedure for, and it is worth having regardless of obfuscation because a bad feed produces the same value.

How much harder does obfuscation make monitoring logic to read?

It raises the cost of lifting your alarm logic and thresholds out of a shipped bundle, which is a reasonable goal. It does not place that logic beyond a determined analyst, since the code still runs in the browser, and it is not a substitute for server-side validation of anything clinically consequential.

Related reading