In July, ENISA (the EU Agency for Cybersecurity) published its Secure by Design and Default Playbook: twenty-two playbooks aimed at SMEs building products with digital elements, with an annex mapping each one to the Cyber Resilience Act's essential requirements. The first playbook focused on trust boundaries and threat modelling.

Two things resonated with me.

The first is the distribution. ENISA shipped the playbooks as markdown files in a public GitHub repo. The agency writing the EU's product security guidance chose version-controlled plain text, in a repository, over a portal or a PDF-only release. Whatever you think of the content, the format is a statement about how this material is meant to be used: forked, diffed, wired into delivery.

The second is the shape of what Playbook 01 asks for. Its "minimum evidence" comes down to four things:

  1. One architecture or data-flow diagram with trust boundaries and entry points, stored in the repo or wiki.

  2. A list of the top five to ten threat scenarios, each with a high, medium or low priority and a named owner.

  3. A control mapping for each of those threats: what the control is, where it's enforced, how it's verified.

  4. And CI or test output showing that key negative tests fail closed.

Read it once, and it's documentation. Read it twice, and it's a schema.

A diagram derived from a source of truth. Threats as records with a priority field and an owner field. Controls as records with an enforcement point and a verification reference. Evidence emitted by a pipeline. None of that describes a workshop output. It describes structured data with a build step.

So we built it.

A worked example

SensorHub is a fictional SME fleet-telemetry product: devices reporting over MQTT/TLS to a cloud ingest API, operators watching fleets on a dashboard, admins pushing firmware over the air. It's deliberately shaped to hit the trust boundaries ENISA names in the playbook: device to cloud, internet to back end, tenant to tenant, user to admin.

The entire threat model is one HCL file. The data flow diagram is generated from it, so the picture can't quietly diverge from the model it claims to describe. CI regenerates the diagram on every change and fails if the committed copy is stale.

That covers the first evidence item. The interesting part is the last checkbox.

A checklist that fights back

ENISA wrote Playbook 01's release gate as a six-item checklist, intended for a release review or CI. A checklist you tick by hand has a known failure mode: it gets ticked. At 5pm on a Friday, everything is compliant.

So in the example repo, the release gate is a policy file. Eleven invariants, one per checkbox plus the sub-checks each implies, run by threatcl validate in CI. A high-severity threat with no implemented control is a build failure. A service outside a trust zone is a build failure. A control with no named owner, or no negative test on a privileged path, is a build failure.

$ make gate
Validated 1 threatmodels in 1 files
Checked 11 invariants against 1 threatmodels: 0 errors, 0 warnings, 0 exemptions

Delete the negative-test reference from the RBAC control, exactly the kind of thing that disappears in a rushed refactor, and the pull request goes red:

Invariant violation [error] 'critical_paths_have_negative_tests':
threat 'operator_escalates_to_admin' is high but names no
negative_test. ENISA requires at least one negative test per
critical boundary

ENISA allows documented exceptions, and so does the gate: an exemption has to be written into the policy file with a justification, where a reviewer can see it. The waiver becomes a diff.

Priority, as a computed field

The playbook asks for high, medium or low priority assigned "using a documented method": impact, likelihood, plausibility, exploitability, exposure. Prose in a wiki is not a method. In the model, it's a risk block:

risk {
  likelihood = "medium"
  impact     = "high"
  rationale  = "Operator accounts are numerous and easy to
                phish. Rollout endpoints reach customer
                fleets."
}

Likelihood and impact are ordinal enums, resolved through a severity matrix, so the band is computed the same way for every threat rather than argued case by case. The rationale is where exploitability and exposure get recorded, and it's versioned alongside the rating it justifies. SensorHub carries six threats, inside ENISA's five-to-ten band, and that band is itself checked by a warning-severity invariant.

Because the ratings are structured, "show me every unmitigated high" is a query, not a reading exercise.

The checkbox that decays

The gate's last item is the one that quietly rots: refresh the model on a new interface, an auth change, new sensitive data, a dependency change, an OTA change, an architecture change.

Every one of those triggers is visible in a diff, and there are two directions to cover.

The model changed, and the gate didn't re-run. That direction is deterministic and already handled: the workflow triggers on any PR touching the model or the policy, and the eleven invariants run before the change can merge.

The code changed, and the model didn't. This is the harder and far more common direction. The new endpoint ships, and nobody opens the threat model. So as of this week, there's an action for it: drift-action reviews every pull request against the repo's threat model and posts one sticky comment answering one question. Do the code changes in this PR require the model to evolve?

Don’t worry, we’ll write up something more about threatcl’s drift-action soon

-me

Put ENISA's trigger list next to what it detects, and the mapping is nearly one-to-one. A new API or interface: new unmodelled surface. An auth change: stale assertions and phantom controls, the control that still claims implemented = true after the PR deleted the middleware. New sensitive data: a fresh ssn field with no information asset covering it. A dependency change: a crypto library arriving in go.mod with no third_party_dependency block. An architecture change: a service that started calling a third party missing from the DFD.

Findings carry file and line evidence, and each one ships with a prompt you can hand straight to the agent tooling to update the model. We wrote about the local, interactive version of this earlier in the month; the action is its CI counterpart.

One honesty note, because this audience will rightly ask. The eleven invariants are deterministic policy: same model, same verdict, every run. Drift detection is a judgement call made by an LLM, and the action is built to not launder that uncertainty. A run that assessed nothing, because there was no API key or the diff was too large, reports itself as unassessed rather than clean, and by default it reviews rather than blocks. Silence is never presented as safety. It's also self-hosted by design: your diff goes to the model provider under your own key, never to us.

Neither mechanism replaces judgement. Both remove the excuse that nobody noticed.

The clock

Under the CRA, reporting obligations begin on 11 September 2026, and the full essential requirements follow in December 2027. Annex C of the ENISA playbook maps each principle to the CRA's Annex I. The teams that will find this easy are the ones whose evidence is produced continuously, as a side effect of how they already work. The teams that will find it hard are the ones planning to reconstruct it during an audit.

One repo with a gate is a Makefile. Forty repos with a consistent gate, a shared severity matrix, and someone accountable for every unmitigated high is an organisational problem, and that's the layer Threatcl Cloud is being built for.

The example is a template repo: use it as a starting point, or copy the two files that matter, the model and the policy, into a repo you already have. The CLI is open source. The playbook is worth your time either way.