From 6c164877e6f9e98cafea20655fdd6ba2bc051beb Mon Sep 17 00:00:00 2001 From: sonupreetam Date: Wed, 29 Apr 2026 11:47:16 +0200 Subject: [PATCH 1/2] feat(riskcatalog): optional rank for per-risk ordering (#339) - Add optional rank (int) on #Risk; lower = higher importance - Enforce unique rank values among risks that set rank - Extend fixtures and add duplicate-rank negative test Made-with: Cursor Signed-off-by: sonupreetam --- riskcatalog.cue | 8 ++++ test/schema_test.go | 3 ++ .../bad-risk-catalog-duplicate-rank.yaml | 37 +++++++++++++++++++ test/test-data/good-risk-catalog.yaml | 9 +++++ 4 files changed, 57 insertions(+) create mode 100644 test/test-data/bad-risk-catalog-duplicate-rank.yaml diff --git a/riskcatalog.cue b/riskcatalog.cue index a762b55d..c829211c 100644 --- a/riskcatalog.cue +++ b/riskcatalog.cue @@ -21,6 +21,9 @@ import "list" if risks != _|_ { _uniqueRiskIds: {for i, r in risks {(r.id): i}} + // Each distinct rank value may appear at most once among risks that set rank (partial ranking allowed). + // Keys are stringified ranks because CUE struct labels cannot be raw integers. + _uniqueRiskRanks: {for i, r in risks if r.rank != _|_ {"\(r.rank)": i}} groups: [#RiskCategory, ...#RiskCategory] let _validGroupIds = [for g in groups {g.id}] @@ -82,6 +85,11 @@ import "list" // severity describes the assessed level of this risk severity: #Severity @go(Severity) + // rank optionally orders risks for the same catalog (e.g. when several share the same severity). + // Lower values mean higher relative importance. Omitted when the four severity levels are enough. + // When set, each value must be unique among all risks in the catalog that specify rank. + rank?: int @go(Rank) @yaml("rank,omitempty") + // owner defines the RACI roles responsible for managing this risk owner?: #RACI @go(Owner) diff --git a/test/schema_test.go b/test/schema_test.go index 9c56fcc3..e80df0b4 100644 --- a/test/schema_test.go +++ b/test/schema_test.go @@ -72,6 +72,9 @@ func TestSchemaValidation(t *testing.T) { // RiskCatalog — positive {"valid risk catalog", "./test-data/good-risk-catalog.yaml", "#RiskCatalog", false, ""}, + // RiskCatalog — negative + {"risk catalog with duplicate rank", "./test-data/bad-risk-catalog-duplicate-rank.yaml", "#RiskCatalog", true, ""}, + // Policy — positive {"valid policy", "./test-data/good-policy.yaml", "#Policy", false, ""}, {"valid security policy", "./test-data/good-security-policy.yml", "#Policy", false, ""}, diff --git a/test/test-data/bad-risk-catalog-duplicate-rank.yaml b/test/test-data/bad-risk-catalog-duplicate-rank.yaml new file mode 100644 index 00000000..d4af8b8e --- /dev/null +++ b/test/test-data/bad-risk-catalog-duplicate-rank.yaml @@ -0,0 +1,37 @@ +metadata: + id: BAD-RISK-CATALOG-DUP-RANK + type: RiskCatalog + gemara-version: "0.20.0" + version: "1.0.0" + description: Invalid — two risks use the same rank + author: + id: test + name: Test + type: Human + mapping-references: + - id: EXAMPLE-THREAT-CATALOG + title: Example + version: "1.0.0" + description: x + +title: Invalid duplicate rank + +groups: + - id: CAT-SECURITY + title: Security + description: x + appetite: Low + +risks: + - id: RISK-A + title: A + description: x + group: CAT-SECURITY + severity: High + rank: 1 + - id: RISK-B + title: B + description: x + group: CAT-SECURITY + severity: High + rank: 1 diff --git a/test/test-data/good-risk-catalog.yaml b/test/test-data/good-risk-catalog.yaml index 492b3e86..5adbaef5 100644 --- a/test/test-data/good-risk-catalog.yaml +++ b/test/test-data/good-risk-catalog.yaml @@ -37,6 +37,7 @@ risks: description: Third-party or base container images may contain known vulnerabilities or malicious code, leading to exploitation at runtime. group: CAT-SECURITY severity: High + rank: 2 owner: responsible: - name: Platform Engineering Lead @@ -81,3 +82,11 @@ risks: group: CAT-COMPLIANCE severity: Medium impact: Regulatory fines, audit findings, and reputational damage. + + - id: RISK-004 + title: Cluster Admin Credential Exposure + description: Long-lived admin credentials for the cluster control plane may be exposed through logs, tickets, or shared stores. + group: CAT-SECURITY + severity: High + rank: 1 + impact: Full cluster compromise; all workloads and secrets on the platform are at risk. From fbce1de4040a6889d56a2e0b1466b1f072b547d3 Mon Sep 17 00:00:00 2001 From: sonupreetam Date: Wed, 29 Apr 2026 11:50:19 +0200 Subject: [PATCH 2/2] docs: document optional risk rank (#339) - Risk Catalog Guide: rank field, examples, validation note - risk-catalog-example.yaml: ranks for two High risks - Tutorials index: mention optional rank Made-with: Cursor Signed-off-by: sonupreetam --- docs/tutorials/index.md | 4 ++-- docs/tutorials/policy/risk-catalog-example.yaml | 2 ++ docs/tutorials/policy/risk-catalog-guide.md | 15 ++++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index 55209b3b..5f4031b7 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -45,7 +45,7 @@ Create a policy document that translates risk appetite into mandatory rules — ### Creating a risk catalog -When you need a structured inventory of organizational or system risks—**risk categories** (appetite, optional max-severity), per-risk **severity**, optional RACI **owner** and **impact**, and optional **threats** links backed by `metadata.mapping-references`—so policies can reference mitigated or accepted risks → [Risk Catalog Guide](policy/risk-catalog-guide) (Layer 3). +When you need a structured inventory of organizational or system risks—**risk categories** (appetite, optional max-severity), per-risk **severity**, optional **`rank`** for ordering within the catalog, optional RACI **owner** and **impact**, and optional **threats** links backed by `metadata.mapping-references`—so policies can reference mitigated or accepted risks → [Risk Catalog Guide](policy/risk-catalog-guide) (Layer 3). ## What You'll Build @@ -54,7 +54,7 @@ When you need a structured inventory of organizational or system risks—**risk | **Layer 1** — Guidance | Guidance Catalog (guidelines, groups, mapping-references); [Principle Catalog](https://gemara.openssf.org/schema/principlecatalog.html) (principles, groups) | [Guidance Catalog Guide](guidance/guidance-guide) | | **Layer 2** — Controls | Threat Catalog + Control Catalog (assessment requirements, threats) | [Threat Assessment](controls/threat-assessment-guide), [Control Catalog](controls/control-catalog-guide) | | **Layer 3** — Policy | Policy Document (scope, imports, adherence) | [Policy Guide](policy/policy-guide) | -| **Layer 3** — Risks | Risk Catalog (risk categories, appetite, risks, optional threat mappings) | [Risk Catalog Guide](policy/risk-catalog-guide) | +| **Layer 3** — Risks | Risk Catalog (risk categories, appetite, risks, optional rank and threat mappings) | [Risk Catalog Guide](policy/risk-catalog-guide) | ## What You'll Need diff --git a/docs/tutorials/policy/risk-catalog-example.yaml b/docs/tutorials/policy/risk-catalog-example.yaml index ab2e378f..6824ea65 100644 --- a/docs/tutorials/policy/risk-catalog-example.yaml +++ b/docs/tutorials/policy/risk-catalog-example.yaml @@ -44,6 +44,7 @@ risks: description: "Mutable image tags or lack of verification can lead to pulling stale or compromised images, increasing supply chain and runtime risk." group: "infrastructure" severity: "High" + rank: 2 impact: "Supply chain compromise, unauthorized code execution, or data exfiltration." owner: responsible: @@ -62,6 +63,7 @@ risks: description: "Images may be tampered with in transit or at rest, or built from poisoned dependencies or build pipelines." group: "infrastructure" severity: "High" + rank: 1 threats: - reference-id: CCC entries: diff --git a/docs/tutorials/policy/risk-catalog-guide.md b/docs/tutorials/policy/risk-catalog-guide.md index 9e5f9051..ff44fb95 100644 --- a/docs/tutorials/policy/risk-catalog-guide.md +++ b/docs/tutorials/policy/risk-catalog-guide.md @@ -8,13 +8,13 @@ description: Step-by-step guide to creating Gemara-compatible risk catalogs This guide walks through creating a **Risk Catalog** using the [Gemara](https://gemara.openssf.org/) project. The document conforms to `#RiskCatalog` in [riskcatalog.cue](https://github.com/gemaraproj/gemara/blob/main/riskcatalog.cue) and the published [Risk Catalog schema](https://gemara.openssf.org/schema/riskcatalog.html). Examples use `gemara-version: "1.0.0"` to match the [v1.0.0](https://github.com/gemaraproj/gemara/releases/tag/v1.0.0) specification release; adjust if you target a different Gemara version. -**The basic idea:** A Risk Catalog is a structured list of **[risks](../../model/02-definitions.html#risk)** that might affect an organization, system, or service. You organize them into **groups** (schema type `#RiskCategory`) that express how much risk you are willing to carry ( risk appetite) and optionally cap how bad a single risk in that group can be (**max-severity**). Each risk has an assessed **severity** and can point to Layer 2 **[threats](../../model/02-definitions.html#threat)** so mitigations and policies stay traceable to threat catalogs. +**The basic idea:** A Risk Catalog is a structured list of **[risks](../../model/02-definitions.html#risk)** that might affect an organization, system, or service. You organize them into **groups** (schema type `#RiskCategory`) that express how much risk you are willing to carry ( risk appetite) and optionally cap how bad a single risk in that group can be (**max-severity**). Each risk has an assessed **severity** and can point to Layer 2 **[threats](../../model/02-definitions.html#threat)** so mitigations and policies stay traceable to threat catalogs. Optionally, **`rank`** records a total order among risks in this catalog (for example when several share the same severity); see Step 2. In technical terms: -* **[Risk catalogs](../../model/02-definitions.html#risk-catalog)** declare `metadata` (including `type: RiskCatalog`), **groups** as `#RiskCategory` (id, title, description, `#RiskAppetite` appetite, optional `#Severity` max-severity), and **[risks](../../model/02-definitions.html#risk)** (`#Risk`) with required id, title, description, `group`, and `severity`, plus optional owner (`#RACI`), impact, and `threats` (`#MultiEntryMapping` from [mapping_inline.cue](https://github.com/gemaraproj/gemara/blob/main/mapping_inline.cue). +* **[Risk catalogs](../../model/02-definitions.html#risk-catalog)** declare `metadata` (including `type: RiskCatalog`), **groups** as `#RiskCategory` (id, title, description, `#RiskAppetite` appetite, optional `#Severity` max-severity), and **[risks](../../model/02-definitions.html#risk)** (`#Risk`) with required id, title, description, `group`, and `severity`, plus optional **`rank`**, owner (`#RACI`), impact, and `threats` (`#MultiEntryMapping` from [mapping_inline.cue](https://github.com/gemaraproj/gemara/blob/main/mapping_inline.cue). * **When the catalog lists any risks**, the schema requires at least one group (`groups` becomes required in that case). * **Groups** use appetite values `Minimal`, `Low`, `Moderate`, or `High` and optional group-level **max-severity** (`Low`, `Medium`, `High`, `Critical`). -* **Risks** use **severity** at the same scale (`Low` through `Critical`). Links under `threats` use the same multi-entry mapping pattern as control and threat catalogs; every outer `reference-id` must match an id in `metadata.mapping-references`. +* **Risks** use **severity** at the same scale (`Low` through `Critical`). Optional **`rank`** is an integer; **lower values mean higher relative importance** among risks that set `rank`. Each `rank` value must be **unique** among risks that specify it (partial ranking is allowed: risks without `rank` are unconstrained). Links under `threats` use the same multi-entry mapping pattern as control and threat catalogs; every outer `reference-id` must match an id in `metadata.mapping-references`. This exercise produces a risk catalog that you can reference from a [Policy](policy-guide) when documenting mitigated and accepted risks. @@ -105,6 +105,7 @@ Define **[risks](../../model/02-definitions.html#risk)** with required fields an | `description` | Yes | Explains the risk scenario and context | | `group` | Yes | Id of a group in this catalog (from Step 1) | | `severity` | Yes | Impact level: `Low`, `Medium`, `High`, or `Critical` | +| `rank` | No | Optional ordering within the catalog: integer; **lower = more important**. Unique among risks that set `rank`. Omit when severity alone is enough. | | `owner` | No | RACI roles (responsible, accountable, consulted, informed) for this risk | | `impact` | No | Business or operational impact description | | `threats` | No | Links to Layer 2 threat entries via reference-id and entries (see below) | @@ -120,6 +121,7 @@ risks: description: "Mutable image tags or lack of verification can lead to pulling stale or compromised images, increasing supply chain and runtime risk." group: "infrastructure" severity: "High" + rank: 2 impact: "Supply chain compromise, unauthorized code execution, or data exfiltration." owner: responsible: @@ -138,6 +140,7 @@ risks: description: "Images may be tampered with in transit or at rest, or built from poisoned dependencies or build pipelines." group: "infrastructure" severity: "High" + rank: 1 threats: - reference-id: CCC entries: @@ -169,12 +172,12 @@ From a **clone of this repository** (uses the working tree’s CUE package, incl cue vet -c -d '#RiskCatalog' . docs/tutorials/policy/risk-catalog-example.yaml ``` -Fix any errors (e.g. missing required fields, risks present without `groups`, invalid `group` reference, severity or appetite not in allowed enums, or a `threats` outer `reference-id` not listed in `metadata.mapping-references`) so the catalog is schema-valid. +Fix any errors (e.g. missing required fields, risks present without `groups`, invalid `group` reference, severity or appetite not in allowed enums, **duplicate `rank` values** among risks that set `rank`, or a `threats` outer `reference-id` not listed in `metadata.mapping-references`) so the catalog is schema-valid. ## Minimal Full Example -A complete, schema-valid example is in [risk-catalog-example.yaml](risk-catalog-example.yaml) in this directory. The following matches that file (omit optional sections such as `owner`, `impact`, or `threats` in your own catalogs if you do not need them). Both align with `#RiskCatalog` in [riskcatalog.cue](https://github.com/gemaraproj/gemara/blob/main/riskcatalog.cue). +A complete, schema-valid example is in [risk-catalog-example.yaml](risk-catalog-example.yaml) in this directory. The following matches that file (omit optional sections such as `rank`, `owner`, `impact`, or `threats` in your own catalogs if you do not need them). Both align with `#RiskCatalog` in [riskcatalog.cue](https://github.com/gemaraproj/gemara/blob/main/riskcatalog.cue). ```yaml # Organization Risk Catalog for Cloud and Container Workloads (Layer 3) @@ -223,6 +226,7 @@ risks: description: "Mutable image tags or lack of verification can lead to pulling stale or compromised images, increasing supply chain and runtime risk." group: "infrastructure" severity: "High" + rank: 2 impact: "Supply chain compromise, unauthorized code execution, or data exfiltration." owner: responsible: @@ -241,6 +245,7 @@ risks: description: "Images may be tampered with in transit or at rest, or built from poisoned dependencies or build pipelines." group: "infrastructure" severity: "High" + rank: 1 threats: - reference-id: CCC entries: