Research Briefing
From Fifteen Taxonomies to One: A Method for Building a Canonical Risk Model That Can Actually Judge a Scenario
A methodology for deriving a single canonical risk model from fifteen incompatible security and AI governance frameworks, so a scenario-assessment engine can reason over one fixed vocabulary instead of special-casing every source standard.
Contents
The problem
Any security product that wants to say “here are the risks in your AI deployment, and here is what standards say about them” faces a translation problem before it can do anything else.
Security and AI governance frameworks — OWASP Top 10, MITRE ATT&CK/ATLAS, NIST AI RMF, NIST CSF, ISO/IEC 27001/42001/23894, the EU AI Act, CIS Controls, ENISA guidance, and others — do not share a common data model. Each one names its own units of citable knowledge in its own vocabulary.
OWASP has risk categories. MITRE has techniques and mitigations. NIST has subcategories organized under functions. ISO has clauses and annex controls. The EU AI Act has articles, recitals, and annexes. None of these line up cleanly with one another, and none of them line up cleanly with what an assessment engine actually needs to reason over: a fixed, small set of entity types with well-defined evaluation semantics.
A minimal example makes the mismatch concrete:
OWASP LLM01 MITRE ATLAS AML.T0051
"Risk category" "Technique"
│ │
└───────────┬───────────┘
▼
Canonical model
Risk
Two different frameworks, two different native vocabularies (“risk category,” “technique”) — but both describe something your engine ultimately needs to reason over as one thing: a risk. The canonical model is what makes that collapse possible without losing what each source actually said.
If you try to import these frameworks directly — one bespoke integration per standard — you get an explosion of one-off logic. Worse, you get silent inconsistency: the same underlying risk (say, prompt injection) gets represented differently depending on which standard happened to describe it first, because your model absorbed that standard’s vocabulary instead of a vocabulary of your own design.
The alternative — build a single canonical model up front and force every framework into it — fails for a different reason. It’s easy to under-observe: to design the canonical vocabulary by looking at one or two frameworks (the ones you’re integrating first) and missing entire categories that only become visible once you look at the full landscape. It’s also easy to over-design: to keep adding entity types every time a new framework doesn’t fit cleanly, until the model is as complicated and inconsistent as the frameworks it was supposed to unify.
The method below is what actually worked to solve this — not as a one-off fix, but as a repeatable discipline for adding new frameworks over time without re-litigating the same design questions.
The core idea: separate three planes that are usually conflated
The failure mode that causes the most damage is conflating three questions that are logically independent:
- What kind of citable item does this framework actually contain? — a purely observational question. You are just reading the standard and cataloguing what it names as a distinct, individually-referenceable thing.
- What does that kind of item translate to, once imported? — a design/policy question. Given that a framework calls something a “technique,” what entity should it become in your system?
- Does your canonical model have room for that entity, or does the model itself need to change? — an architectural question, the most expensive one to get wrong, because canonical models are hard to walk back once other systems depend on them.
Almost every mistake in this kind of work comes from answering question 2 or 3 while still trying to answer question 1 — deciding what a framework “is” by asking what it should become, instead of first establishing what it actually is on its own terms.
The fix is procedural: answer the three questions in strict sequence, and don’t let an answer from a later stage leak backward into an earlier one.
Stage 1 — Document the taxonomy of each framework, in its own words
For every framework you plan to support, inventory the types of citable item it defines, using its own terminology, and explicitly separate two things that are easy to conflate:
- Leaf items — units of knowledge with a stable, citable identity (an ID, a numbered clause, a code). These are the things a third party would actually reference: “OWASP LLM01,” “MITRE ATLAS AML.T0051,” “EU AI Act Article 9.”
- Structural constructs — containers and selection mechanisms: tactics, functions, categories, chapters, families, levels, profiles, baselines. These organize leaf items; they are not themselves items. A tactic groups techniques the same way a chapter groups clauses — it’s a shelf, not a book.
This distinction alone eliminates a large class of errors. It is tempting to treat a container as a lightweight version of a leaf item — “this Tactic is kind of like a risk” — because containers often carry meaningful names. Resist this.
If something exists only to group other citable things, it is structure, and belongs in your snapshot’s hierarchy, not in your item-type vocabulary.
Fix the exact version of every framework you inventory, and re-verify at each real import. Standards revise; a taxonomy derived against last year’s version can silently drift from what you actually import.
Stage 2 — Derive a common vocabulary by observing convergence, not by picking a favorite
Once you have fifteen (or however many) independent inventories, look across them for genera — categories of item that recur across multiple frameworks under different names. A genus earns a place in your common vocabulary when multiple frameworks demonstrably exercise it as a first-class, individually-citable thing — not when it merely sounds similar to something else you’ve already decided to keep.
Two disciplines make this step trustworthy.
Name the genus by majority observation, not by whichever framework you looked at first. If six frameworks call something a “control” and only one calls it a “mitigation,” the shared name should be “control.” This sounds obvious once stated, but it’s the single most common bias in this kind of exercise: whichever standard is analyzed first (usually because it’s the most technically detailed, or the first one on the adoption roadmap) ends up donating its vocabulary to everything else by default. Naming by majority, deliberately, breaks that bias.
Trust intra-framework distinctions over cross-framework impressions. The strongest evidence that two candidate genera are genuinely different — not just two names for the same thing — comes not from comparing frameworks to each other, but from finding a single framework that maintains both as separate citable sets internally. If a standard has both “clauses” (process obligations) and “annex controls” (concrete safeguards) as two distinct numbered lists within the same document, that framework has already done the disambiguation work for you. When several independent frameworks draw the same internal boundary, the taxonomy should preserve it rather than collapse it into “prescription” for the sake of a smaller vocabulary.
Also watch for genera that go invisible because you’re already thinking about destinations. It is very easy, while inventorying frameworks, to unconsciously filter for “things that map to an entity type I already have.” Interpretive, non-binding content — footnotes, recitals, implementation guidance, discussion sections — often gets skipped this way, because nothing in your canonical model obviously wants it.
But if a framework cites that content with a stable identity (numbered recitals cited routinely in legal practice, for example), it is real citable knowledge, whether or not you yet know what to do with it. A genus is real because the frameworks exercise it, not because your model already has a place for it.
Genera with weak cross-framework support deserve real scrutiny, but weak support is not automatic disqualification. If only one framework in your set exercises a genus, but that framework is a first-priority adoption target and the genus has its own stable, citable identifiers in the source, the honest move is to include it — while being explicit that its justification rests on fidelity to that one important source, not on consensus. The alternative — folding it into an unrelated genus, or dropping its individual identity to avoid a “wasted” vocabulary slot — corrupts your ability to cite that framework accurately, which is the whole point of the exercise.
Finally, stress-test the resulting vocabulary against frameworks outside your original inventory. A closed vocabulary that has never been checked against anything but the frameworks used to build it isn’t closed — it’s untested. A genuinely stable vocabulary should be able to absorb a handful of frameworks it has never seen without needing a new category; document explicitly which observed-but-excluded categories exist, why they were excluded, and what future condition would justify revisiting that exclusion. “Closed, with documented exceptions and explicit revisit triggers” is a defensible position. “Closed because we didn’t look further” is not.
Stage 3 — Ask separately whether the canonical model can absorb what you found
Only now, with a documented and observation-grounded vocabulary in hand, ask the architectural question: for each genus, can your existing canonical entities represent it, or is there a real gap?
Evaluate each genus against three possible verdicts:
-
Natural absorption — an existing canonical entity already means the same thing. A genus meaning “something that can go wrong” maps cleanly onto an existing “Risk” entity; internal variants (a risk framed as a threat, a risk framed as a root-cause weakness, a risk framed as an organizational risk source) don’t need separate treatment — they’re the same genus described with different emphasis by different authors, and any distinction that matters operationally can live as an attribute rather than a new type.
-
A policy matter, not a model gap — the genus is real, but where it lands depends on the specific item’s content, not on its genus in the abstract, and your existing entities are expressive enough to hold whichever destination is chosen. Interpretive content that clarifies another item, for instance, doesn’t need its own canonical entity — a reference plus a relationship to the item it interprets is sufficient, and inventing an entity for “commentary” would blur the line between knowledge and annotation.
-
A genuine model gap — no existing canonical entity, and no simple relationship among existing entities, can represent the genus without distorting its meaning. This is the expensive case, and it deserves its own discipline (below).
The critical failure to avoid at this stage is doing the opposite of what caused trouble at Stage 1: now that you’re finally allowed to think about destinations, it’s tempting to let destination convenience quietly redefine the vocabulary you just spent two stages deriving honestly. Keep the vocabulary fixed. If a genus doesn’t fit, that’s information about your canonical model, not license to reshape the taxonomy.
What this separation actually buys you
Before going further into the harder cases, it’s worth being explicit about why this three-stage discipline is worth the overhead. Kept apart, these three planes deliver four concrete benefits:
- New frameworks can be added without touching the reasoning engine. Because the engine only ever evaluates the canonical vocabulary, onboarding framework sixteen is an inventory-and-translation exercise, not a rewrite.
- Every fact stays traceable to its source. A canonical “Risk” entity can still point back to “OWASP LLM01” or “Article 9” — the abstraction doesn’t erase the citation.
- Duplicate entities stop happening. When the mapping from genus to entity is a disciplined, checked process rather than an ad hoc guess, the same real-world concept doesn’t get modeled twice under two different names.
- Framework churn and knowledge evolution move independently. Standards revise, get superseded, or get reinterpreted; the canonical vocabulary the engine reasons over doesn’t have to move in lockstep with every one of those changes.
With that motivation in place, the harder question is what happens when a genus genuinely doesn’t fit anywhere in the model yet.
When a genus is irreducible: separate “the model can’t represent this” from “the engine can’t reason without it”
The hardest case is a genus that is demonstrably real (multiple frameworks exercise it, possibly validated by an independent standard outside your framework list) and demonstrably irreducible (it doesn’t collapse into any existing canonical entity without losing its meaning). MITRE’s technique genus — a concrete method an adversary uses, distinct from the risk it produces, distinct from the control that mitigates it, distinct from any detection signal — is a canonical example.
This is where a second conflation causes real damage: a representational gap is not the same claim as a functional insufficiency, and only the second one justifies changing your core model.
A representational gap means: this genus, as a concept, doesn’t have a home in the model. It’s a claim about the taxonomy.
A functional insufficiency means: the assessment engine, given the model as it stands, produces worse outcomes — misses real risks, produces unreliable results, can’t explain its reasoning — because that entity doesn’t exist. It’s a claim about system behavior under real evaluation.
The first can be true while the second is false. A genus can sit permanently as pure reference material — citable, useful for traceability, contributing nothing to automated reasoning — without that being a defect. This is in fact the correct steady state for genera that are only weakly connected to your reasoning engine: a framework can name obligations or interpretive guidance that your system simply cites without ever needing to reason over them directly.
- State the null hypothesis — the existing model, used as intended (including its narrative fields and its existing relationships), is functionally sufficient for the reasoning task in question.
- State the competing hypothesis — the existing model cannot express the required reasoning with the fidelity, reproducibility, and traceability the product needs.
- Design a real experiment, not a thought experiment. Reuse whatever evaluation harness the reasoning engine already has — don’t build new infrastructure just to settle this question. Curate a set of test cases specifically designed to discriminate between the two hypotheses: cases where the answer is only reachable by recognizing the pattern the missing entity would represent, and where the literal, already-existing detection mechanisms deliberately do not fire.
- Run two arms, not one. Test the model as it stands (the null hypothesis) — but also test the best possible workaround achievable within the existing model (documenting the missing concept using whatever existing entity comes closest, wired up with existing relationships). The second arm exists specifically to prevent the new entity from being approved out of modeling laziness rather than a genuine limit: if the workaround performs just as well, the gap really was purely representational, and adding a new entity would only add cost, not capability.
- Pre-register your metrics and thresholds before running the experiment, not after. Decide what “the model failed” looks like — missed detections, false positives, non-reproducibility across repeated runs, and a specific one that matters more than it first appears: can the system’s output cite something with a stable identity, or does it fall back to unstructured prose? — before you see any results.
- Watch for the failure mode in both directions. If the “best workaround” arm only succeeds by turning an existing lightweight entity into something that behaves, structurally, exactly like the proposed new entity — evaluable, with de facto typed relationships bolted on — that is not evidence the existing model is sufficient. It’s the new entity in disguise, minus the schema guarantees a real entity would have, which is the worst outcome available: you pay the full semantic cost of the new concept while getting none of the integrity benefits of formalizing it. Conversely, if the model-as-is genuinely resolves the test cases through its existing narrative fields, resist adding the entity anyway “because it would be more elegant” — representational elegance is not, by itself, evidence of a functional requirement.
This turns a debate that otherwise resolves by seniority or persuasiveness into something falsifiable, and it keeps expensive, hard-to-reverse model changes gated behind actual evidence from the system doing actual work — not behind how compelling the argument sounds in the abstract.
Translating a genus at import time, without hard-coding a destination
Even for genera that map naturally onto an existing entity, resist the temptation to fix that mapping as a rigid, one-to-one function of the genus label alone. The same underlying concept is frequently described differently by different frameworks — the same attack technique might be the leaf item of one framework’s “technique” genus and, independently, the leaf item of another framework’s “risk” genus, because the two standards simply chose different levels of abstraction to describe the same real-world problem.
If your import logic maps “technique” to one fixed destination regardless of context, you will create duplicate entities every time two frameworks describe the same underlying concept at different levels of abstraction — and that duplication corrupts everything downstream that depends on your canonical catalog having exactly one entity per real-world concept.
The correct design keeps the genus-to-entity mapping conditional on runtime context, not fixed at the schema level:
- Check first whether an equivalent entity already exists in your catalog before creating anything new. This has to be a mandatory, ordered first step — never create-then-reconcile. Reconciliation after the fact accepts duplicates as a temporary state; checking-first prevents them from ever existing.
- Fall back to a versioned heuristic table, parameterized by the specific combination of source framework and item substructure (not by the genus alone), for the cases where no equivalent is found and a genuine judgment call is required. This preserves determinism — the same heuristic table applied to the same import always produces the same decision — which matters because importing frameworks needs to be a reproducible operation, not a one-off human judgment call each time.
- Log every translation decision — which rule fired, what equivalence (if any) was detected, what the final result was. This is what turns “the system decided X” into something an operator can audit and, if wrong, correct going forward without re-deriving the whole heuristic from scratch.
This design deliberately keeps the ambiguity where it belongs — in the runtime translation logic, evaluated against the actual state of the catalog — rather than pretending it doesn’t exist by baking a false certainty into the schema.
Preserving the negative results
The single most underrated output of this kind of analysis is not the list of things you decided to build — it’s the list of things you deliberately decided not to build, and why. A canonical model debate that gets settled once and then forgotten gets re-opened from scratch a year later, at real cost, by someone who has no way of knowing it was already settled.
Every rejected or deferred model change deserves a short, permanent record, separate from the working analysis that produced it, containing exactly three things:
- What was considered — the specific entity or relationship that was proposed.
- Why it wasn’t adopted — representational gap without demonstrated functional need; the hypothesis-and-falsification framing if one was used; whatever the actual reasoning was.
- The condition that would reopen the question — a specific, checkable trigger, not a vague “revisit later.” “If the reasoning engine gains a compliance-assessment mode” is a trigger. “Reconsider if needed” is not.
This record is architecture documentation, not a task list — it’s knowledge about the shape of the domain that outlives whichever specific piece of work prompted the analysis. Anyone touching the canonical model later should be able to find it and understand, in under a minute, whether a given idea has already been tried and explicitly set aside, and under what circumstances it becomes worth trying again.
Why this matters for scenario assessment specifically
None of this is abstract taxonomy work for its own sake. The entire point of building a single canonical model out of many incompatible framework vocabularies is that a scenario-assessment engine can only reason reliably over a fixed, well-understood set of entity types with clear evaluation semantics.
An engine that has to special-case its logic per source framework — “if this fact came from MITRE, reason about it one way; if it came from ISO, reason about it another way” — cannot give a reproducible, explainable answer to “does this risk apply to this scenario, and why.” A canonical model gives the engine one consistent vocabulary to evaluate against, regardless of which standard originally supplied the underlying knowledge; the translation work described above is what makes that vocabulary trustworthy rather than merely convenient.
The discipline is what makes the result durable: observe before you translate, translate before you judge the model, and never let a model change happen without the burden of proof resting on demonstrated need rather than representational tidiness.
Frameworks will keep evolving — new versions, new standards, new regulatory language. This architecture is what lets that churn stay someone else’s problem: new frameworks get onboarded through inventory and translation, not through a rewrite of the reasoning engine, because the canonical vocabulary underneath it was built to stay stable while everything feeding into it keeps changing.