Skip to content

Change Risk Policy Design

Ed Mozley edited this page Nov 5, 2026 · 2 revisions

Change risk policies - design

Issue #246, requested by Dr4inder Β· Status: built, released in 4.10.0 (changelog #2736)

As built: Change risk policies (user page) Β· Change risk policies - Developer Guide. Where the build differs from the plan below, it is marked As built.

Let an administrator set the Change risk scale, the guidance for each level, the results and the matrix that links them, so FreeITSM follows the organisation's own risk policy instead of a fixed 5 Γ— 5. The built-in 5 Γ— 5 stays the default, and nothing changes until somebody deliberately sets up a policy of their own.

Related: Change Management (the user page), Change Management - Developer Guide, and #231, which added the rating guide this builds on.


1. Where it stands today (4.9.0)

Part Today Where
Likelihood and impact Fixed 1-5 each, labelled 1 - Very low to 5 - Very high the change form, ChangesService::riskInput()
Score likelihood Γ— impact, 1-25 ChangesService::createChange() / updateChange()
Result Bands: ≀4 Low, ≀9 Medium, ≀15 High, ≀20 Very High, else Critical ChangesService::riskLevel(), copied by hand into riskLevelForScore() in change-management.js
Stored on the change risk_likelihood, risk_impact_score, risk_score, risk_level (the English word, VARCHAR(20)) changes
Colours CSS classes from the word (risk-very-high) change-management.css; a second copy as a SQL CASE in the Report Pack block
Guidance The #231 guide: what likelihood and impact mean, example criteria for 1-5, a worked example, and "these are examples, not a policy" change-management/index.php, translated
Matrix 5 Γ— 5, drawn in the browser renderRiskMatrix()
Templates A template holds a likelihood and impact, copied onto the change and part of what an approved template pre-authorises ChangeTemplates::FIELDS
Approvals Likelihood and impact are "material" by default: changing them after approval needs a fresh approval change_approval_material_fields
Workflows change.risk is the result word; the built-in CAB template matches High, Very High, Critical workflow/includes/templates.php
REST API Accepts 1-5, documents the five words, filters and sorts on them api/v1/resources/changes.php, spec.json

What does not depend on it. No approval route, SLA or permission branches on the risk result. It is shown, reported, filtered and compared by workflows - so a different scale cannot break a rule elsewhere, only change what those places show.

Out of scope: the Projects module's RAID log has its own 1-5 probability and impact. It is a different register and stays as it is.


2. The model

A risk policy is:

Part What the administrator sets
Name e.g. Information security policy 2026
Likelihood levels 2 to 10, in order from lowest; each a label (gering) and criteria (what it means here)
Impact levels the same, set separately - the two axes have their own number of levels, labels and criteria
Results 2 to 8, in order from lowest; each a label (sehr hoch) and a colour
Matrix for every likelihood Γ— impact cell, which result it is

An explicit cell mapping, not thresholds. The issue leaves this to the maintainer. Thresholds over a score assume the policy is "multiply and band", which many policies are not (an impact of Very high is often never below High whatever the likelihood). A mapping can express any policy, including "multiply and band", and it is what the administrator already has on paper. A policy cannot be saved with an empty cell.

The score stays likelihood Γ— impact. It is kept for continuity - the matrix cells show it, the API returns it, existing reports sort by it - but it no longer decides the result. The matrix does.

Nothing is built in except today's 5 Γ— 5. No four-level preset, no percentages or durations: the issue asks for none, and a preset would be FreeITSM writing somebody's policy. A new policy starts as a copy of the one in use, which the administrator then shapes.

Versions

A policy is never edited in place. Every save makes a new version; versions are immutable. Each change records the version it was assessed under, so:

  • a change assessed under the 5 Γ— 5 keeps its 1-5 values, its score, its result and its matrix, drawn from that version, for ever;
  • what an approver approved is what is still shown;
  • switching to four levels never discards or remaps a level-5 value - it stays on the change, under the version it was given in.

The built-in 5 Γ— 5 is version "built-in". It is not stored; it is in the code, translated as today. A change with no recorded version was assessed under it. So every existing change is correct on upgrade with no data migration, and an install that never opens the new tab is byte-for-byte unchanged.

One policy per install now, per company later

Now Later
Who it applies to Every company A company may have its own; the rest use the install's
Stored as One change_risk_policies row with tenant_id NULL More rows, one per company that has its own
Looked up by ChangeRiskPolicy::current($conn, $tenantId) - which already takes the change's company and falls back to the install's the same call, unchanged

So adding per-company policies later is a settings screen, not a rework: every code path already asks for this change's company's policy.


3. What happens where

The change form

  • Likelihood and impact offer the policy's levels, by label. The value saved is still the level's number (1 = lowest), so an integration sending numbers keeps working.
  • The ? guide shows the policy's criteria for each level in its table. The general paragraphs from #231 (what likelihood and impact mean, the same-thing note) stay. "These are examples, not a policy" is replaced by "Your organisation's risk policy: {name}" - because then it is.
  • The result shows in the policy's colour.

A change assessed under an older version

The form keeps showing that version's levels, so editing the title does not touch the risk. A line under the fields says "Assessed under {policy} (version {n}). Re-assess to use the current policy." Re-assess clears the two ratings and offers the current policy's levels. Saving records the move in the audit trail (Risk policy: Built-in 5 Γ— 5 β†’ Information security policy v2).

The API follows the same rule. Sending a rating for a change on an older version re-assesses it under the current policy, and then both ratings are required - one new number next to one old one would mean nothing. Sending neither leaves the risk alone.

Approvals

When the risk ratings are material (the default), a re-assessment under a new policy is material too, even when the numbers happen to stay the same - 3 of 5 and 3 of 4 are not the same rating. The approval fingerprint therefore includes the policy version - but only when it is not the built-in one, so every existing round's fingerprint is unchanged and no approved change suddenly reads as edited.

Templates

A template's ratings belong to the policy they were given under. The template remembers that version. Applying a template under a different policy version leaves the two ratings empty and says so, rather than reading 4 on one scale as 4 on another. Its other fields still apply. An approved template whose ratings were not copied does not pre-authorise the change - the change differs from what was approved. Re-rating the template and approving the new version puts it back.

The matrix, lists, table, evidence and preview

All of them draw from the version the change was assessed under - its levels, its results, its colours. The matrix is as many rows and columns as that version has. A stored result is shown as stored; colours come from the version (the built-in one keeps today's exact classes and colours).

Reports

The Report Pack changes by risk block groups by the stored result word, as now, and takes each group's colour from the policies instead of the hard-coded CASE.

Workflows

change.risk is still the result as stored, so a condition on it reads the policy's own words. Renaming a result in a new version means updating any workflow that compares it - the policy page says so, beside the results. The built-in CAB template still matches the built-in words.

REST API

  • Write: risk_likelihood / risk_impact_score are validated against the change's policy: "'risk_likelihood' must be between 1 and 4."
  • Read: risk gains policy - { "version_id", "name", "version", "levels": { "likelihood", "impact" } } - and the result's label as stored. level is unchanged. As built: policy is { version_id, builtin, name, levels }, name already carrying the version ("Name (version 2)"), and null when unrated; level is the stored word, so no separate label.
  • New: GET /changes/risk-policy - the current policy in full (levels, criteria, results, matrix), so an integration can offer the right choices. As built: GET /change-risk-policy, beside the other change reference data (/change-impacts), so it can never be mistaken for /changes/{id}.
  • The documented list of five words becomes "the policy's results; the built-in policy's are ...".

4. Settings

A new tab, Change management β†’ Settings β†’ Risk policy, behind its own permission, Manage the change risk policy (changes.risk_policy). Marked sensitive: it decides how risky every new change reads.

  • In use - the policy name and version, how many changes were assessed under it, and the versions before it with their counts.
  • Edit - name; likelihood levels; impact levels; results; the matrix as a grid of dropdowns, coloured by the chosen result, rows = likelihood (highest at the top, as on a change), columns = impact. Adding or removing a level adds or removes its row or column; new cells start empty.
  • Save makes the next version and applies it to new assessments. A summary is asked to confirm first: "Changes already assessed keep their rating. New and re-assessed changes use this version."
  • Built-in - go back to the built-in 5 Γ— 5 for new assessments. Also recorded as a version change. As built: the button is Use built-in (two words for clarity); it sets the policy back to the built-in without adding a version, and the history lists the built-in with its count.
  • Labels are what the administrator types, in their own language - they are the policy's words, not FreeITSM's, and are not translated. The built-in policy stays translated.

5. Data

CREATE TABLE change_risk_policies (
    id                  INT AUTO_INCREMENT PRIMARY KEY,
    tenant_id           INT NULL,              -- NULL = the install's policy; a company's own later
    current_version_id  INT NULL,              -- NULL = the built-in 5 x 5
    updated_by_id       INT NULL,
    updated_datetime    DATETIME NULL,
    KEY ix_crp_tenant (tenant_id)              -- see the note below
);

CREATE TABLE change_risk_policy_versions (
    id              INT AUTO_INCREMENT PRIMARY KEY,
    policy_id       INT NOT NULL,
    version_no      INT NOT NULL,
    content         LONGTEXT NOT NULL,         -- the JSON below; never updated
    content_hash    CHAR(64) NOT NULL,
    created_by_id   INT NULL,
    created_datetime DATETIME NOT NULL,
    UNIQUE KEY uq_crpv_policy_version (policy_id, version_no)
);

ALTER TABLE changes          ADD risk_policy_version_id INT NULL;   -- NULL = built-in
ALTER TABLE change_templates ADD risk_policy_version_id INT NULL;
ALTER TABLE changes MODIFY risk_level VARCHAR(50) NULL;           -- a policy's own words can be longer

MySQL treats every NULL as distinct in a unique key, so a unique key on tenant_id would not stop two install-wide rows. It is a plain key, and the service finds-or-creates the one row inside a GET_LOCK instead.

The version content:

{
  "name": "Information security policy",
  "likelihood": [ { "label": "gering", "criteria": "..." }, ... ],
  "impact":     [ { "label": "gering", "criteria": "..." }, ... ],
  "results":    [ { "label": "gering", "colour": "#16a34a" }, ... ],
  "matrix":     [ [0, 0, 1, 1], [0, 1, 1, 2], ... ]
}

matrix[l][i] is the result index for likelihood level l + 1 and impact level i + 1. Levels and results are lowest first.


6. Decisions taken

# Question Decision
1 Thresholds or a cell mapping? Cell mapping - expresses every policy, including multiply-and-band
2 Edit in place or versions? Immutable versions; each change records its own
3 Existing changes on upgrade No migration: no version = built-in, which is still in the code
4 One policy or one per company? One per install now, stored and looked up per company, so per-company is a later screen
5 A four-level preset? No - a new policy starts as a copy of the current one
6 Re-rating an older change Deliberate Re-assess, both ratings required, recorded in the audit trail
7 Approval fingerprint Includes the version only when not built-in, so no existing round changes
8 Templates under another version Ratings not copied, and said so; pre-authorisation needs a template rated under the current version
9 Translation Built-in policy translated; a custom policy's words shown as typed
10 Workflows Compare the stored word, as now; the policy page warns that renaming a result needs workflow conditions updated
11 Score Still likelihood Γ— impact, kept for continuity; the matrix decides the result
12 Projects RAID log Out of scope

7. How it is tested

As built: all of the below, 46 checks, plus a browser pass on the test install and a REST pass with a real key - see Testing it.

tests/change-risk-policy.php, on a test database:

  • the built-in policy gives exactly today's results for all 25 cells;
  • a 4 Γ— 4 policy validates, saves as version 1, and a new change gets its result from the matrix, not from multiplying;
  • a change assessed under the built-in policy keeps 5 / 5 / 25 / Critical and its built-in version after the 4 Γ— 4 is introduced, and its approval fingerprint is unchanged;
  • a rating of 5 is refused under the 4 Γ— 4 with a message naming the range, by the service and so by the API;
  • sending one rating to an older change is refused; both re-assess it and audit the policy change;
  • a policy with an empty cell, a result out of range, a level count out of range or a bad colour is refused;
  • a template rated under the built-in policy does not copy its ratings once the 4 Γ— 4 is in use;
  • the policy is looked up per company and falls back to the install's.

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally