diff --git a/.gitignore b/.gitignore index 56fc1ea..8ff1d8f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ assets/css/*.css.map schema/*.md !schema/index.md model/02-definitions.md + +# IDE +.vscode +.idea \ No newline at end of file diff --git a/tutorials/capabilities/capability-extension-example.yaml b/tutorials/capabilities/capability-extension-example.yaml new file mode 100644 index 0000000..54eee98 --- /dev/null +++ b/tutorials/capabilities/capability-extension-example.yaml @@ -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 diff --git a/tutorials/capabilities/capability-extensions-guide.md b/tutorials/capabilities/capability-extensions-guide.md new file mode 100644 index 0000000..deb1920 --- /dev/null +++ b/tutorials/capabilities/capability-extensions-guide.md @@ -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) diff --git a/tutorials/capabilities/extending-vs-constraining.md b/tutorials/capabilities/extending-vs-constraining.md new file mode 100644 index 0000000..ee1beb5 --- /dev/null +++ b/tutorials/capabilities/extending-vs-constraining.md @@ -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) diff --git a/tutorials/index.md b/tutorials/index.md index c1ece52..20e9341 100644 --- a/tutorials/index.md +++ b/tutorials/index.md @@ -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 | @@ -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