Kai Ole Hartwig
10 min read
By

Grundschutz++ as code, part 2: Applicability as data

Every ISO 27001 ISMS has a Statement of Applicability. It defines what gets checked. In part 1 it was a table in Markdown, and a script turned it into a checklist. Now it is an OSCAL profile: generated from the same table, checked against the BSI catalogue and readable by any tool.

Part 2 of the series “Grundschutz++ as code”. The overview of all parts is further down.

01 — Why the Statement of Applicability comes first

Every further step of the series needs an answer to the same question: which requirements apply at all? A component describes which requirements it covers. A measurement checks whether a requirement is met. A security plan brings both together. All three refer to a selection from the catalogue.

That selection already exists. ISO 27001 requires it in clause 6.1.3 d as the Statement of Applicability. Through the library's mapping it becomes a list of Grundschutz++ requirements, as shown in part 1. As long as that list exists only as text, every tool has to guess it again. That is why it is the first document that should become OSCAL.

02 — What an OSCAL profile is

A catalogue contains every requirement. A profile selects from it and tailors. It points at the catalogue, names the requirements that apply, and can set parameters or add text. The result can be resolved into a catalogue again that contains only the selection.

The BSI uses exactly this mechanism itself. The Grundschutz++ user catalogue is a resolved profile over several source catalogues of the library (Kernel-G0, methodology, part of risk management). Your own profile simply sits one level further down. Shortened, it looks like this:

 

{
  "profile": {
    "uuid": "…",
    "metadata": {
      "title": "Grundschutz++ tailoring of Example GmbH",
      "last-modified": "2026-09-27T00:00:00Z",
      "version": "2026-09-27",
      "oscal-version": "1.1.3"
    },
    "imports": [{
      "href": "https://raw.githubusercontent.com/BSI-Bund/Stand-der-Technik-Bibliothek/367d775…/control_layer/Grundschutz%2B%2B/Grundschutz%2B%2B-resolved_catalog.json",
      "include-controls": [{
        "with-child-controls": "no",
        "with-ids": ["DET.2.1", "DET.3.1", "KONF.3.2", "…"]
      }]
    }],
    "merge": { "as-is": true }
  }
}

 

Two things stand out. The catalogue is imported at a fixed commit, not at the current version. And the profile names IDs, not texts. The BSI texts stay in the catalogue; the profile only points at them.

03 — From document to profile

Copying the Statement of Applicability into JSON by hand would repeat the mistake from part 1 in a new form. There would be two copies, and one of them would soon be wrong. So the profile is generated, in two halves.

The tailoring file is deliberately plain:

 

{
  "title": "Grundschutz++ tailoring of Example GmbH",
  "version": "2026-09-27",
  "last-modified": "2026-09-27T00:00:00Z",
  "catalog": {
    "path": "control_layer/Grundschutz++/Grundschutz++-resolved_catalog.json",
    "commit": "367d775010abee641b258926bb482fcd05270059"
  },
  "applicable": ["DET.2.1", "KONF.3.2"],
  "not-applicable": [{ "id": "ARCH.6.1", "reason": "no own WAN links" }]
}

 

git clone github.com/ohartwig/grundschutz-worksheets
cd grundschutz-worksheets
npm ci   # only for validate.mjs

C=367d775010abee641b258926bb482fcd05270059
CAT=control_layer/Grundschutz++/Grundschutz++-resolved_catalog.json

node profile.mjs tailoring.json > profile.json
node validate.mjs profile.json
node worksheet.mjs $CAT --profile profile.json --commit $C

 

The last line closes the loop. The worksheet from part 1 now reads the profile instead of a list of Annex A controls. The output is exactly the requirements that apply. When I wrote this, it was 257 of 280 in my case.

Nothing drops out silently

The last point paid off on the very first run. Two rows carried a note in the level column that the first pattern did not know. The result was 278 requirements instead of 280, without any warning.

04 — The gap in the format: not applicable, because

A Statement of Applicability does not only say what applies. It also says what does not, and why. That is exactly what an OSCAL profile has no field for. It can include or exclude requirements, but it cannot give a reason.

There are three ways out:

I chose the third:

 

"back-matter": {
  "resources": [{
    "uuid": "…",
    "title": "Not applicable: ARCH.6.1",
    "description": "no own WAN links",
    "props": [
      { "name": "control-id", "ns": "https://github.com/ohartwig/grundschutz-worksheets/ns", "value": "ARCH.6.1" },
      { "name": "tailoring", "ns": "https://github.com/ohartwig/grundschutz-worksheets/ns", "value": "not-applicable" }
    ]
  }]
}

 

OSCAL actually expects this statement in the security plan. That only arrives in part 5. Until then the reason lives in the profile, machine-readable and valid against the schema.

Applying is not meeting

A requirement that applies but is still open stays in the profile. Whether something applies and whether it is met are two questions. The profile answers the first. Measurements answer the second, and they come in part 4. Mix both into one file and you get a Statement of Applicability that changes with every finding.

05 — Generating deterministically

A generated document can only be reviewed if the same input gives the same bytes. Otherwise every merge request shows a diff across the whole file, and nobody reads it.

The test is simple. Set one requirement to “not applicable”, and the diff shows exactly that: the ID leaves the selection, one back-matter entry appears, two counters change. Nothing else.

The pipeline checks this on every change. It regenerates the profile and compares it with the committed version. If they differ, the merge request is red. A changed table without a new profile does not get through, and neither does a hand edit to the JSON.

06 — Validating against the schema

NIST publishes a JSON schema for every OSCAL version. The tool validate.mjs checks every file against the schema of the version the file itself declares. That is necessary because the BSI library mixes several versions: catalogues and profiles in 1.1.3, the mapping and newer component definitions in 1.2.2, older component definitions still in 1.1.2. Not every file in the library passes. Two component definitions and the ISO mapping itself deviate from the schema of their version.

A practical note for anyone running CI runners on IPv6 only: github.com and its release downloads have no IPv6 address, raw.githubusercontent.com does. The check in my own repository therefore fetches only the dependency-free generator from raw.githubusercontent.com. Schema validation runs when the profile is generated and in the public repository's CI.

07 — What the profile now carries

The profile is small and unspectacular. That is exactly what makes it useful. Every further step can refer to it instead of determining the selection again.

08 — The series at a glance

  1. Grundschutz++ against my own documentation: the method, the catalogue as a data model, the comparison with IaC and GitOps.
  2. Applicability as data: the Statement of Applicability as an OSCAL profile. This post.
  3. Containers that say which requirements they meet: a signed component definition attached to every image the pipeline builds.
  4. Measurement as evidence: nightly checks write their results as OSCAL. Plus the second level, which finds in the running system what no repository knows about.
  5. 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 is running and measured. The code for it lands in grundschutz-worksheets first. The titles are linked here as soon as each post is online.

Frequently asked questions

Does this work without ISO 27001?+

Yes. The tailoring file only needs IDs and reasons. The generator does not care where the selection comes from. With ISO 27001, the library's mapping is a good starting point.

What happens when the BSI changes the catalogue?+

Nothing, as long as the profile points at a fixed commit. Whoever raises the commit generates the profile again. IDs that no longer exist in the new catalogue stop the generator. That way every change is noticed.

Does the profile replace the Statement of Applicability?+

No. The Statement of Applicability stays the document people read and approve. The profile is generated from it and never edited by hand. When the statement changes, the profile is generated again.

Do I need OSCAL to use Grundschutz++?+

No. The requirements can also be read as a worksheet. OSCAL pays off once several tools need the same selection: worksheets, components, measurements and a security plan.

Conclusion

The Statement of Applicability is the smallest OSCAL document with the biggest effect. It defines what everything else refers to. As a profile it is generated from your own documentation, checked against the catalogue and the schema, and compared again on every change. The format's gap for exclusions with a reason can be bridged cleanly until the security plan takes them over.

Next up, part 3: containers that say which requirements they meet.

I make your Statement of Applicability machine-readable. Generated from your documentation, checked against Grundschutz++.

Matching your Statement of Applicability against the state-of-the-art library, an OSCAL profile for your scope, and a pipeline that regenerates and checks it on every change.

Platform operations instead of paper consulting: on request I build the next steps too, from the component definition to the security plan.

Book a call →

About the author

Photo of Kai Ole Hartwig.

Kai Ole Hartwig

Freelance DevSecOps consultant · OnlyOle Consulting

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.