Skip to content

Commit 44a243c

Browse files
authored
Merge pull request #389 from ConductionNL/wip/bio-compliance-assessment
feat: bio-compliance-assessment — BIO measures, BBN level, DPIA tracking
2 parents 382d4c0 + f1c09e7 commit 44a243c

24 files changed

Lines changed: 1743 additions & 138 deletions

File tree

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
3+
- SPDX-License-Identifier: EUPL-1.2
4+
-->
5+
6+
# BIO compliance assessment
7+
8+
Adds the Dutch government security/privacy compliance stack on top of the
9+
existing GEMMA compliance model: a seedable **BIO 2.0 measures** catalog,
10+
per-application **BBN level** and **DPIA** tracking, a reference to the
11+
organisation's **register van verwerkingen** entry, catalog filters, a
12+
per-organisation **BIO coverage report**, and a declarative notification for
13+
overdue DPIA reviews.
14+
15+
Specifications:
16+
[`openspec/specs/bio-compliance-assessment/spec.md`](../../openspec/specs/bio-compliance-assessment/spec.md)
17+
(new capability) and the `module-compliance-assessment` MODIFIED delta
18+
(BIO-measure column source on the compliance matrix).
19+
20+
## Why this extends `compliancy`, not a new schema
21+
22+
BIO measure compliance is asserted through the **same** `compliancy` record
23+
model already used for GEMMA standards (`module ↔ standaardversie`,
24+
evidence, verified/claimed). `compliancy` gains one more optional relation,
25+
`bioMaatregel`, parallel to `standaardversie`. A record links a module to
26+
**exactly one** of the two relations — never both (a record carrying both is
27+
flagged as a data-quality issue and excluded from every matrix rather than
28+
matched to either column). This reuses the entire evidence/verified-claimed
29+
mechanism and the matrix mapper instead of forking a parallel "BIO
30+
assessment" object.
31+
32+
## The BIO measures catalog
33+
34+
A `bioMaatregel` object is one BIO 2.0 measure: `code`, `naam`,
35+
`omschrijving`, `thema`, `bioVersie`, the applicable `bbnNiveau`(s), and a
36+
`bron` reference to the published measure list. It is a publicly readable
37+
reference catalog (`authorization.read: ["public"]`), seeded on
38+
install/upgrade from `lib/Settings/softwarecatalogus_register.json`'s
39+
`x-openregister.seedData.objects.bioMaatregel` array — the same
40+
seed-and-reimport pattern the GEMMA `element` catalog already uses, so
41+
re-running the `InitializeSettings` repair step is idempotent (upsert by
42+
`slug`).
43+
44+
Browsable via **BIO measures** (`BioMaatregelen` index / `BioMaatregelDetail`
45+
detail page), which lists every compliance claim referencing the measure —
46+
the same "claims for this X" pattern `StandaardDetail` already uses.
47+
48+
## Application-level fields
49+
50+
`module` gains six optional fields (no `required` change, so existing
51+
objects stay valid without migration):
52+
53+
| Field | Type | Notes |
54+
|---|---|---|
55+
| `bbnLevel` | enum `BBN1`/`BBN2`/`BBN3` | `facetable: true` — drives the catalog filter and the coverage report |
56+
| `dpiaStatus` | enum `not required`/`required`/`executed` | |
57+
| `dpiaDate` | date | Meaningful only when `dpiaStatus` is `executed`; not enforced at write time |
58+
| `dpiaVolgendeBeoordeling` | date | Next DPIA review due date; drives the overdue notification |
59+
| `dpiaDocumentRef` | string (NC Files reference) | Link-don't-store, mirroring `compliancy.bewijsReferentie` |
60+
| `verwerkingsregisterRef` | string (URL or identifier) | Reference only — this change does not model the register van verwerkingen itself |
61+
62+
These render on the `ModuleDetail` page's data widget alongside the
63+
application's other fields, and on the `Modules` index/catalog listing.
64+
65+
## Catalog filters
66+
67+
The `Modules` index page's quick filters include `BBN1`/`BBN2`/`BBN3` and a
68+
compound **"Without DPIA (BBN2+)"** filter. The compound filter is expressed
69+
as two bare-array IN-clauses —
70+
`{"bbnLevel": ["BBN2", "BBN3"], "dpiaStatus": ["not required", "required"]}`
71+
— rather than a `{dpiaStatus: {ne: "executed"}}` operator object: the
72+
frontend's `useObjectStore.buildQueryString` JSON.stringifies plain-object
73+
filter values into a single GET query-string value, which OpenRegister's
74+
`MagicSearchHandler` never `json_decode`s back into an array (only
75+
bracket-repeated `field[]=` array params survive as a real PHP array on
76+
that path), so an operator object would silently no-op. The IN-list only
77+
matches the two explicit "not executed" enum values — a module with
78+
`dpiaStatus` entirely unset is not caught by this quick filter (SQL `IN()`
79+
never matches `NULL`), though the catalog's DPIA column still shows it as
80+
unset for a human reviewer.
81+
82+
## BIO coverage report
83+
84+
Extends the existing `ComplianceMatrixView` (`src/views/ComplianceMatrixView.vue`,
85+
`src/utils/complianceMatrix.js`) rather than adding a new page. A radio
86+
switch picks the column source — **Standards** (`standaardversie`) or
87+
**BIO measures** (`bioMaatregel`) — and, in the BIO scope, each row also
88+
shows the module's BBN level and DPIA status. An organisation picker scopes
89+
the rows to that organisation's in-use applications (`gebruik.afnemer`
90+
`gebruik.module`); applications with no BBN level, DPIA data, or BIO measure
91+
compliance are still listed, rendered as "none" / "Not set" — never omitted.
92+
The selection (column source, selected columns, organisation) is encoded in
93+
the URL so a comparison is shareable.
94+
95+
`complianceMatrix.js`'s `partitionCompliancy()` / `buildComplianceMatrix()` /
96+
`buildOrganisationCoverage()` all take a `columnSource` parameter
97+
(`COLUMN_SOURCE.STANDAARDVERSIE` or `COLUMN_SOURCE.BIO_MAATREGEL`) so the
98+
BIO-measure matrix reuses the exact same mapper as the standards matrix — no
99+
second cell-state computation exists.
100+
101+
## Overdue DPIA notification
102+
103+
`module` declares a `dpia-review-overdue` rule in the canonical
104+
`x-openregister-notifications` dialect (ADR-031 — declarative, no bespoke
105+
PHP notification service):
106+
107+
```json
108+
{
109+
"trigger": {
110+
"type": "scheduled",
111+
"intervalSec": 86400,
112+
"filter": {
113+
"dpiaStatus": { "operator": "equals", "value": "executed" },
114+
"dpiaVolgendeBeoordeling": { "operator": "withinNext", "value": "P0D" }
115+
}
116+
},
117+
"channels": ["nc-notification", "email"],
118+
"recipients": [
119+
{ "kind": "groups", "groups": ["software-catalog-admins"] },
120+
{ "kind": "object-acl", "permission": "manage" }
121+
],
122+
"subject": {
123+
"nl": "DPIA-beoordeling verlopen: {{naam}} (uiterlijk {{dpiaVolgendeBeoordeling}})",
124+
"en": "DPIA review overdue: {{naam}} (due {{dpiaVolgendeBeoordeling}})"
125+
}
126+
}
127+
```
128+
129+
`withinNext` with a zero-day window (`P0D`) reads as "due on or before
130+
today" — the same trigger/filter shape already proven in this register by
131+
`contract`'s `contract-expiry` and `gebruik`'s `phaseout-approaching` rules,
132+
so no new engine behaviour is assumed.
133+
134+
## Fixed along the way
135+
136+
- The `Modules`/`Diensten` navigation pages were, on disk before this
137+
change resumed, mid-replacement: the pre-existing `type: custom`
138+
`FacetedCatalogIndexView` pages (proposed by the still-open
139+
`gemma-faceted-search` change) point at a component that was never
140+
registered in `customComponents.js` — a pre-existing dangling reference.
141+
An earlier pass of this change had silently deleted `Diensten`
142+
(nav entry and page) while replacing `Modules`; both are restored/kept
143+
here — `Modules` becomes a working `type: index` page (needed for the
144+
BBN/DPIA fields and filters), `Diensten` is left exactly as it was
145+
(broken, pending `gemma-faceted-search`, out of scope for this change).
146+
- `organisation-merge`'s Organisaties index filter had the same
147+
`{"$ne": ...}` defect this change's own compound filter would have
148+
introduced (see docs/features/organisation-merge.md) — fixed to the
149+
working IN-list shape while investigating the operator dialect.
150+
151+
## Out of scope
152+
153+
- Automated BIO evidence collection.
154+
- ISMS workflows beyond the DPIA review date.
155+
- NIS2 incident reporting, audit certification flows (ENSIA, DigiD).
156+
- Modelling the register van verwerkingen itself (reference only).
157+
- Computing `bbnLevel` or `dpiaStatus` automatically — both are user-entered.
158+
159+
## Screenshots
160+
161+
Not captured in this change — the implementing session had no live
162+
Nextcloud instance to drive Playwright against without touching the shared
163+
dev environment (out of bounds for this change). Follow-up: capture the
164+
Modules BBN/DPIA fields, the BIO measures catalog, the BIO coverage report,
165+
and the DPIA filter per ADR-010 once verified against a running instance.

docs/features/organisation-merge.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,21 @@ Once every relation type has completed, the source organisation is updated
9393
- `mergedInto = "<target uuid>"`
9494

9595
The source is **never deleted**. It disappears from the default
96-
Organisaties index listing (`config.filter: {"status": {"$ne": "samengevoegd"}}`
96+
Organisaties index listing (`config.filter: {"status": ["Concept", "Actief", "Deactief"]}`
9797
in `src/manifest.json`), but stays resolvable by direct UUID lookup — its
9898
detail page renders a read-only notice with a link to the organisation it
9999
was merged into.
100100

101+
<!-- Fixed by bio-compliance-assessment: the filter previously read
102+
`{"status": {"$ne": "samengevoegd"}}`, an operator shape that
103+
useObjectStore's buildQueryString JSON.stringifies into a single
104+
query-string value on GET — OpenRegister's MagicSearchHandler never
105+
json_decodes it back into an array, so the exclusion silently never
106+
applied (merged organisations stayed listed). Bare-array filter
107+
values ARE correctly bracket-encoded (`field[]=`) and parsed by PHP
108+
as a real array, so the fix enumerates the three non-merged status
109+
values as an IN-list instead of the unsupported operator object. -->
110+
101111
`OrganisatieService::mapStatus('samengevoegd')` also returns `false`, so the
102112
linked OpenRegister core `Organisation` entity's `active` flag is kept in
103113
sync (organisatie-service spec delta).

l10n/en.json

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,11 @@
431431
"Approaching": "Approaching",
432432
"Approval": "Approval",
433433
"Cloud-transition share": "Cloud-transition share",
434+
"All applications": "All applications",
435+
"Approval": "Approval",
436+
"BBN level": "BBN level",
437+
"BIO measures": "BIO measures",
438+
"Compliance matrix scope": "Compliance matrix scope",
434439
"Compliance records": "Compliance records",
435440
"Confirm organisation merge": "Confirm organisation merge",
436441
"Contact persons": "Contact persons",
@@ -527,6 +532,38 @@
527532
"TIME quadrant counts": "TIME quadrant counts",
528533
"Tolerate": "Tolerate",
529534
"Unclassified": "Unclassified",
530-
"Usage records": "Usage records"
535+
"Usage records": "Usage records",
536+
"DPIA status": "DPIA status",
537+
"Executed": "Executed",
538+
"Fold this organisation into another one (gemeentelijke herindeling or leveranciersovername). Every contract, usage record, contact person, offering and compliance record is re-pointed to the target; this organisation is then marked as merged, never deleted.": "Fold this organisation into another one (gemeentelijke herindeling or leveranciersovername). Every contract, usage record, contact person, offering and compliance record is re-pointed to the target; this organisation is then marked as merged, never deleted.",
539+
"Go to the organisation it was merged into": "Go to the organisation it was merged into",
540+
"Group members": "Group members",
541+
"Loading merge status": "Loading merge status",
542+
"Merge organisation": "Merge organisation",
543+
"Merge organisations": "Merge organisations",
544+
"No BIO measures seeded": "No BIO measures seeded",
545+
"Not required": "Not required",
546+
"Not set": "Not set",
547+
"Offerings": "Offerings",
548+
"Organisation (scope to in-use applications)": "Organisation (scope to in-use applications)",
549+
"Organisation successfully merged.": "Organisation successfully merged.",
550+
"Pick one or more columns above to render the compliance matrix.": "Pick one or more columns above to render the compliance matrix.",
551+
"Preview merge": "Preview merge",
552+
"Records that will be re-pointed to {target}:": "Records that will be re-pointed to {target}:",
553+
"Required": "Required",
554+
"Select BIO measures to compare": "Select BIO measures to compare",
555+
"Select one or more BIO measures": "Select one or more BIO measures",
556+
"Select the organisation to merge into": "Select the organisation to merge into",
557+
"Some compliancy records reference both a standard and a BIO measure — a data-quality issue. They are excluded from both matrices until corrected.": "Some compliancy records reference both a standard and a BIO measure — a data-quality issue. They are excluded from both matrices until corrected.",
558+
"Target organisation": "Target organisation",
559+
"The BIO measures catalog is seeded on install/upgrade. Refresh, or check the BIO measures catalog.": "The BIO measures catalog is seeded on install/upgrade. Refresh, or check the BIO measures catalog.",
560+
"This organisation has been merged and is no longer active.": "This organisation has been merged and is no longer active.",
561+
"This will permanently fold {source} into {target}.": "This will permanently fold {source} into {target}.",
562+
"Usage records": "Usage records",
563+
"Which applications support which BIO 2.0 measures, plus each application's BBN level and DPIA status. A verified cell traces to evidence; a claimed cell is a supplier statement without evidence.": "Which applications support which BIO 2.0 measures, plus each application's BBN level and DPIA status. A verified cell traces to evidence; a claimed cell is a supplier statement without evidence.",
564+
"BBN1": "BBN1",
565+
"BBN2": "BBN2",
566+
"BBN3": "BBN3",
567+
"Without DPIA (BBN2+)": "Without DPIA (BBN2+)"
531568
}
532569
}

l10n/en_US.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,11 @@ OC.L10N.register(
343343
"SPDX (JSON)" : "SPDX (JSON)",
344344
"SBOM import failed" : "SBOM import failed",
345345
"Imported {count} components." : "Imported {count} components.",
346-
"Last imported {date} from {file} ({format})" : "Last imported {date} from {file} ({format})"
346+
"Last imported {date} from {file} ({format})" : "Last imported {date} from {file} ({format})",
347+
"BBN1" : "BBN1",
348+
"BBN2" : "BBN2",
349+
"BBN3" : "BBN3",
350+
"Without DPIA (BBN2+)" : "Without DPIA (BBN2+)"
347351
},
348352
"nplurals=2; plural=(n != 1);"
349353
);

l10n/en_US.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,27 @@
385385
"SPDX (JSON)": "SPDX (JSON)",
386386
"SBOM import failed": "SBOM import failed",
387387
"Imported {count} components.": "Imported {count} components.",
388-
"Last imported {date} from {file} ({format})": "Last imported {date} from {file} ({format})"
388+
"Last imported {date} from {file} ({format})": "Last imported {date} from {file} ({format})",
389+
"All applications": "All applications",
390+
"BBN level": "BBN level",
391+
"BIO measures": "BIO measures",
392+
"Compliance matrix scope": "Compliance matrix scope",
393+
"DPIA status": "DPIA status",
394+
"Executed": "Executed",
395+
"No BIO measures seeded": "No BIO measures seeded",
396+
"Not required": "Not required",
397+
"Not set": "Not set",
398+
"Organisation (scope to in-use applications)": "Organisation (scope to in-use applications)",
399+
"Pick one or more columns above to render the compliance matrix.": "Pick one or more columns above to render the compliance matrix.",
400+
"Required": "Required",
401+
"Select BIO measures to compare": "Select BIO measures to compare",
402+
"Select one or more BIO measures": "Select one or more BIO measures",
403+
"Some compliancy records reference both a standard and a BIO measure — a data-quality issue. They are excluded from both matrices until corrected.": "Some compliancy records reference both a standard and a BIO measure — a data-quality issue. They are excluded from both matrices until corrected.",
404+
"The BIO measures catalog is seeded on install/upgrade. Refresh, or check the BIO measures catalog.": "The BIO measures catalog is seeded on install/upgrade. Refresh, or check the BIO measures catalog.",
405+
"Which applications support which BIO 2.0 measures, plus each application's BBN level and DPIA status. A verified cell traces to evidence; a claimed cell is a supplier statement without evidence.": "Which applications support which BIO 2.0 measures, plus each application's BBN level and DPIA status. A verified cell traces to evidence; a claimed cell is a supplier statement without evidence.",
406+
"BBN1": "BBN1",
407+
"BBN2": "BBN2",
408+
"BBN3": "BBN3",
409+
"Without DPIA (BBN2+)": "Without DPIA (BBN2+)"
389410
}
390411
}

l10n/nl.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,11 @@ OC.L10N.register(
380380
"SPDX (JSON)" : "SPDX (JSON)",
381381
"SBOM import failed" : "SBOM-import mislukt",
382382
"Imported {count} components." : "{count} componenten geïmporteerd.",
383-
"Last imported {date} from {file} ({format})" : "Laatst geïmporteerd op {date} vanuit {file} ({format})"
383+
"Last imported {date} from {file} ({format})" : "Laatst geïmporteerd op {date} vanuit {file} ({format})",
384+
"BBN1" : "BBN1",
385+
"BBN2" : "BBN2",
386+
"BBN3" : "BBN3",
387+
"Without DPIA (BBN2+)" : "Zonder DPIA (BBN2+)"
384388
},
385389
"nplurals=2; plural=(n != 1);"
386390
);

l10n/nl.json

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,28 @@
528528
"SPDX (JSON)": "SPDX (JSON)",
529529
"SBOM import failed": "SBOM-import mislukt",
530530
"Imported {count} components.": "{count} componenten geïmporteerd.",
531-
"Last imported {date} from {file} ({format})": "Laatst geïmporteerd op {date} vanuit {file} ({format})"
531+
"Last imported {date} from {file} ({format})": "Laatst geïmporteerd op {date} vanuit {file} ({format})",
532+
"Approval": "Goedkeuring",
533+
"All applications": "Alle applicaties",
534+
"BBN level": "BBN-niveau",
535+
"BIO measures": "BIO-maatregelen",
536+
"Compliance matrix scope": "Bereik compliance-matrix",
537+
"DPIA status": "DPIA-status",
538+
"Executed": "Uitgevoerd",
539+
"No BIO measures seeded": "Geen BIO-maatregelen geladen",
540+
"Not required": "Niet vereist",
541+
"Not set": "Niet ingesteld",
542+
"Organisation (scope to in-use applications)": "Organisatie (beperk tot applicaties in gebruik)",
543+
"Pick one or more columns above to render the compliance matrix.": "Kies hierboven een of meer kolommen om de compliance-matrix te tonen.",
544+
"Required": "Vereist",
545+
"Select BIO measures to compare": "Selecteer BIO-maatregelen om te vergelijken",
546+
"Select one or more BIO measures": "Selecteer een of meer BIO-maatregelen",
547+
"Some compliancy records reference both a standard and a BIO measure — a data-quality issue. They are excluded from both matrices until corrected.": "Sommige compliance-records verwijzen zowel naar een standaard als naar een BIO-maatregel — een datakwaliteitsprobleem. Deze worden uitgesloten van beide matrices totdat dit is gecorrigeerd.",
548+
"The BIO measures catalog is seeded on install/upgrade. Refresh, or check the BIO measures catalog.": "De BIO-maatregelencatalogus wordt gevuld bij installatie/upgrade. Ververs de pagina, of controleer de BIO-maatregelencatalogus.",
549+
"Which applications support which BIO 2.0 measures, plus each application's BBN level and DPIA status. A verified cell traces to evidence; a claimed cell is a supplier statement without evidence.": "Welke applicaties voldoen aan welke BIO 2.0-maatregelen, plus het BBN-niveau en de DPIA-status van elke applicatie. Een geverifieerde cel verwijst naar bewijs; een geclaimde cel is een leveranciersverklaring zonder bewijs.",
550+
"BBN1": "BBN1",
551+
"BBN2": "BBN2",
552+
"BBN3": "BBN3",
553+
"Without DPIA (BBN2+)": "Zonder DPIA (BBN2+)"
532554
}
533555
}

0 commit comments

Comments
 (0)