Grundschutz++ as code, part 3: Containers that say which requirements they meet
An image knows what it is made of. That is in the SBOM. Which requirements the pipeline met while building it was written nowhere on the image. Now every image carries a signed OSCAL component definition. It names the Grundschutz++ requirements for which this very build has evidence.
Part 3 of the series “Grundschutz++ as code”. The overview of all parts is further down.
01 — What a component definition describes
Besides catalogue and profile, OSCAL has a third model: the component definition. It describes what a product, a service or a procedure does for particular requirements. NIST meant it for vendors. Whoever ships software can state, machine-readably, which requirements it supports.
A component definition is not a statement about a running system. It says what a component provides. Whether a requirement is met in an environment only a measurement can tell. That separation runs through the whole series.
02 — How the BSI describes components
The Stand-der-Technik library ships six component definitions of its own (as of 28 September 2026, commit 367d775). Among them are Keycloak, a password policy, a network architecture and proposals for supply-chain security. They are worth a look before writing your own.
- Reference by UUID: The requirements do not appear with an ID such as BER.1.4. They appear with the UUID the catalogue carries as an alternative identifier. Anyone who wants to read both has to join them.
- Product, not instance: The Keycloak definition describes what Keycloak supports in general. It is explicitly marked as a draft and bound to no particular installation.
- Mixed versions: Keycloak is in OSCAL 1.2.2, supply-chain security still in 1.1.2. Part 2 showed why you therefore validate against the version a file declares.
For an image from a pipeline that is not enough. What counts there is not what a tool can do, but what happened in this build.
03 — One definition per image
The pipeline generates the definition afresh on every build and binds it to the image digest. The generator component.mjs from grundschutz-worksheets takes a small source file for this: requirement, description in your own words, links to the evidence.
node component.mjs source.json \
--subject registry.example.org/app@sha256:… \
--prop pipeline=https://ci.example.org/p/123 > component.json
node validate.mjs component.json- Both identifiers: Every requirement carries the Grundschutz++ ID and the UUID from the catalogue. People read the ID, tools join on the UUID with the BSI's components.
- Checked against the catalogue: An ID missing from the catalogue at the pinned commit stops the generation.
- Checked against the schema: Before anything is signed,
validate.mjschecks the file against the NIST schema 1.2.2.
The example has four requirements a build pipeline typically covers: software components (DEV.4.3), updates of external software (DEV.4.5), vulnerability scans (DET.5.3) and the integrity test (TEST.3.1.3).
04 — Claim only what has evidence
The first version built the source file from the pipeline's settings. SBOM switched on, so DEV.4.3. Scan as a gate, so DEV.4.5. That sounds reasonable and is still wrong. The file said how the pipeline was configured, not what happened to this image. If the signing job failed or the SBOM was missing, both would still be listed.
The second version checks first and describes afterwards. A separate step runs after signing and scanning and looks at the pushed digest:
REF=registry.example.org/app@sha256:…
cosign verify --key cosign.pub "$REF"
cosign verify-attestation --key cosign.pub --type cyclonedx "$REF"
# plus the verdict of the vulnerability scan from the same pipeline- DEV.4.3 software components: only with an SBOM attestation that verifies.
- DEV.4.5 updates of external software: only with a scan as a gate and no open finding above the threshold.
- DET.5.3 vulnerability scans: only with a scan verdict for exactly this digest.
- TEST.3.1.3 integrity test: only with a signature that verifies against the public key.
Every entry links its evidence. What is missing is missing from the definition too, and the log says why. Without a single piece of evidence nothing is signed.
There is one trap. A shared pipeline component does not know in which stage a project signs. If the check sits in a fixed stage, it may land before signing, and GitLab rejects the whole pipeline. It therefore belongs in .post, which runs after every other stage.
05 — Sign, attach, verify from outside
Like the SBOM, the finished file is attached to the digest as an attestation, signed with the same key. The division of labour matters. The step that fetches code from the network and builds the file has no access to the key. The step with the key only signs a finished file.
Whether all of this holds only a check from outside shows, independent of the pipeline that produced the file:
cosign verify-attestation --key cosign.pub \
--type github.com/ohartwig/grundschutz-worksheets/predicate/oscal-component-definition/v1 \
"$REF" | jq -r .payload | base64 -d | jq .predicate > component.json
node validate.mjs component.json
I run this check as a case of its own in a proof repository, on every tag. It checks six things:
- The attestation verifies with the right key and fails with a foreign one.
- It belongs to exactly the digest that was built.
- The signed content is identical to the generated file.
- The repository deliberately does not scan. So DEV.4.5 and DET.5.3 must not be listed, and they are not.
- Every ID is in the catalogue at the pinned commit.
- A simulated catalogue change is detected, more on that below.
The fourth point matters most. A check that only knows green cases does not show that it can leave something out.
06 — When the catalogue changes
Grundschutz++ is still being built. The BSI will rename, merge or withdraw requirements. A definition pointing at an old state then stays formally valid and still says the wrong thing.
The tool recheck.mjs compares the claimed requirements with another state of the catalogue, by ID and UUID at once:
node recheck.mjs component.json --commit <newer library commit>
node recheck.mjs component.json --catalog edited-catalog.json- unchanged: ID and UUID as before.
- renamed: The UUID is still there, under another ID.
- uuid-changed: The ID is still there, but possibly with a different meaning.
- withdrawn: Neither ID nor UUID is in the catalogue any more.
On any change the tool exits with an error code. So it can run as a scheduled job against the current state of the library. On 28 September 2026 the pinned commit was still the current one, so there was nothing to report. That is why the proof case checks a simulated change. It withdraws one requirement in a copy of the catalogue and expects exactly that report.
This is where carrying both identifiers pays off. Only the UUID tells a rename from a withdrawal.
07 — What the definition does not say
A signed component definition is good evidence. It is not evidence of everything.
- Not the configuration: Whether the image runs as root or which ports it opens is not in it.
- Not the operation: Whether the image running right now is this one only a comparison with the cluster can tell.
- No conformity: It says what was evidenced in this build. Whether a system meets the requirements is an assessment.
That is the topic of part 4: measurements that write their results as OSCAL themselves.
08 — The series at a glance
- Grundschutz++ against your own documentation: the method, the catalogue as a data model, the comparison with IaC and GitOps.
- Applicability as data: the Statement of Applicability as an OSCAL profile.
- Containers that say which requirements they meet: a signed component definition on every image the pipeline builds. This post.
- Measurement as evidence: nightly checks write their results as OSCAL. Plus the second level, which finds in the running system what no repository knows.
- The ISMS as a build product: a security plan that is assembled rather than written, with an honest account of the effort.
A part appears only once the step it describes runs in operation and has been measured. The code for it is published beforehand in grundschutz-worksheets. The titles are linked here as soon as each post is online.
Frequently asked questions
Why not use the BSI's definitions?+
They describe products in general, and some are still drafts. For a single image the link to the build is missing. The UUID lets you join the two, though: the BSI's definition for the product, your own for the build.
What happens when the BSI changes the catalogue?+
The definition points at a fixed commit and stays valid there. Before the commit is raised, recheck.mjs reports which requirements were renamed, given a new meaning or withdrawn. Only then is the new state entered.
Does the definition replace the SBOM?+
Does this work without cosign?+
Yes. The component definition is an ordinary JSON file and can also be stored as a release artefact. The attestation only binds it to a digest in a way that cannot be forged. Without it you have to trust the storage.
Conclusion
A component definition on the image is quickly built. Only one thing is hard: it must not claim anything that has no evidence. Generated from settings, it says what should be. Generated from evidence, it says what was. With the check from outside and the comparison against new catalogues, it stays right later on too.
Next up, part 4: measurement as evidence.
I attach to your images what your pipeline actually evidences. Signed and verifiable from outside.
An evidence check after signing and scanning, an OSCAL component definition per image, the attestation with your key and a test case that shows missing evidence stays missing.
Platform operation rather than consulting on paper: if you like, I build the next steps too, from the measurements to the security plan.
About the author

Kai Ole Hartwig
Programming since 2002 – self-taught, set up my own business with KO-Web in 2012. Over 100 projects, with a focus on security, performance, automation and quality. Today freelance: DevSecOps consulting, training and software development.
