Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ assets/css/*.css.map
schema/*.md
!schema/index.md
model/02-definitions.md

# IDE
.vscode
.idea
55 changes: 55 additions & 0 deletions tutorials/capabilities/capability-extension-example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Example CapabilityCatalog extended with a community Kubernetes extension.
# See capability-extensions-guide.md. Validates against #KubernetesCapabilityCatalog
# only (your extension module) — does NOT validate against base #CapabilityCatalog.

title: Acme Kubernetes Capability Catalog

metadata:
id: ACME-K8S
type: CapabilityCatalog
gemara-version: "1.2.0"
description: Kubernetes workload capabilities for the Acme platform.
version: 0.1.0
author:
id: acme-platform
name: Acme Platform Team
type: Human

groups:
- id: workloads
title: Workloads
description: |
Capabilities related to pod and deployment management.
- id: secrets
title: Secrets
description: |
Capabilities related to secret and config management.

capabilities:
- id: CAP-K8S-001
title: Create deployments
description: |
The cluster can create deployment resources in application namespaces.
group: workloads
api-group: apps
api-resource: deployments
verb: create
namespaced: true
- id: CAP-K8S-002
title: List pods
description: |
The cluster can list running pods across namespaces.
group: workloads
api-group: ""
api-resource: pods
verb: list
namespaced: true
- id: CAP-K8S-003
title: Read secrets
description: |
The cluster can read secret values in application namespaces.
group: secrets
api-group: ""
api-resource: secrets
verb: get
namespaced: true
146 changes: 146 additions & 0 deletions tutorials/capabilities/capability-extensions-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
layout: page
title: Capability Extensions Guide
description: Step-by-step guide to extending Gemara with CUE
---

## What This Is

This guide shows how to write a **Capability Extension**: a domain-specific extension of Gemara's `#Capability` as its own CUE type.

**The basic idea:** `#Capability` is deliberately neutral — `id`, `title`, `description`, `group`. It stays that way. If your domain needs more, you define those fields in your own CUE module and unify them with `gemara.#Capability` rather than asking Gemara to add them to core.

This is the **extend** direction. If instead you need to *restrict* the base type, a profile or baseline that tightens what values are allowed without adding fields. See [Extending vs Constraining Gemara](extending-vs-constraining), which explains when to reach for each and how they differ.

## Walkthrough

### Step 1: Create Your Extension Module

Extensions live in their own CUE module. Create a directory and initialize it:

```sh
mkdir acme-k8s && cd acme-k8s
cue mod init acme.example/k8s@v0
```

Add Gemara as a dependency in `cue.mod/module.cue`, then let CUE resolve it:

```cue
module: "acme.example/k8s@v0"
language: version: "v0.17.0"
deps: {
"github.com/gemaraproj/gemara@v1": v: "v1.2.0"
}
```

```sh
cue mod tidy
```

### Step 2: Define Your Extension Type

Define a new type embedding `#Capability`:

```cue
package kubernetes

import gemara "github.com/gemaraproj/gemara@v1"

#KubernetesCapability: {
gemara.#Capability
"api-group"?: string
"api-resource": string
verb: "get" | "list" | "watch" | "create" | "update" | "patch" | "delete" | "deletecollection"
namespaced: bool
}

#KubernetesCapabilityCatalog: {
gemara.#CapabilityCatalog
capabilities: [#KubernetesCapability, ...#KubernetesCapability]
}
```

`gemara.#Capability` is embedded in `#KubernetesCapability` and unified with that struct so it has every `Capability` base field plus the additions. Because it keeps every base field, an extended catalog is a **superset** of a base catalog (see [the concept guide](extending-vs-constraining) for what that buys you).

### Step 3: Author Your Catalog

Write a `CapabilityCatalog` document as you normally would, adding your extension's fields alongside the base ones:

```yaml
title: Acme Kubernetes Capability Catalog

metadata:
id: ACME-K8S
type: CapabilityCatalog
gemara-version: "1.2.0"
version: "0.1.0"
description: Kubernetes workload capabilities for the Acme platform.
author:
id: acme-platform
name: Acme Platform Team
type: Human

groups:
- id: workloads
title: Workloads
description: Capabilities related to pod and deployment management.

capabilities:
- id: CAP-K8S-001
title: Create deployments
description: The cluster can create deployment resources in application namespaces.
group: workloads
api-group: apps
api-resource: deployments
verb: create
namespaced: true
```

See [`capability-extension-example.yaml`](capability-extension-example.yaml) for the full file.

### Step 4: Validate Against Your Extension

From inside your module directory, vet the document against your extension definition:

```sh
cue vet -c -d '#KubernetesCapabilityCatalog' . capability-extension-example.yaml
```

Validate against the **extension** definition you authored, not base `#CapabilityCatalog`. Vetting an extended document against base fails — base `#Capability` is a closed definition, so the extra fields are rejected (`field not allowed`). That is expected: the contract your document conforms to is your extension type. Base consumers still read the document fine (see below).

## Consuming an Extended Catalog in Go

Gemara generates Go types from its CUE schema, and your extension module does the same. This is what makes an extension a superset rather than a fork: the same document is readable by both extension-aware and generic base tooling.

**Extension-aware consumer** — embeds the generated base struct and adds the typed fields:

```go
type KubernetesCapability struct {
gemara.Capability // embedded base type
APIGroup *string `json:"api-group,omitempty"`
APIResource string `json:"api-resource"`
Verb string `json:"verb"`
Namespaced bool `json:"namespaced"`
}
```

Unmarshalling populates both the base fields (via the embed) and your extensions.

**Generic base consumer** — a tool that only knows `gemara.Capability` unmarshals the same document and reads the base fields. Go's `encoding/json` silently ignores unknown fields by default, so the extension attributes are dropped, no error:

```go
var c gemara.Capability
json.Unmarshal(doc, &c) // id, title, description, group populated; extras ignored
```

**One caveat:** a consumer that opts into strict decoding with `Decoder.DisallowUnknownFields()` **will** error on the extension fields (`json: unknown field "api-group"`). If you distribute a base-only tool meant to read extended catalogs, do not enable strict unknown-field rejection.

## Publishing and Discovery

Publish your extension module like any CUE module (to the public CUE registry or your own) and open a PR on [`awesome-gemara`](https://github.com/gemaraproj/awesome-gemara) to add it to the index. Today that index is how extensions are shared and discovered; see [Extending vs Constraining Gemara](extending-vs-constraining) for how sanctioned support is expected to evolve.

## Have Ideas?

- Reach out via Slack in `#gemara`
- Discuss in one of our bi-weekly meetings on the [OpenSSF calendar](https://calendar.google.com/calendar/u/0?cid=czYzdm9lZmhwNWk5cGZsdGI1cTY3bmdwZXNAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ)
- Open a [GitHub Issue](https://github.com/gemaraproj/gemara/issues)
73 changes: 73 additions & 0 deletions tutorials/capabilities/extending-vs-constraining.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
layout: page
title: Extending vs Constraining Gemara
description: How to specialize Gemara types
---

## Two Ways to Specialize a Gemara Type

Gemara's core types are deliberately neutral. When a domain needs something the base type does not give you, there are two distinct moves depending on the use case.

| | **Extend** | **Constrain** |
|--------------------------|-----------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| Goal | Add domain-specific fields | Restrict the values the base already allows |
| CUE mechanism | [Embed](https://cuelang.org/docs/reference/spec/#embedding) the base type (widen) | [Unify](https://cuelang.org/docs/reference/spec/#unification) with `&` (narrow) |
| Fields | Base fields **plus** new ones | Base fields only |
| Valid against base type? | No — extra fields are rejected by the closed base definition | Yes — every constrained document is still a valid base document |
| Use for | New capability attributes (K8s API resources, kernel capabilities) | Profiles and baselines that tighten an existing catalog |

Pick **extend** when your domain has attributes the base type simply doesn't model. Pick **constrain** when the base type already fits, and you only need to require, restrict, or shape its values.

### Extend

Embed the base type in your own definition and add fields. Full walkthrough — module setup, authoring, validation, and Go consumption — in the [Capability Extensions Guide](capability-extensions-guide):

```cue
#KubernetesCapability: {
gemara.#Capability
"api-resource": string
verb: "get" | "list" | "watch" | "create" | "update" | "patch" | "delete" | "deletecollection"
namespaced: bool
}
```

### Constrain

Unify the base type with tighter rules using `&`. You add no fields, so the result is still a valid instance of the base type. This is how the [OSSF Security Baseline](https://github.com/ossf/security-baseline/tree/main/schema) layers OSPS-specific requirements onto a Gemara `#ControlCatalog`:

```cue
// #OSPSBaseline layers OSPS-specific constraints on top of the Gemara #ControlCatalog.
#OSPSBaseline: gemara.#ControlCatalog & {
let _agID = =~"^maturity-"

metadata: "applicability-groups": [{id: _agID}, ...{id: _agID}]
}
```

Every `#OSPSBaseline` document is, by construction, a valid `#ControlCatalog` — the constraint only narrows what is allowed. Generic Gemara tooling reads it with no special knowledge.

## Superset, Not Fork

An extension embeds every base field, so an extended document is a **superset** of a base document, not a divergent one. That distinction matters in practice:

- **Extension-aware tools** read the full typed shape — base fields plus your additions.
- **Generic base tools** read the base fields and ignore the rest. In Go, `encoding/json` drops unknown fields by default, so a base-only consumer reads an extended catalog with no error. (The one exception is strict decoding via `DisallowUnknownFields` — see the [extensions guide](capability-extensions-guide#consuming-an-extended-catalog-in-go).)

So an extension is not a fork of Gemara. It is Gemara plus typed, validated, domain-specific fields — making it shareable and verifiable.

## The Intended Path

Gemara extensions do not yet have: a **dedicated extension point** the core is aware of or tooling support. Gemara does not support a property bag solution because they lack discoverability, are difficult to verify, and create subtle fragmentation.

In Gemara today, the base type does not know that a given document is a maintained extension of it. It only sees a superset it happens to be able to read. This is intentional, for now. The plan is to let the community pattern prove itself before formalizing it:

1. **Today** — write extensions with the embed pattern and publish them to [`awesome-gemara`](https://github.com/gemaraproj/awesome-gemara). That index is the de-facto registry: how extensions are shared, discovered, and versioned.
2. **Next** — as real extensions reach critical mass and their shapes stabilize, Gemara adds formal, core-aware support so an extension can declare itself a sanctioned extension of a base type rather than an undeclared superset.

Building the extension pattern this way — typed, published, and versioned from the start — means today's extensions are already shaped for that future support.

## Have Ideas?

- Reach out via Slack in `#gemara`
- Discuss in one of our bi-weekly meetings on the [OpenSSF calendar](https://calendar.google.com/calendar/u/0?cid=czYzdm9lZmhwNWk5cGZsdGI1cTY3bmdwZXNAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbQ)
- Open a [GitHub Issue](https://github.com/gemaraproj/gemara/issues)
5 changes: 5 additions & 0 deletions tutorials/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ Create a policy document that translates risk appetite into mandatory rules —

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).

### Working with Gemara CUE types

Specialize Gemara types using CUE tooling — start with [Extending vs Constraining Gemara](capabilities/extending-vs-constraining) to choose between adding fields (extend) and restricting values (constrain), then follow the [Capability Extensions Guide](capabilities/capability-extensions-guide) for the full embedding walkthrough, including Go consumption.

## What You'll Build

| Layer | Artifact | Guide |
Expand All @@ -60,6 +64,7 @@ When you need a structured inventory of organizational or system risks—**risk
| **Layer 3** — Policy | Policy Document (scope, imports, adherence) | [Policy Guide](policy/policy-guide) |
| **Layer 3** — Risks | Risk Catalog (risk categories, appetite, risks, optional rank and threat mappings) | [Risk Catalog Guide](policy/risk-catalog-guide) |
| **Cross-artifact** | Mapping Document (typed `source`/`target` references, `targets` per mapping, relationship types; entry types per schema include guidelines, controls, Principle, threats, risks, and others) | [Mapping Document Guide](mapping/mapping-document-guide) |
| **Community pattern** | Capability Extension (domain-specific extension of `#Capability` via CUE embedding; superset of the base catalog, read by base and extension-aware tooling alike) | [Extending vs Constraining](capabilities/extending-vs-constraining), [Capability Extensions Guide](capabilities/capability-extensions-guide) |

## What You'll Need

Expand Down
Loading