From b7ea5c956b46e8fa79af6decd7988c0a2ffa5d90 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Tue, 26 May 2026 01:07:52 +0200 Subject: [PATCH 1/2] docs(notifications): change request for softwarecatalog schema notifications --- .../.openspec.yaml | 2 + .../softwarecatalog-notifications/proposal.md | 181 ++++++++++++++++++ .../softwarecatalog-notifications/spec.md | 30 +++ .../softwarecatalog-notifications/tasks.md | 19 ++ 4 files changed, 232 insertions(+) create mode 100644 openspec/changes/softwarecatalog-notifications/.openspec.yaml create mode 100644 openspec/changes/softwarecatalog-notifications/proposal.md create mode 100644 openspec/changes/softwarecatalog-notifications/specs/softwarecatalog-notifications/spec.md create mode 100644 openspec/changes/softwarecatalog-notifications/tasks.md diff --git a/openspec/changes/softwarecatalog-notifications/.openspec.yaml b/openspec/changes/softwarecatalog-notifications/.openspec.yaml new file mode 100644 index 00000000..fd706ac4 --- /dev/null +++ b/openspec/changes/softwarecatalog-notifications/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-26 diff --git a/openspec/changes/softwarecatalog-notifications/proposal.md b/openspec/changes/softwarecatalog-notifications/proposal.md new file mode 100644 index 00000000..06d88529 --- /dev/null +++ b/openspec/changes/softwarecatalog-notifications/proposal.md @@ -0,0 +1,181 @@ +--- +kind: config +depends_on: [] +--- + +# softwarecatalog — schema-declared notifications + +## Why + +Softwarecatalog is the GEMMA software catalogue used by municipal architects, +VNG, and suppliers. The headline notification needs are: **a reported +vulnerability** (urgent — suppliers + catalogue admins), **contract expiry** +(scheduled reminder), **a new module version**, and **a new review**. None of +softwarecatalog's schemas currently declare `x-openregister-notifications`, so the +OpenRegister notification engine has nothing to dispatch on. This change declares +schema-level notification rules for those four events. + +All rules use trigger types that work **today** (`created`, `scheduled`). No rule +depends on the unshipped `updated`-field-change engine condition, so this change +carries **no** `depends_on`. + +> The register file is `lib/Settings/softwarecatalogus_register.json` (note the +> `-us` suffix — it does **not** match the app slug). + +## What Changes + +Add a top-level `x-openregister-notifications` key to the relevant schemas in +`lib/Settings/softwarecatalogus_register.json`, using the verified engine dialect. + +### `kwetsbaarheid` (vulnerability) — reported (created) — URGENT + +`kwetsbaarheid` carries `naam`, `cveCode`, `cvssScore`, and `modules` (array of +module references). There is **no** direct supplier/owner field on the schema +(see Caveats), so recipients use a catalogue-admin `groups` recipient plus the +record's manage-ACL. + +```jsonc +"x-openregister-notifications": { + "vulnerability-reported": { + "trigger": {"type": "created"}, + "enabled": true, + "channels": ["nc-notification", "email"], + "recipients": [ + {"kind": "groups", "groups": ["softwarecatalog-admins"]}, + {"kind": "object-acl", "permission": "manage"} + ], + "subject": { + "nl": "Kwetsbaarheid gemeld: {{naam}} ({{cveCode}}, CVSS {{cvssScore}})", + "en": "Vulnerability reported: {{naam}} ({{cveCode}}, CVSS {{cvssScore}})" + } + } +} +``` + +### `contract` — expiry reminder (scheduled) + +`contract` carries `eindDatum` (end date), `status` (enum: `Actief` / `Verlopen` / +`In onderhandeling`), and nested `contactpersoonAanbieder` / +`contactpersoonGebruiker` objects. A `scheduled` rule periodically checks active +contracts whose `eindDatum` is approaching. + +```jsonc +"x-openregister-notifications": { + "contract-expiry": { + "trigger": {"type": "scheduled", "intervalSec": 86400, "filter": {"status": {"op": "equals", "value": "Actief"}}}, + "enabled": false, + "channels": ["nc-notification", "email"], + "recipients": [ + {"kind": "groups", "groups": ["softwarecatalog-admins"]}, + {"kind": "object-acl", "permission": "manage"} + ], + "subject": { + "nl": "Contract verloopt: {{contractNummer}} (einddatum {{eindDatum}})", + "en": "Contract expiring: {{contractNummer}} (end date {{eindDatum}})" + } + } +} +``` + +Ships **disabled by default** because the `scheduled` filter needs a +date-window comparison on `eindDatum` (e.g. "within 30 days") whose engine +support must be confirmed; see Caveats. + +### `moduleVersie` (module version) — published (created) + +`moduleVersie` carries `module`, `versie`, `status`, and `geregistreerdDoor`. A +new version row is a `created` event. + +```jsonc +"x-openregister-notifications": { + "module-version-published": { + "trigger": {"type": "created"}, + "enabled": true, + "channels": ["nc-notification"], + "recipients": [ + {"kind": "object-acl", "permission": "manage"}, + {"kind": "groups", "groups": ["softwarecatalog-admins"]} + ], + "subject": { + "nl": "Nieuwe moduleversie: {{versie}}", + "en": "New module version: {{versie}}" + } + } +} +``` + +### `beoordeeling` (review) — submitted (created) + +`beoordeeling` carries `naam`, `waardering` (rating), and `modules`. A new review +is a `created` event. + +```jsonc +"x-openregister-notifications": { + "review-submitted": { + "trigger": {"type": "created"}, + "enabled": true, + "channels": ["nc-notification"], + "recipients": [ + {"kind": "object-acl", "permission": "manage"}, + {"kind": "groups", "groups": ["softwarecatalog-admins"]} + ], + "subject": { + "nl": "Nieuwe beoordeling: {{naam}} (waardering {{waardering}})", + "en": "New review: {{naam}} (rating {{waardering}})" + } + } +} +``` + +## Capabilities + +### New Capabilities +- `softwarecatalog-notifications`: declarative schema-level notification rules on + `kwetsbaarheid`, `contract`, `moduleVersie`, and `beoordeeling`, consumed by the + OpenRegister notification engine, surfacing reported vulnerabilities (urgent), + approaching contract expiry, newly published module versions, and submitted + reviews to catalogue admins and record managers. + +## Impact + +- **File:** `lib/Settings/softwarecatalogus_register.json` — adds + `x-openregister-notifications` blocks to `kwetsbaarheid`, `contract`, + `moduleVersie`, `beoordeeling`. +- The OpenRegister notification engine (shipped in OR change + `notification-schema-rules-and-userconfig-prefs`) consumes these blocks at + runtime. No PHP/Vue changes in softwarecatalog. +- Users **opt out** per `(schema, rule)` via override-only user-config prefs; + schema `enabled` is only the default. +- `contract-expiry` ships **disabled by default** — see Caveats. + +## Caveats + +- **Recipient fields are inferred, not confirmed (plan flag).** `kwetsbaarheid` + has **no** supplier/owner uid field — only `modules` (module references). To + reach the actual supplier you would have to traverse `kwetsbaarheid → module → + aanbieder (organisatie) → contactpersoon`, which the engine's `field`/`relation` + resolver cannot do in one hop today. So vulnerability + module-version + review + rules fall back to an `object-acl` `manage` recipient plus a `groups` recipient + pointed at a `softwarecatalog-admins` group. **That group must exist** (or be + remapped to a real NC group) for the `groups` recipients to resolve. The plan's + "urgent → suppliers + admins" reaches **admins** reliably; **supplier** delivery + needs either a relation-traversal recipient resolver or a structured supplier-uid + field on the schemas. +- **`contract` contactpersoon fields are nested objects, not uid strings.** + `contactpersoonAanbieder` / `contactpersoonGebruiker` are nested `object` + properties, so a `field:` recipient will not resolve an NC uid from them. The + contract rule therefore uses `object-acl` + `groups` instead of those fields. +- **`scheduled` date-window filtering** ("eindDatum within 30 days") engine + support must be confirmed before `contract-expiry` is enabled; it ships disabled. +- **No named lifecycle/`transition` actions and no `updated`-field-change support.** + A "vulnerability status changed" or "contract status → Verlopen" rule is not + expressible today; it would need transition actions on the schema or the + unshipped `notification-updated-field-change-condition` engine change. The + vulnerability rule therefore fires on **creation** of the kwetsbaarheid record + (the report event), which matches the plan's "vulnerability reported" headline. +- **External-recipient email** (suppliers held as email strings / external + contacts) is out of scope — the `field` resolver resolves NC uids only. The + `contactpersoon` schema's `e-mailadres` is an email string, not a uid. + +See `hydra/openspec/fleet-notification-plan.md` (softwarecatalog row + cross-cutting +engine-gap section) for the full analysis. diff --git a/openspec/changes/softwarecatalog-notifications/specs/softwarecatalog-notifications/spec.md b/openspec/changes/softwarecatalog-notifications/specs/softwarecatalog-notifications/spec.md new file mode 100644 index 00000000..7f36e816 --- /dev/null +++ b/openspec/changes/softwarecatalog-notifications/specs/softwarecatalog-notifications/spec.md @@ -0,0 +1,30 @@ +# softwarecatalog-notifications + +## ADDED Requirements + +### Requirement: Catalogue schemas declare notification rules + +The `kwetsbaarheid`, `contract`, `moduleVersie`, and `beoordeeling` schemas SHALL +declare `x-openregister-notifications` rules so the OpenRegister +notification engine dispatches notifications on reported vulnerabilities, +approaching contract expiry, newly published module versions, and submitted +reviews. Every rule SHALL use a trigger type that works today (`created` or +`scheduled`), reference only an existing schema property, the record manage-ACL, +or a named group (never a `field:` recipient pointing at a nested-object or +non-existent property), and provide both `nl` and `en` subject strings. + +#### Scenario: Reported vulnerability urgently notifies admins and record managers + +- **WHEN** a `kwetsbaarheid` record is created +- **THEN** the engine dispatches `nc-notification` + `email` to the `softwarecatalog-admins` group and the record's manage-ACL holders +- **AND** the subject includes the vulnerability name, CVE code, and CVSS score in the recipient's locale (nl/en) + +#### Scenario: New review notifies record managers + +- **WHEN** a `beoordeeling` record is created +- **THEN** the engine dispatches an `nc-notification` to the record's manage-ACL holders and the `softwarecatalog-admins` group + +#### Scenario: Disabled-by-default contract expiry does not fire until confirmed + +- **WHEN** the `contract` `contract-expiry` rule (`enabled: false`) would match +- **THEN** no notification is dispatched until the scheduled date-window filter is confirmed and an admin enables the rule diff --git a/openspec/changes/softwarecatalog-notifications/tasks.md b/openspec/changes/softwarecatalog-notifications/tasks.md new file mode 100644 index 00000000..2d3c7c08 --- /dev/null +++ b/openspec/changes/softwarecatalog-notifications/tasks.md @@ -0,0 +1,19 @@ +# Tasks — softwarecatalog notifications + +- [ ] Add `x-openregister-notifications` (rule `vulnerability-reported`, created, urgent) to `kwetsbaarheid` in lib/Settings/softwarecatalogus_register.json +- [ ] Add `x-openregister-notifications` (rule `contract-expiry`, scheduled, enabled:false) to `contract` in lib/Settings/softwarecatalogus_register.json +- [ ] Add `x-openregister-notifications` (rule `module-version-published`, created) to `moduleVersie` in lib/Settings/softwarecatalogus_register.json +- [ ] Add `x-openregister-notifications` (rule `review-submitted`, created) to `beoordeeling` in lib/Settings/softwarecatalogus_register.json +- [ ] Add nl + en `subject` strings to every rule (already specified in proposal.md) +- [ ] Validate the register JSON still parses (e.g. `python3 -c "import json;json.load(open('lib/Settings/softwarecatalogus_register.json'))"`) +- [ ] Confirm the `softwarecatalog-admins` group exists or remap `groups` recipients to a real NC group before enabling +- [ ] Confirm engine support for a `scheduled` date-window filter on `eindDatum` before enabling `contract-expiry` +- [ ] Decide whether supplier delivery requires a relation-traversal recipient resolver or a structured supplier-uid field (file follow-up issue if so) + +## Acceptance criteria + +- The register JSON parses and every touched schema keeps its existing keys intact. +- Each rule uses only trigger types that work today (`created`, `scheduled`) — no dependency on the unshipped `updated`-field-change condition. +- Every rule's recipient references either an existing schema property, the record's manage-ACL, or a named group (no `field:` recipient pointing at a non-existent or nested-object property). +- Every rule has both `nl` and `en` subject strings. +- `vulnerability-reported`, `module-version-published`, `review-submitted` ship enabled; `contract-expiry` ships disabled by default. From 86fe88e46afe7179cd2f8152ac3e32ad6412e3c9 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Tue, 26 May 2026 01:41:04 +0200 Subject: [PATCH 2/2] feat(notifications): add x-openregister-notifications to softwarecatalog schemas --- lib/Settings/softwarecatalogus_register.json | 60 +++++++++++++++++++ .../softwarecatalog-notifications/tasks.md | 12 ++-- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/lib/Settings/softwarecatalogus_register.json b/lib/Settings/softwarecatalogus_register.json index ca2a2870..0922b653 100644 --- a/lib/Settings/softwarecatalogus_register.json +++ b/lib/Settings/softwarecatalogus_register.json @@ -1535,6 +1535,21 @@ "kwetsbaarheid": { "uri": null, "slug": "kwetsbaarheid", + "x-openregister-notifications": { + "vulnerability-reported": { + "trigger": {"type": "created"}, + "enabled": true, + "channels": ["nc-notification", "email"], + "recipients": [ + {"kind": "groups", "groups": ["softwarecatalog-admins"]}, + {"kind": "object-acl", "permission": "manage"} + ], + "subject": { + "nl": "Kwetsbaarheid gemeld: {{naam}} ({{cveCode}}, CVSS {{cvssScore}})", + "en": "Vulnerability reported: {{naam}} ({{cveCode}}, CVSS {{cvssScore}})" + } + } + }, "title": "Kwetsbaarheid", "description": "Schema voor kwetsbaarheden. Dit schema is onderdeel van het vastgestelde datamodel maar wordt niet daadwerkelijk in de applicatie gebruikt.", "version": "1.0.19", @@ -3119,6 +3134,21 @@ "contract": { "uri": null, "slug": "contract", + "x-openregister-notifications": { + "contract-expiry": { + "trigger": {"type": "scheduled", "intervalSec": 86400, "filter": {"status": {"op": "equals", "value": "Actief"}}}, + "enabled": false, + "channels": ["nc-notification", "email"], + "recipients": [ + {"kind": "groups", "groups": ["softwarecatalog-admins"]}, + {"kind": "object-acl", "permission": "manage"} + ], + "subject": { + "nl": "Contract verloopt: {{contractNummer}} (einddatum {{eindDatum}})", + "en": "Contract expiring: {{contractNummer}} (end date {{eindDatum}})" + } + } + }, "title": "Contract", "description": "Een formele overeenkomst voor het inzetten van een Dienst op een Gebruik. Dit schema is onderdeel van het vastgestelde datamodel maar wordt niet daadwerkelijk in de applicatie gebruikt.", "version": "0.1.0", @@ -3751,6 +3781,21 @@ "beoordeeling": { "uri": null, "slug": "beoordeeling", + "x-openregister-notifications": { + "review-submitted": { + "trigger": {"type": "created"}, + "enabled": true, + "channels": ["nc-notification"], + "recipients": [ + {"kind": "object-acl", "permission": "manage"}, + {"kind": "groups", "groups": ["softwarecatalog-admins"]} + ], + "subject": { + "nl": "Nieuwe beoordeling: {{naam}} (waardering {{waardering}})", + "en": "New review: {{naam}} (rating {{waardering}})" + } + } + }, "title": "Beoordeeling", "description": "Schema voor beoordelingen en waarderingen van applicaties en diensten. Dit schema is onderdeel van het vastgestelde datamodel maar wordt niet daadwerkelijk in de applicatie gebruikt.", "version": "0.1.2", @@ -7227,6 +7272,21 @@ "moduleVersie": { "uri": null, "slug": "moduleVersie", + "x-openregister-notifications": { + "module-version-published": { + "trigger": {"type": "created"}, + "enabled": true, + "channels": ["nc-notification"], + "recipients": [ + {"kind": "object-acl", "permission": "manage"}, + {"kind": "groups", "groups": ["softwarecatalog-admins"]} + ], + "subject": { + "nl": "Nieuwe moduleversie: {{versie}}", + "en": "New module version: {{versie}}" + } + } + }, "title": "Applicatieversie", "description": "Schema voor applicatieversies", "version": "0.1.0", diff --git a/openspec/changes/softwarecatalog-notifications/tasks.md b/openspec/changes/softwarecatalog-notifications/tasks.md index 2d3c7c08..16203be6 100644 --- a/openspec/changes/softwarecatalog-notifications/tasks.md +++ b/openspec/changes/softwarecatalog-notifications/tasks.md @@ -1,11 +1,11 @@ # Tasks — softwarecatalog notifications -- [ ] Add `x-openregister-notifications` (rule `vulnerability-reported`, created, urgent) to `kwetsbaarheid` in lib/Settings/softwarecatalogus_register.json -- [ ] Add `x-openregister-notifications` (rule `contract-expiry`, scheduled, enabled:false) to `contract` in lib/Settings/softwarecatalogus_register.json -- [ ] Add `x-openregister-notifications` (rule `module-version-published`, created) to `moduleVersie` in lib/Settings/softwarecatalogus_register.json -- [ ] Add `x-openregister-notifications` (rule `review-submitted`, created) to `beoordeeling` in lib/Settings/softwarecatalogus_register.json -- [ ] Add nl + en `subject` strings to every rule (already specified in proposal.md) -- [ ] Validate the register JSON still parses (e.g. `python3 -c "import json;json.load(open('lib/Settings/softwarecatalogus_register.json'))"`) +- [x] Add `x-openregister-notifications` (rule `vulnerability-reported`, created, urgent) to `kwetsbaarheid` in lib/Settings/softwarecatalogus_register.json +- [x] Add `x-openregister-notifications` (rule `contract-expiry`, scheduled, enabled:false) to `contract` in lib/Settings/softwarecatalogus_register.json +- [x] Add `x-openregister-notifications` (rule `module-version-published`, created) to `moduleVersie` in lib/Settings/softwarecatalogus_register.json +- [x] Add `x-openregister-notifications` (rule `review-submitted`, created) to `beoordeeling` in lib/Settings/softwarecatalogus_register.json +- [x] Add nl + en `subject` strings to every rule (already specified in proposal.md) +- [x] Validate the register JSON still parses (e.g. `python3 -c "import json;json.load(open('lib/Settings/softwarecatalogus_register.json'))"`) - [ ] Confirm the `softwarecatalog-admins` group exists or remap `groups` recipients to a real NC group before enabling - [ ] Confirm engine support for a `scheduled` date-window filter on `eindDatum` before enabling `contract-expiry` - [ ] Decide whether supplier delivery requires a relation-traversal recipient resolver or a structured supplier-uid field (file follow-up issue if so)