API & integration · v0.2
API & integration
There is deliberately no hosted TIR-CMM API. The assessment tool’s strongest property is that nothing you type ever leaves your machine, and that property is only credible if there is no server to send it to. Integrators get four layers instead — a JavaScript library, two JSON Schemas, a reference REST server you run yourself, and the machine-readable model — all of which keep assessment data inside the organisation that owns it.
The design decision, stated plainly. A hosted scoring endpoint would be trivial to build and would quietly undo the one guarantee this project makes. Every claim on this site about data never being transmitted rests on there being nowhere for it to go. So there is no api.tir-cmm.com, no account, no key, no rate limit and no telemetry — and there will not be one.
If your platform needs REST, run the reference server yourself. If it needs a function call, import the library. If it needs neither, read the schemas and the machine-readable model and build your own instrument against the same structure.
Four ways to integrate
Four artefacts, in decreasing order of coupling. Pick the lowest-coupling one that does the job: the model data outlives any particular implementation of it, and an integration built against the schemas survives changes to the library that an integration built against internals does not.
| Layer | What it is | Use it when | Licence |
|---|---|---|---|
| JavaScript library | dist/tir-cmm.mjs and dist/tir-cmm.js — the scoring engine and the model data, no DOM code, no dependencies. | You are scoring in JavaScript, in a browser, in Node, in a worker or in a build pipeline, and you want the constraint engine rather than a reimplementation of it. | Source-available, all rights reserved. Free to run and self-host; not licensed for redistribution as your own product. |
| JSON Schemas | The export contract and the TID-CMM import contract, published at tir-cmm.com/schemas/. | You are consuming or producing TIR-CMM results in any language, or validating a payload at a boundary you do not control. | CC BY 4.0 Build against them freely, including commercially. |
| Self-hosted REST server | server/server.mjs — a dependency-free HTTP wrapper around the library. | Your platform speaks REST and cannot import JavaScript, and you can run one Node process inside your own boundary. | Source-available, all rights reserved. Free to run and self-host. |
| Machine-readable model | data/tir-cmm-model.json and data/tir-cmm-model.yaml — the complete model as data. | You are building your own instrument, generating documentation, mapping the model into a GRC schema, or citing it from an AI system. | CC BY 4.0 Build against it freely, including commercially. |
Note the split. The model is CC BY-ND 4.0 and the tooling is source-available, but the schemas and the machine-readable model are deliberately CC BY 4.0 so that integration is frictionless. You do not need permission to build against them, and you do not owe a fee for doing so commercially. The licence page sets out the whole position.
JavaScript library
The library is the scoring engine and the model data, and nothing else. It carries no DOM code, no rendering and no storage, which is what lets the same file run in a browser tab, in Node, in a service worker or in a build step that generates a report.
- dist/tir-cmm.mjs — ES module. import { score } from './dist/tir-cmm.mjs'
- dist/tir-cmm.js — classic script. Load it with a <script> tag and the API appears on window.TIRCMM. It also detects CommonJS and AMD, so require() works.
Both are about 100 KB, built from the same flattened source, with no dependencies at all. There is no build step to adopt and no package to install: copy the file, or serve it from your own origin.
A worked example
Score a partial assessment, then take the leadership view, the findings and the sequenced plan off the same result object. Every call below is on the published surface.
import {
score, resolveDetection, readiness, weaknesses, blueprint,
} from './dist/tir-cmm.mjs';
// 1. Sub-capability scores. One entry per sub-capability you assessed:
// v = maturity 0-5, or null for not applicable
// ev = evidence level 0-3, which caps v under constraint R3
// A blank evidence level is treated conservatively as level 0.
const scores = {
'RP-1': { v: 3, ev: 2 }, // IR plan and scope
'RP-3': { v: 2, ev: 2 }, // roles, rotas and 24/7 reachability
'RA-1': { v: 2, ev: 2 }, // pre-authorised containment
'RA-2': { v: 1, ev: 1 }, // decision latency (MTTDecide)
'RE-1': { v: 3, ev: 1 }, // playbook coverage - capped to 2 by R3
'CE-1': { v: 3, ev: 2 }, // tiered containment options
'RV-1': { v: 1, ev: 1 }, // tabletop exercise programme
// ... the remaining sub-capabilities in scope
};
// 2. The Containment Lattice: attack-path stage x asset class, keyed 'S3-A1'.
// rrs = Response Readiness Status 0-3
// tier = criticality 1 (T3) to 3 (T1, a crown jewel)
const lattice = {
'S3-A1': { inScope: true, rrs: 0, tier: 3 }, // priv-esc on identity: blind, T1
'S3-A2': { inScope: true, rrs: 2, tier: 2 },
'S4-A1': { inScope: true, rrs: 1, tier: 3 },
'S7-A7': { inScope: true, rrs: 3, tier: 2 }, // impact on data/backup: proven
};
// 3. Decide where the detection figure for constraint R4 comes from, and what
// band ceiling that source justifies. An imported TID-CMM score carries no
// ceiling; the prerequisite check caps at L4; nothing at all caps at L3.
const detection = resolveDetection({ imported: 2.34 });
// 4. Score it.
const r = score(scores, {
detection,
lattice,
tempo: { actor: 'TA-01', breakout: 62, mttd: 180, mttdecide: 95, mttc: 40 },
});
console.log(r.band.id, r.band.name); // constraint-adjusted band
console.log(r.overall, r.selfOverall); // adjusted vs self-assessed
console.log(r.caps.map(c => c.id)); // which constraints capped the band
console.log(r.lattice.vrsCj); // crown-jewel-weighted VRS
console.log(r.tempo.margin, r.tempo.ratio); // Containment Margin, Tempo Ratio
// 5. The three leadership lenses, re-cutting the same sub-capabilities.
// The rehearsal ceiling applies here too: no lens may exceed RV + 1.
const lenses = readiness(r.effSubs, r.domains.RV);
for (const l of lenses) console.log(l.name, l.proven, l.verdict.label);
// 6. What is actually holding you back, and what to do about it.
const findings = weaknesses(r);
for (const w of findings) console.log(w.severity, w.title, w.fix);
const plan = blueprint(r, 6); // up to 6 items per horizon bucket
for (const b of plan) console.log(b.name, b.window, b.total, b.items);
The example scores a handful of sub-capabilities for brevity. A real assessment supplies every sub-capability in scope — 58 at Baseline and Assurance, 20 at Pulse — and the full in-scope lattice. Sub-capabilities left out are treated as not assessed, not as zero.
The public surface
Forty-five exports: thirty data structures and fifteen functions. This is the whole contract. Anything not in the list below is an implementation detail and may change between versions without a major version bump — including internals you can reach from a bundled build. If something you need is missing from the published surface, say so rather than reaching past it.
Data exports
The model as data. All of it is also available, in language-neutral form, in the machine-readable model.
| Export | What it is |
|---|---|
| MODEL_VERSION | The model version string, currently 0.2. Carry it into every output you produce. |
| BANDS | The six maturity bands L0 to L5, each with its score range, name and definition. |
| EVIDENCE | The four evidence levels 0 to 3, with what each expects and what it permits. |
| EVIDENCE_CAP | The R3 cap table: {0:1, 1:2, 2:4, 3:5}. A high evidence grade permits a high score; it does not create one. |
| STAGES | The eight attack-path stages S0 to S7 — the lattice rows — each with its leverage weight. |
| ASSETS | The eight asset classes A1 to A8 — the lattice columns. |
| RRS | The four Response Readiness Status values 0 to 3, from no option to proven under fire. |
| CRITICALITY_TIERS | The three cell criticality tiers T1 to T3 and their weights. |
| DOMAINS | The eight domains with their weights and their 58 sub-capabilities, each carrying its level-3 anchor, expected evidence and improvement action. |
| CONSTRAINTS | The seven integrity constraints R1 to R7, with what each caps and why. |
| CROSSWALK | Domain-level mapping to NIST CSF 2.0, SP 800-61r3, ISO/IEC 27035, RE&CT, D3FEND, SOC-CMM and the regulatory regimes. |
| TIERS | The three assessment tiers — Pulse, Baseline, Assurance — with their time cost and band ceiling. |
| PULSE_SUBS | The twenty sub-capability IDs a Pulse assessment asks about. |
| TELEMETRY_ATTRS | The attributes each asset class’s response telemetry is graded on. |
| SCENARIOS | The twelve starter scenarios. |
| SCENARIO_STAGES | The ten lifecycle stages a scenario is scored across. |
| PASS_CONDITIONS | What counts as passing a scenario rather than merely running it. |
| SCENARIO_RECORD | The minimum record a scenario run must carry to be admissible as evidence. |
| GOV_CHECKS | The governance checklist the tool actually scores. |
| CALIBRATION | Calibration questions asked of the assessor, not the assessed — each designed to find a score that is true on paper and false in practice. |
| DECISION_RIGHTS | Who decides what during an assessment, and who challenges it. |
| GOVERNANCE_RULES | The rules an Assurance-tier assessment has to satisfy. |
| ANTI_GAMING | The anti-gaming rules — the known ways a result gets flattered, named. |
| RACI | Responsibility assignment across the assessment process. |
| BOARD_RULES | How to report a result to a board without it becoming a dashboard. |
| PUBLICATION_CAUTIONS | What to be careful about when publishing a result externally. |
| LENSES | The three readiness lenses — respond, recover, resilience — and the weighted sub-capabilities behind each. |
| HORIZONS | The three roadmap horizons: quick fixes, ninety-day moves, structural work. |
| PREREQ | The prerequisite check: the detection and threat-modelling questions that stand in for a TID-CMM import. |
| ACTIONS | One improvement action per sub-capability, each with effort, type, owner, the act and the outcome it buys. |
Functions
| Function | Signature | What it does |
|---|---|---|
| score | score(scores, opts) | The engine. Applies the integrity constraints and returns the whole result object. Documented in detail below. |
| bandFor | bandFor(n) | Maps a 0–5 score to its band object. |
| resolveDetection | resolveDetection({ imported, prereq }) | Decides which detection figure constraint R4 uses and what band ceiling that source justifies: imported TID-CMM score, no ceiling; prerequisite check, L4; nothing, L3. |
| readiness | readiness(effSubs, rvDomainScore) | Scores the three leadership lenses from the R3-adjusted sub-capability scores, applying the rehearsal ceiling of RV + 1. Returns claimed, proven and a verdict per lens. |
| weaknesses | weaknesses(r) | Turns a result into ranked findings: kind, id, severity, title, body and fix. |
| blueprint | blueprint(r, limitPerBucket) | Sequences the roadmap into the three horizon buckets, each item carrying an owner, a type, the action and the outcome. Defaults to six items per bucket. |
| scenarioScore | scenarioScore(state) | Scores scenario exercising: how many were in scope, run, passed, and run but never timed. |
| governanceScore | governanceScore(state) | Scores the governance checklist and returns any band ceiling that assessment quality itself justifies. |
| telemetryScore | telemetryScore(telemetry, assets) | Grades response telemetry per asset class against the telemetry attributes. |
| stateReport | stateReport(r, { org }) | The current-state narrative in plain language, with no model jargon in it. |
| headline | headline(r) | One sentence naming the single most important thing the result says. |
| tierById | tierById(id) | Looks up an assessment tier by 'pulse', 'baseline' or 'assurance'. Falls back to Baseline. |
| prereqScores | prereqScores(answers) | Averages the prerequisite check into a detection figure, a threat-modelling figure and a count of how many questions were answered. |
| pulseCoverage | pulseCoverage() | Reports which domains the twenty Pulse questions reach and how thinly. |
| evidenceLevelOf | evidenceLevelOf(rec) | Reads the evidence level off a score record, treating a blank level conservatively as 0. |
score(scores, opts) in detail
The first argument is an object keyed by sub-capability ID, each value { v, ev } where v is the 0–5 maturity score or null for not applicable, and ev is the evidence level 0–3. The second argument is options.
| Option | Type | Effect |
|---|---|---|
| detection | object | The resolved detection object from resolveDetection(): { value, source, ceiling, note }. Its value supplies D for constraint R4, which caps RE, CE and FI at D + 1, and its ceiling caps the band when detection maturity is self-assessed or absent. A bare detectionScore number is still accepted for backwards compatibility. |
| lattice | object | The Containment Lattice, keyed 'S3-A1', each cell { inScope, rrs, tier }. Drives the Validated Response Score, the crown-jewel weighted VRS, the engineered and proven rates, the blind-cell list and constraint R6. Omit it and the lattice metrics are all zero. |
| tempo | object | null | { actor, breakout, mttd, mttdecide, mttc } in minutes. Produces the Containment Margin and the Tempo Ratio and satisfies constraint R5. Omitting it caps the band at L3, because without tempo evidence L4 and L5 are unassessable rather than merely unproven. |
| ceilings | array | Extra band ceilings contributed by constraint R7 — assessment depth and governance quality — each { id, bandId, reason }. This is how the tier ceiling and the Assurance-tier governance ceiling are injected. |
| skipEvidenceCap | boolean | Disables the R3 evidence cap. Set this only for a Pulse assessment, which does not ask for evidence at all and so must not be penalised as though evidence were absent — its L3 band ceiling carries that weight instead. Setting it anywhere else produces a number the model does not stand behind. |
score() returns a single object:
- overall and selfOverall — the constraint-adjusted score and the score before any constraint was applied. The gap between them is the part of the capability that is currently assumed rather than demonstrated.
- domains, rawDomains and preConstraint — adjusted, self-assessed, and the post-R3 baseline used for reporting each constraint’s independent effect.
- band and caps — the final band object, and every ceiling that was applied to it with its constraint ID and reason.
- adjustments and r3Hits — per-constraint detail, separating marginal from independent effects, plus every sub-capability the evidence cap actually lowered.
- lattice — vrs, vrsCj, engineered, proven, cell count, and the blind Tier-1 and Tier-2 cells.
- tempo — the resolved tempo object with margin and ratio, or null if tempo evidence was not supplied.
- detection, detectionScore — what R4 used and where it came from.
- roadmap — capability and lattice gaps merged and ranked by normalised impact.
- effSubs — the R3-adjusted sub-capability scores, which is what you pass to readiness().
JSON Schemas
Two contracts, both JSON Schema draft 2020-12, both licensed CC BY 4.0. That is deliberate and it is the whole point: integration should be frictionless, so you may build against them freely, including in commercial products, with attribution and without asking.
| Schema | URL | What it is for |
|---|---|---|
| Assessment export | https://tir-cmm.com/schemas/ |
The result of a TIR-CMM assessment: band, adjusted and self-assessed scores, per-domain scores, lattice metrics, blind cells, tempo, constraint adjustments, readiness lenses and scenario counts. Emitted by the assessment tool and consumable by UTIOM roadmap tooling, GRC platforms and reporting pipelines. Requires schema, model_version, overall, band and domains. |
| TID-CMM import | https://tir-cmm.com/schemas/ |
What TIR-CMM consumes from a TID-CMM detection assessment: detection maturity, crown jewels, modelled attack paths, priority actors with breakout times, and in-scope techniques with their ATT&CK mitigation codes. Every field is optional except the schema tag, because TIR-CMM runs standalone and a partial import simply pre-fills less. |
The field that carries the most weight
detection_score_pre_substitution supplies D for constraint R4, which caps the response-execution, containment and forensics domains at D + 1: you cannot respond to what you never saw.
It is deliberately the pre-substitution figure. Once a TIR-CMM assessment exists, TID-CMM substitutes the TIR-CMM overall into its own incident-response domain. If TIR-CMM then read the post-substitution TID-CMM score, the response score would be feeding the constraint that governs it, and both numbers would inflate each other quietly and indefinitely. Reading the pre-substitution figure breaks that loop. The ordering is stated in both specifications, and an integration that supplies the wrong figure will produce a result that looks better than it is.
Self-hosted REST server
A minimal HTTP wrapper around the library, provided so that platforms which need REST can have it without assessment data leaving the organisation that owns it. You run it, inside your own boundary, so the data never reaches anyone else. That is the only form of TIR-CMM REST there is, and the only form there is going to be.
- Zero dependencies. Node’s own http, fs, url and path modules, nothing else. No install step.
- Node 18 or later, for top-level await and native ES module import.
- No storage. Nothing is written to disk and nothing is logged beyond the startup line. Every response is sent with cache-control: no-store and an x-tir-cmm-version header.
- Request bodies are capped at 2 MB.
node server/server.mjs --port 8787
TIR-CMM reference server v0.2 on http://localhost:8787
Assessment data stays in this process. Nothing is sent anywhere.
The port may also be set with the PORT environment variable; it defaults to 8787.
Endpoints
| Endpoint | Returns |
|---|---|
| GET /health | Liveness: { ok, model, version }. |
| GET /model | The complete machine-readable model, served straight from data/tir-cmm-model.json. |
| GET /model/domains | The eight domains with their weights and all 58 sub-capabilities. |
| GET /model/constraints | The seven integrity constraints. |
| GET /model/tiers | The three assessment tiers with their band ceilings. |
| GET /model/scenarios | The twelve scenarios, the ten scenario stages and the pass conditions. |
| POST /score | Scores an assessment and returns the full export payload, shaped to the tir-cmm/export/0.2 schema. |
| POST /report | Everything /score returns, plus the headline, the plain-language state report, the ranked findings and the full sequenced blueprint. |
Scoring a request
Both POST endpoints take the same body. Every key is optional: scores, lattice, tempo, telemetry, scenarios, governance, prereq, detectionScore, tier, and organisation for /report. The server resolves detection, looks up the tier, scores governance, assembles the R7 ceilings and calls the library exactly as the assessment tool does.
curl -s http://localhost:8787/score \
-H 'content-type: application/json' \
-d '{
"tier": "baseline",
"organisation": "Meridian Group",
"detectionScore": 2.34,
"scores": {
"RP-1": { "v": 3, "ev": 2 },
"RA-1": { "v": 2, "ev": 2 },
"RA-2": { "v": 1, "ev": 1 },
"RE-1": { "v": 3, "ev": 1 },
"CE-1": { "v": 3, "ev": 2 },
"RV-1": { "v": 1, "ev": 1 }
},
"lattice": {
"S3-A1": { "inScope": true, "rrs": 0, "tier": 3 },
"S4-A1": { "inScope": true, "rrs": 1, "tier": 3 },
"S7-A7": { "inScope": true, "rrs": 3, "tier": 2 }
},
"tempo": { "actor": "TA-01", "breakout": 62,
"mttd": 180, "mttdecide": 95, "mttc": 40 }
}'
The response is the export payload. Abbreviated, and using the specification’s worked example rather than the shortened body above:
{
"schema": "tir-cmm/export/0.2",
"model_version": "0.2",
"tier": "baseline",
"overall": 2.32,
"self_assessed": 2.69,
"band": "L1",
"band_name": "Documented",
"band_capped_by": ["R5", "R6"],
"domains": { "RP": 2.80, "RA": 1.90, "RE": 2.40, "CE": 2.50,
"AO": 2.20, "FI": 2.00, "RV": 1.60, "RG": 2.30 },
"detection_score": 2.34,
"detection_source": "tid-cmm",
"readiness": {
"respond": { "proven": 2.10, "claimed": 2.44, "verdict": "Partial" }
},
"vrs_cj": 0.41,
"engineered_rate": 0.406,
"proven_rate": 0.031,
"blind_cells_t1t2": [ { "stage": "S3", "asset_class": "A1", "tier": "T1" } ],
"tempo": [ { "actor": "TA-01", "breakout_min": 62,
"mttd_min": 180, "mttdecide_min": 95, "mttc_min": 40,
"containment_margin_min": -253, "tempo_ratio": 5.08 } ],
"constraint_adjustments": [ { "constraint": "R1", "domains": ["RP","CE","AO"] } ]
}
Say it again, because it matters. This server exists so that nobody has to send an assessment to us. Run it on localhost, in your own container, on an air-gapped network — anywhere inside your own boundary. If you find yourself pointing a client at a TIR-CMM server you do not operate, something has gone wrong, because there is no official one to point at.
Machine-readable model
The whole model as data, in two formats with identical content: data/tir-cmm-model.json for programs, and data/tir-cmm-model.yaml for humans, diffs and review. Licensed CC BY 4.0.
It carries:
- the eight domains with weights, the question each one answers, and their 58 sub-capabilities — each with its weight, its level-3 anchor, the evidence it expects, and its improvement action with effort, type, owner, the act and the outcome;
- the eight attack-path stages and eight asset classes that form the Containment Lattice, plus the Response Readiness Status scale and the three criticality tiers;
- the seven integrity constraints, the six maturity bands, the four evidence levels and the evidence cap table;
- the three assessment tiers with their ceilings, the Pulse subset, the telemetry attributes, the prerequisite check, the three readiness lenses and the three horizons;
- the twelve scenarios with their lifecycle stages and pass conditions, the governance checks, the calibration questions and the anti-gaming rules;
- the crosswalk to NIST CSF 2.0, SP 800-61r3, ISO/IEC 27035, RE&CT, D3FEND, SOC-CMM and the regulatory regimes;
- and the licence block, so the terms travel with the data.
This is the best artefact to cite. If you are building documentation, a GRC mapping, a training course or an AI system that needs to answer questions about TIR-CMM accurately, read this file rather than scraping the pages: it is generated from the same source the tool runs on, so it cannot drift from the model, and it carries the version and licence in the payload. The site also publishes llms.txt and llms-full.txt for the same reason.
Versioning and stability
- The export schema tag is tir-cmm/export/0.2. It is a const in the schema, so a payload carrying any other tag is invalid against it rather than merely unfamiliar.
- The model version travels in every payload, in model_version, in the machine-readable model, in the library as MODEL_VERSION and in the server’s x-tir-cmm-version response header. Record it alongside any result you store; a score without its model version cannot be compared with anything later.
- Breaking changes bump the schema tag. A change to the meaning of a field, the removal of a field, or a change to how a constraint is applied all count as breaking. Additive fields do not: both schemas set additionalProperties: true, so a consumer that ignores unknown keys will keep working.
- The published API list is the contract. Anything outside it — internal helpers reachable in a bundled build, the exact shape of intermediate objects, the wording of a generated narrative — may change without a major bump.
- TIR-CMM is at v0.2, a draft for review. Several design questions are deliberately open. Expect the model to change before v1.0, and pin the version you built against.
Attribution
If your product integrates TIR-CMM, credit it, name the version, and link to the canonical source. A reasonable citation is:
TIR-CMM v0.2 - Threat-Informed Response Capability Maturity Model,
Reza Adineh, https://tir-cmm.com
In a user interface, a line such as “Maturity scoring based on TIR-CMM v0.2 (CC BY-ND 4.0) — tir-cmm.com” near the result satisfies it. In a machine-readable output, carry model_version and the schema tag, which you get for free by emitting a conforming payload.
Two things attribution does not license, and both are on the licence page in full: you may not publish a modified, rebranded or “extended” version of the model, and you may not redistribute the tooling as your own product. You may build whatever you like against the schemas and the machine-readable model, run the reference server, self-host the tool inside your organisation, assess clients commercially, and publish the results — those are yours.
Building something and unsure whether it falls inside the licence? Ask. The about page explains how, and permission for translations, workbook variants and similar is usually given. The only changes withheld are the ones that would weaken the integrity constraints.