The Problem
You've identified risks through the AIA (Lesson 1) and classified the system under the EU AI Act (Lesson 2). Now you need a living document that tracks each risk from identification through mitigation to closure — with an owner, a status, and evidence. This is the AI risk register, and it's the operational backbone of AI governance.
Risk register components
| Field | Description | Example |
|---|
| Risk ID | Unique identifier | RISK-001 |
| Description | What could go wrong | "Model discriminates against Group_B in loan approvals" |
| Category | Type of risk | Bias, Privacy, Security, Performance, Compliance |
| Likelihood | Probability of occurrence | Low, Medium, High |
| Severity | Impact if it occurs | Low, Medium, High, Critical |
| Inherent Risk Score | Likelihood × Severity (before mitigation) | 3×4 = 12 |
| Mitigation | What's being done about it | "Fairlearn bias audit + exponentiated gradient mitigation" |
| Residual Risk Score | Likelihood × Severity (after mitigation) | 1×2 = 2 |
| Owner | Who is responsible | "RAI Engineering Team" |
| Status | Current state | Open, In Progress, Mitigated, Closed |
| Evidence | Artifact proving mitigation | "bias_audit_report.json, mitigation_comparison.json" |
| Framework Mapping | NIST/EU AI Act/ISO references | "NIST MEASURE 2.7, EU AI Act Art. 10(2)(f)" |
Building the risk register tool
"""
ai_risk_register.py
AI risk register with scoring, ownership, and framework mapping.
"""
import json
from datetime import datetime
SEVERITY_SCORES = {"low": 1, "medium": 2, "high": 3, "critical": 4}
LIKELIHOOD_SCORES = {"low": 1, "medium": 2, "high": 3}
STATUS_LEVELS = ["open", "in_progress", "mitigated", "closed"]
class AIRiskRegister:
"""AI risk register for tracking and managing AI-related risks."""
def __init__(self, system_name):
self.system_name = system_name
self.risks = []
self.next_id = 1
def add_risk(self, description, category, likelihood, severity,
mitigation="", owner="", framework_refs=None):
"""Add a new risk to the register."""
risk = {
"id": f"RISK-{self.next_id:03d}",
"description": description,
"category": category,
"likelihood": likelihood,
"severity": severity,
"inherent_score": LIKELIHOOD_SCORES.get(likelihood, 1) * SEVERITY_SCORES.get(severity, 1),
"mitigation": mitigation,
"owner": owner,
"status": "open",
"evidence": [],
"framework_refs": framework_refs or [],
"created_date": datetime.now().isoformat(),
"updated_date": datetime.now().isoformat(),
}
# Calculate residual score (simplified: mitigation reduces likelihood by 1 level)
if mitigation:
reduced_likelihood = max(1, LIKELIHOOD_SCORES.get(likelihood, 1) - 1)
risk["residual_score"] = reduced_likelihood * SEVERITY_SCORES.get(severity, 1)
risk["status"] = "in_progress"
else:
risk["residual_score"] = risk["inherent_score"]
self.risks.append(risk)
self.next_id += 1
return risk
def update_risk(self, risk_id, status=None, mitigation=None, evidence=None):
"""Update an existing risk entry."""
for risk in self.risks:
if risk["id"] == risk_id:
if status:
risk["status"] = status
if mitigation:
risk["mitigation"] = mitigation
reduced_likelihood = max(1, LIKELIHOOD_SCORES.get(risk["likelihood"], 1) - 1)
risk["residual_score"] = reduced_likelihood * SEVERITY_SCORES.get(risk["severity"], 1)
if evidence:
risk["evidence"].extend(evidence)
risk["updated_date"] = datetime.now().isoformat()
return risk
return None
def get_summary(self):
"""Get a summary of the risk register."""
by_status = {}
by_category = {}
total_inherent = 0
total_residual = 0
for risk in self.risks:
by_status[risk["status"]] = by_status.get(risk["status"], 0) + 1
by_category[risk["category"]] = by_category.get(risk["category"], 0) + 1
total_inherent += risk["inherent_score"]
total_residual += risk["residual_score"]
return {
"system_name": self.system_name,
"total_risks": len(self.risks),
"by_status": by_status,
"by_category": by_category,
"total_inherent_score": total_inherent,
"total_residual_score": total_residual,
"risk_reduction_pct": round((1 - total_residual / total_inherent) * 100, 1) if total_inherent > 0 else 0,
}
def to_json(self, filename):
"""Export the risk register to JSON."""
data = {
"system_name": self.system_name,
"export_date": datetime.now().isoformat(),
"risks": self.risks,
"summary": self.get_summary(),
}
with open(filename, "w") as f:
json.dump(data, f, indent=2)
return filename
def to_markdown(self, filename):
"""Export the risk register to Markdown."""
summary = self.get_summary()
lines = []
lines.append(f"# AI Risk Register — {self.system_name}\n")
lines.append(f"**Total risks:** {summary['total_risks']}")
lines.append(f"**Risk reduction:** {summary['risk_reduction_pct']}% (inherent → residual)\n")
lines.append("## Summary by Status\n")
for status, count in summary["by_status"].items():
lines.append(f"- {status}: {count}")
lines.append("")
lines.append("## Summary by Category\n")
for cat, count in summary["by_category"].items():
lines.append(f"- {cat}: {count}")
lines.append("")
lines.append("## Risk Register\n")
lines.append("| ID | Description | Category | Likelihood | Severity | Inherent | Mitigation | Residual | Owner | Status |")
lines.append("|---|---|---|---|---|---|---|---|---|---|")
for risk in self.risks:
lines.append(f"| {risk['id']} | {risk['description'][:60]} | {risk['category']} | "
f"{risk['likelihood']} | {risk['severity']} | {risk['inherent_score']} | "
f"{risk['mitigation'][:40]} | {risk['residual_score']} | "
f"{risk['owner']} | {risk['status']} |")
lines.append("")
lines.append("## Framework Mapping\n")
lines.append("| Risk ID | Framework References |")
lines.append("|---|---|")
for risk in self.risks:
if risk["framework_refs"]:
lines.append(f"| {risk['id']} | {', '.join(risk['framework_refs'])} |")
lines.append("")
lines.append("## Evidence Artifacts\n")
for risk in self.risks:
if risk["evidence"]:
lines.append(f"### {risk['id']}: {risk['description'][:50]}")
for ev in risk["evidence"]:
lines.append(f"- {ev}")
lines.append("")
with open(filename, "w") as f:
f.write("\n".join(lines))
return filename
if __name__ == "__main__":
# Example: Risk register for a loan approval system
register = AIRiskRegister("LoanApproval-GBM-v1")
register.add_risk(
description="Model discriminates against Group_B in loan approval decisions",
category="Bias",
likelihood="high",
severity="high",
mitigation="Fairlearn bias audit + exponentiated gradient mitigation (Phase 2)",
owner="RAI Engineering Team",
framework_refs=["NIST MEASURE 2.7", "EU AI Act Art. 10(2)(f)", "ISO 42001 Clause 9.1"],
)
register.add_risk(
description="Membership inference attack could reveal loan application status",
category="Privacy",
likelihood="medium",
severity="medium",
mitigation="Model regularization (max_depth=5), differential privacy planned for v2",
owner="Privacy Engineering Team",
framework_refs=["GDPR Art. 35", "EU AI Act Art. 10", "NIST MEASURE 2.9"],
)
register.add_risk(
description="Model performance degrades over time due to economic drift",
category="Performance",
likelihood="medium",
severity="high",
mitigation="Post-deployment monitoring with drift detection (Lesson 5)",
owner="MLOps Team",
framework_refs=["NIST MANAGE 4.1", "EU AI Act Art. 15", "ISO 42001 Clause 9.1"],
)
register.add_risk(
description="SHAP explanations may be insufficient for regulatory audit",
category="Compliance",
likelihood="low",
severity="high",
mitigation="LIME cross-validation + model card + transparency report (Phase 3)",
owner="RAI Engineering Team",
framework_refs=["EU AI Act Art. 13", "GDPR Art. 22", "NIST MEASURE 2.11"],
)
register.add_risk(
description="Training data contains PII that could be exposed in a breach",
category="Security",
likelihood="low",
severity="critical",
mitigation="Encryption at rest and in transit, access controls, audit logging",
owner="Security Team",
framework_refs=["GDPR Art. 32", "EU AI Act Art. 15", "NIST GOVERN 2.2"],
)
# Update a risk with evidence
register.update_risk("RISK-001", status="mitigated",
evidence=["bias_audit_report.json", "mitigation_comparison.json"])
# Export
register.to_json("ai_risk_register.json")
register.to_markdown("ai_risk_register.md")
summary = register.get_summary()
print(f"Total risks: {summary['total_risks']}")
print(f"Risk reduction: {summary['risk_reduction_pct']}%")
print(f"By status: {summary['by_status']}")
print(f"By category: {summary['by_category']}")
print("\nRegister saved to ai_risk_register.md and ai_risk_register.json")
Your risk register shows RISK-003 ("model performance degrades over time due to economic drift") with an inherent score of 6 (likelihood=medium=2 × severity=high=3) and no mitigation recorded yet, so its residual score is also 6. Your manager says "residual score equals inherent score, so nothing's been done — mark it red and escalate immediately." Is this the right read of the register?
The register's add_risk method only lowers the residual score below the inherent score once a mitigation string is actually passed in — that's intentional, because a risk with no recorded mitigation should visibly show "residual = inherent" so nobody mistakes an unaddressed risk for one that's been handled. But "unmitigated" and "must be escalated as the top priority" are two different judgments. The right response is: use the inherent score to prioritize (a 6 out of 12 needs attention sooner than a 2, but later than a 12), assign an owner, and update the register once a real mitigation is in place — not treat every unmitigated entry as equally urgent regardless of its actual likelihood × severity score.