Free tool Tier a model against OSFI E-23 in about five minutes — seven risk dimensions, the controls your tier requires, and a report you can print. Open it →
osfi-gate

Controls and how they are checked

The gate evaluates policy/rego/*.rego against one document, gate-input.json, which has three kinds of input:

Input Source Used by
findings[] Scanner output (SARIF or Trivy JSON) and osfi-gate's own pinning and licence checks, normalized and routed by mapping.yaml SD, VM, SC, CB, TP, OR
artifacts The SBOM (presence, digest, component count), changed model files, and AI call sites joined to model-inventory.yaml AM-01, MR-01 to MR-04
metadata Platform API: PR author, approvals on the head commit, commit authors and signatures, changed files, branch protection CM-01 to CM-05, WV-01

config (from mapping.yaml → gate), waivers[] and now are also recorded in the input, so replaying a decision later gives the same answer. artifacts.trivy_db records which vulnerability DB (its update time) the findings came from.

Which controls apply to which event

Event Change controls (CM-01, CM-02, CM-04, CM-05) Everything else
pull_request (also re-run on reviews) Yes Yes. The history scan covers the PR's commits (base..head).
push to a protected branch Yes; the merged PR is looked up from the commit Yes. The history scan covers the pushed commits (before..after).
schedule (daily) No; it isn't a change. CM-03 still checks branch protection. Yes. The history scan covers everything reachable from the default branch.
manual Yes, if metadata is supplied Yes. This is the only event on which a scanner may be skipped.

Semgrep rules

Severity routing: the first matching rule in mapping.yaml → rules sets each finding's control and, optionally, overrides its severity. A finding that matches no rule fails EV-01, so an unmapped scanner rule can't slip through unseen.

OSFI anchors: these are suggestions at the section level. Second-line risk owns and confirms them.

ID Control Suggested anchor Fails when
CM-01 Required approvals B-13 §2.5 Fewer than min_approvals independent approvals on the head commit. Stale approvals and approvals from the author or commit authors don't count.
CM-02 Segregation of duties B-13 §2.5 The author approved the PR, or an approver also authored commits in it
CM-03 Branch protection B-13 §2.5 Protection is missing or unreadable, or its required approvals are below min_approvals
CM-04 Signed commits B-13 §3.2 Any PR commit isn't verified by the platform
CM-05 Traceability B-13 §2.5 A push to a protected branch has no associated PR
SD-01 SAST B-13 §2.4 A Semgrep finding at a blocking severity (critical or high) isn't waived
VM-01 Vulnerability SLA B-13 §2.6, §3.1 A Trivy vulnerability is older than sla_days[severity] (measured from advisory publication), or its date is unknown at a blocking severity. The default branch is rescanned daily, so new advisories surface without a code change.
AM-01 SBOM B-13 §2.2 The CycloneDX SBOM is missing, unparseable or empty
SC-01 No secrets B-13 §3.2 Any Gitleaks finding in the current tree or in the change's commits (a secret added and then deleted still counts and must be rotated), any Checkov CKV_SECRET_* finding, or any Semgrep secret-rule finding, at any severity
SC-02 Approved crypto B-13 §3.2 An osfi-sc02-* finding: MD5, SHA-1, DES, RC4, Blowfish or ECB in Python, JS/TS, Java, Go or C#
CB-01 IaC baseline B-13 §2.1, §3.2 A baseline Checkov ID (encryption, public access, logging) or a Trivy/Semgrep IaC finding at a blocking severity. Other Checkov checks are medium and don't block.
TP-01 Licences B-10 An SBOM component's licence matches denied_licenses
TP-02 Pinning and provenance B-10, B-13 §2.4 Missing lockfile (npm, Python, Go, NuGet), unpinned requirement, floating Maven/NuGet version, base image without a digest, CI action without a SHA, or an unapproved registry
OR-01 Resilience E-21, B-13 §2.9 A criticality=critical resource without a dr-tier tag, a critical RDS instance that isn't Multi-AZ, or Checkov CKV_AWS_133/144/157
MR-01 Model artifacts E-23 A changed model file (model_globs) has no inventory ID or validation record in model-inventory.yaml
MR-02 AI usage registered E-23, B-10 Code calls an AI model (hosted or self-hosted) that isn't in model-inventory.yaml, or the entry has no validation record. Only usage the change introduces blocks; pre-existing usage warns.
MR-03 Provider and residency B-10, B-13 §1 The provider's hosting class isn't in approved_hosting, or the declared region isn't one the provider is approved for, or a region-bound provider has no declared region
MR-04 Model pinned to validated version E-23 The model is a floating alias (floating_model_patterns), or the model in the code differs from the one the inventory says was validated
MR-05 Materiality assessment present and current E-23 §3.2 A model record misses a risk-dimension rating, has no assessor, was assessed longer ago than assessment_max_age_days, or uses a control key or value outside the vocabulary
MR-06 Controls match materiality E-23 §5; HGAI P.4 A control the computed tier requires is not recorded as yes; generative model types additionally require the GenAI control set
MR-07 Independent validation current E-23 §5.3 No validation record, no validation date, or the next review date has passed
WV-01 Waiver governance B-13 §1 A waiver is invalid, has a term longer than max_waiver_days or a future created date, or a PR changes waivers/ without approval from risk_approvers
EV-01 Evidence completeness B-13 §1 A required scanner didn't complete (a skip is accepted only on manual runs), platform metadata errored or lacks a required field, a finding is unmapped, or the Trivy vulnerability DB metadata is missing or older than max_vuln_db_age_days

AI and model usage (MR-02 to MR-04)

What is detected. policy/semgrep/ai/ holds the osfi-mr02-<provider>-<language> rules: hosted APIs (OpenAI, Anthropic, Bedrock, Vertex, Azure OpenAI, Foundry, Gemini, Cohere, Mistral, Groq, Together, Replicate, Hugging Face), frameworks (LangChain, LlamaIndex), and self-hosted inference (transformers, ONNX Runtime, llama.cpp, vLLM, sentence-transformers, torch.load, joblib.load, Ollama). AI/ML packages in the SBOM are recorded alongside as artifacts.ai.dependencies.

The risk reading encoded in mapping.yaml. Detect every provider; approve Canadian-region cloud (Bedrock, Vertex, Azure OpenAI, Foundry) and self-hosted inference; treat a direct call to a provider outside that set as a risk acceptance that needs a waiver. Correct gate.ai if that is not the intent.

Provider resolution. A call goes to the provider its client implies: AnthropicBedrock(...).messages.create(...) is bedrock, not anthropic. The gate applies a cloud client found anywhere in the same file. A file mixing a cloud client and a direct call would be attributed to the cloud provider, so keep them in separate modules.

New versus pre-existing. enforce: new-usage blocks only what a change introduces. The same rules run against the change's base commit (exported with git archive, so fingerprints line up) and anything already present warns instead. Scheduled and manual runs have no base commit, so nothing counts as new. Set enforce: all once a repository is clean.

Waiving a call site. MR-02, MR-03 and MR-04 are raised about a call site rather than by a scanner finding, but the call site carries the fingerprint of the rule that detected it, so they are waived exactly like a finding: a waivers/*.yaml naming that fingerprint and that control, owned, justified, inside the maximum term and approved by risk. A waiver covers the one control it names — accepting a US-hosted provider under MR-03 does not excuse leaving the model out of the inventory under MR-02. Because the fingerprint comes from the rule and the path, it survives the code moving down the file.

Model versions. There is no positive "pinned" pattern: claude-opus-5 is a complete published ID, while gpt-4o is an alias for whatever OpenAI points it at. So MR-04 checks a risk-maintained deny list (floating_model_patterns, where a pattern may start or end with *) and requires the model in the code to equal the model the inventory says was validated. Where the model can't be read from the call site, the gate warns and takes the inventory entry on trust.

Registering usage (model-inventory.yaml in the application repo):

ai_usage:
  - path: ai/summarize.py          # exact path or glob
    provider: bedrock
    model: anthropic.claude-sonnet-4-5-20250929-v1:0
    region: ca-central-1
    inventory_id: MRM-0101
    validation_record: https://grc.bank.example/model-validation/MRM-0101/2026-08
    purpose: Summarises inbound customer letters for the service desk.

E-23 model records (MR-05 to MR-07)

A call site says where a model is used. A model record says what it is, how material it is, and which controls are in place. Records live in model_records: in the same model-inventory.yaml, and call sites reference them by inventory_id.

The structure follows the OSFI E-23 AI Model Risk Alignment Tool: model profile, seven risk dimensions, a materiality tier, a governance control inventory, and the GenAI supplement. The difference is that here it is enforced — an assessment that lives only in a PDF cannot fail a build.

The tier is recorded on every run. MR-05 writes the computed tier into the evidence bundle as a warning carrying materiality, score and max_score, so "how material is this model" is answered by the evidence rather than by anything downstream recomputing it. The console's assessment report reads exactly those fields.

The tier is recomputed, never trusted. The gate scores the seven dimensions (low 1, medium 2, high 3) and derives the tier from gate.ai.e23.tier: any of high_if_any rated high makes the model high materiality outright, otherwise the total decides. A record that writes materiality: low while rating automation high is enforced as high, and the gate says so in a warning. The ratings are the claim; the tier is the consequence.

What each tier must hold is required_controls in the policy, plus genai_required_controls for generative model types. Only yes satisfies a requirement: partial, no and not-applicable do not. A record cannot exempt itself — an exemption is a waiver, which is owned, justified and expiring. Both the key and the value are checked against a vocabulary (control_vocabulary, control_states), so a typo fails rather than becoming a silent gap that looks like a decision.

A note on yes. YAML reads bare yes/no as booleans, so the gate normalises true/false and any casing to the same vocabulary. Both documentation: yes and documentation: "yes" work.

model_records:
  - inventory_id: MRM-0101
    name: Customer Letter Summariser v1
    model_type: llm                  # llm, rag, multi-agent get the GenAI control set
    business_domain: member-services
    owner: Jane Doe, Head of Member Services Technology
    lifecycle_stage: production
    risk_dimensions:                 # each rated low / medium / high
      financial: low
      breadth: medium
      complexity: high
      automation: low
      data_sensitivity: medium
      third_party: high
      criticality: low
    materiality: medium              # recorded, but the gate recomputes it
    assessed_on: 2026-08-14
    assessed_by: Priya Raman, Model Risk
    governance_controls: {documentation: yes, monitoring: yes, ...}
    genai_controls: {hallucination_testing: yes, prompt_change_control: yes, ...}
    validation:
      record: https://grc.bank.example/model-validation/MRM-0101/2026-08
      validated_on: 2026-08-01
      next_review: 2027-08-01
    limitations: English only; performance on French letters has not been validated.

Fingerprints

Waivers match on a 16-hex-character fingerprint that is stable across unrelated edits:

Tool Fingerprint inputs
SARIF tools tool, rule ID, path, and the hash of the whitespace-normalized snippet
Checkov tool, rule ID, path, and the resource address
Trivy vulnerabilities vulnerability ID, package, installed version, and target
Gitleaks tool, rule ID, path, and line number (the snippet is the secret, so it's discarded). History findings also include the commit SHA.

Semgrep rules from the policy repo are reduced to their bare osfi-* ID, so fingerprints are the same locally and in CI.

Adding a check

  1. Add the rule to a scanner (for example a Semgrep rule in policy/semgrep/) or to osfi_gate/inspect.py.
  2. Route it in mapping.yaml → rules.
  3. If the control is new, add it to catalog/controls.yaml, run osfi-gate catalog, and add a deny rule plus a Rego unit test in rego/policy_test.rego.
  4. Run conftest verify and pytest.