• GRC Engineer
  • Posts
  • ⚙️ Your GRC Program Has No Garbage Collector. So It Leaks.

⚙️ Your GRC Program Has No Garbage Collector. So It Leaks.

A control that passes perfectly, while the risk it was built for quietly died. Your dashboard can't tell it apart from one that still matters.

When did you last retire a control, and mean it?

Not retire it on a slide. Not tag it "legacy" and keep collecting evidence anyway. Actually stand it down: stop testing it, stop monitoring it, stop paying for it with audit hours.

Most teams can't remember. They've spent five years adding controls. Every framework, every audit finding, every "let's add a control for that" in a meeting. And they've stood down close to none. The inventory only grows. Controls check in. They rarely check out.

Even the mature programs that run an annual control rationalisation aren't really collecting garbage. They run a manual sweep once a year, by hand, over the controls someone remembered to look at. A sweep like that catches whatever happens to land in front of a human. Everything else keeps running.

Every other system in your building automated this decades ago. GRC engineering never did. And the clutter is the least of it. You keep reading assurance off controls that died years ago and never told you.

The leak

In old C, you managed memory by hand. You asked for it, you gave it back:

control* c = malloc(sizeof(*c));   // every audit finding, every meeting
// ... five years pass ...
free(c);                           // the line nobody ever writes

Forget to free and unreferenced memory piles up until the program bloats and falls over. A memory leak. Modern languages (Go, Java, C#) fixed this with a collector that runs in the background and reclaims whatever the program can no longer reach. Your control inventory has no collector. It only ever calls malloc.

The mechanic that matters is reachability. A collector defines garbage as unreachable from a live root. In a program, the roots are whatever's actually running. In GRC engineering, a control's roots are the live reasons it should exist:

- a real risk it reduces (the root you should already be building from, per Building for Auditors or Attackers?,

- an active regulatory obligation that mandates it,

- a binding contractual or board commitment that requires it.

Trace a control back to any one of those and it's alive. Trace it back to none, and it's garbage. "ISO used to ask for it." "We've always had it." "Some project needed it in 2023." It has been garbage for years. You just never ran the collector.

How a live control rots into garbage

A control almost never dies on purpose. It gets orphaned by an event somewhere else, and keeps running as if nothing happened.

  • The risk went away. You decommissioned the product, deleted the dataset, offboarded the third party. The risk is gone. The control runs on.

  • The obligation moved. A framework version bumped and the clause was dropped, but the control mapped to it stayed. (The framework mapping trap, one step later in its life.)

  • The target disappeared. The system it watched was migrated or shut down. In software, a reference to a torn-down object is a dangling reference: the referent is gone, the pointer lives on. In GRC, it's a control still "monitoring" a server you switched off in Q1.

  • The owner reorg'd away. Nobody left understands why it exists, so nobody dares touch it.

Trigger

What actually died

What your program still shows

Product / data decommissioned

the risk

control active, evidence still collected

Framework version bump

the obligation

control mapped to a deleted clause

System migrated or shut down

the target

control monitoring a host that's gone

Reorg

the owner / context

"we keep it, nobody knows why"

None of these fire an alert. Staleness is silent. And that silence comes at two volumes, which matters more than it looks.

Some of it is loud if you go looking. A control pointed at a dead host errors, and one query (status = ERROR, target last seen 90+ days ago) finds every one. That's the easy 20%.

The dangerous kind is the control that still passes, perfectly. Live target, clean evidence, green check, all built for a risk that died last year. Nothing is broken. Nothing errors. There is no query for "this control works and means nothing," because by every signal your tooling collects, it is identical to a control that matters.

Why the leak never fixes itself

Three structural reasons, and none of them are laziness.

1. The grammar is additive. Every finding, every framework, every meeting adds a control. Nothing subtracts. GRC has malloc and no free.

2. Standing a control down is political; adding one is safe. Adding looks diligent and risks nothing. Retiring means putting your name on "we no longer need this." If you're wrong, it's your head on the finding. So everyone adds and nobody retires.

3. You're tracing the wrong references. This is the mechanical one, and it's why "is anyone still using this?" never shrinks your inventory.

Ask "is anyone still using this control?" and you instinctively check its output: does a report cite it, does another control consume its evidence, does a dashboard render it? Usually something does. So it stays.

That question never asks the only one that counts: is the root still alive? A control whose risk died in 2023 can still feed a report that's read every Monday. Its output is in use. Its reason is dead. Counting usage keeps it immortal. Only tracing from live roots collects it.

It gets worse in pairs. A "quarterly review of the SFTP allowlist" justified by "it supports the nightly reconciliation control," and that reconciliation justified by "it feeds the SFTP job." The batch job was killed two years ago. Each cites the other, neither cites a live root, and "is anyone using this?" keeps both forever. Rare, but real, and invisible to every check except tracing from the roots.

What it costs to keep monitoring the dead

A stale control you still monitor is not neutral dead weight. The compute is cheap, since the check already runs. The cost is everything downstream of the green it emits.

Cost

What monitoring a dead control actually does to you

Signal dilution

Real failures hide among controls that can't meaningfully fail. 4,000 report, six matter, the six drown. (The whole point of Signal vs. Noise, one layer down.)

Counterfeit assurance

If your board or auditor reads program health off an aggregate (a coverage score, a "94% green"), a stale-green control is a counterfeit dollar in the till. Cheap to run, expensive to trust.

Audit surface

Every control left in scope is one an auditor can test, and a place to earn a finding on something that reduces no risk. You manufacture exposure for zero return.

Toil and accountability

The CPU is free. Being answerable for every line in the register is not. Each one is a thing someone has to explain, defend, and own.

The sharpest version is what the dashboard does to an honest signal:

check(control_4471)
  → target:  prod-db-legacy   (offline since March)
  → result:  ERROR — target unreachable (0 of 0 evaluated)
  → board:   🟢 green          # ERROR, N/A and PASS all render the same colour

The control did the right thing. It errored. Your board rolled it up green anyway, because most dashboards have one colour for passed, errored, and not applicable. And that's still the easy case. The dangerous one never errors at all.


The hard part isn't deletion

Deciding what to do with a dead control is the trivial part. Keep it honest, stand it down, or fold it into another. Control triage already tells you how to spend effort once you've found it. And retiring a control doesn't mean erasing it. In a regulated program it's a dated, signed, evidence-retained tombstone. The record of why you stood it down is itself audit evidence. free() with a paper trail.

So the disposition is mechanical. What stops you is two things the tooling won't do for you.

It won't see the rootless-but-passing control, because that control looks identical to one that works. And it won't carry the weight of the call. But "the trace shows no live root reaches this" is a far easier thing to sign than "I, personally, think we don't need this." Root-traced evidence turns a career-risky judgment into a finding of fact.

GRC is the only running system in your building that monitors memory it freed long ago and trusts the readings.

And there's a deeper leak underneath this one. Tracing only works if your roots are real, and a risk register accumulates dead risks exactly the way your inventory accumulates dead controls. Garbage-collecting controls forces you to garbage-collect risks first. (More on that soon.)

For now, start by admitting you have a leak.

Did you enjoy this week's entry?

Login or Subscribe to participate in polls.

That’s all for this week’s issue, folks!

If you enjoyed it, you might also enjoy:

See you next week!

Reply

or to participate.