ISO 27001 · macOS · 100% open source

Compliant laptops. Without spying on your team.

Prüfstein checks every employee Mac against your ISO 27001 controls. Nothing runs in the background, nothing is filed until the person says so, and the fleet data never leaves you.

Devices
macOS today
Licence
Apache-2.0
Runs on
Your infrastructure
pruefstein-agent
$ pruefstein-agent runRunning compliance checks on device 4C4C4544 (user: markus-mbp)  [PASS] FileVault enabled  [PASS] Screen lock requires a password  [FAIL] Firewall enabled  [PASS] Gatekeeper enabled  [PASS] System Integrity Protection enabled  [FAIL] Remote login (SSH) disabled  [PASS] No blacklisted applications installed────────────────────────────────────────────Done: 17/19 checks passedReporting as non-compliant on 25 Sep 2026 unless fixed.Report this run? [y/N] 

A real run. Checking costs nothing and can be repeated all day. Nothing is on record until you answer that last question.

How it works

Four moves, and none of them is a spreadsheet

A Prüfstein deployment is a web app your admins use and a command your colleagues run. Nothing sits resident on anybody's laptop.

  1. 01

    Define the control

    An admin writes a Compliance Item: a name, an osquery query, and a JEXL expression that must evaluate to true. It sits in a group that maps to an ISO 27001 Annex A control family.

    Web app · admin

  2. 02

    The agent pulls the list

    On each laptop, pruefstein-agent fetches the current checks over an OIDC-authenticated call and runs every query through the locally installed osquery.

    GET /api/checks

  3. 03

    Evaluate locally, decide locally

    Each expression is evaluated against the full JSON osquery returned. You see pass or fail in your own terminal before anyone else does, and can fix and re-run as often as you like.

    osqueryi --json

  4. 04

    Report, when you say so

    Say yes and the run is filed against your account and that hostname. The dashboard updates, the cycle closes, and the outcome mail goes out.

    POST /api/reports

Trust, not surveillance

It assumes your colleagues are not the threat

Most endpoint tooling is written as though the person holding the laptop is the adversary. Prüfstein starts from the opposite premise: people want their machine to be compliant, and mostly need to be told what is wrong and how to fix it.

  • Nothing runs behind your back

    No daemon, no kernel extension, no MDM enrolment. The agent starts when somebody starts it, does its work and exits. There is nothing resident on the machine to wonder about.

  • You see the result first

    Queries run on your machine and expressions are evaluated there. Pass and fail land in your own terminal before they land on anybody’s dashboard.

  • Filing is your yes

    Anything short of an explicit “y” is a no, a bare Enter included. Run the checks as often as you like while you fix what they found; nothing is on record until you say so.

  • The check is readable before it runs

    You can read the SQL that will run on your machine and the expression that will judge it. There is no opaque agent behaviour to take on faith and no undisclosed telemetry.

  • You only see your own

    A regular account sees its own reports and nobody else’s. The fleet view belongs to the people who need it for the ISMS, not to everyone with a login.

  • The deadline is stated, not sprung

    The agent prints the date a run would go on record as non-compliant, and a reminder arrives by mail before it does. Nothing about the clock is hidden.

Trust is not the absence of accountability. Someone can decline to report, and if the reporting window closes with nothing filed, the server records a MISSING report for that device by itself and opens the next cycle. The deadline does the enforcing, so the tool never has to watch anybody.

Anatomy of a check

A control is a query and one expression

No rule builder, no proprietary policy language, no agent-side magic. A Compliance Item is SQL against the osquery schema plus a JEXL expression that has to come out true. That is the whole abstraction.

A.10 Cryptography

FileVault enabled

Pass
1 · osquery SQL
SELECT filevault_status
  FROM disk_encryption
 WHERE filevault_status = 'on'
 LIMIT 1;
2 · osqueryi --json returned
[
  { "filevault_status": "on" }
]
3 · JEXL expression, must be true
results.size() > 0

In words: Passes when a row comes back at all. The volume is encrypted.

Auditable

Your auditor does not have to trust a vendor's definition of "encrypted". They can read the query and the expression and decide for themselves.

Evidence kept

The full JSON osquery returned is stored with the result, so a verdict can be re-argued months later against what the machine actually said.

Yours to change

Every seeded check is an ordinary row. Edit the SQL, loosen the threshold, or delete it. Upgrades will not overwrite what you have changed.

What it does

Everything the compliance part of an ISMS actually needs

Not an MDM, not an EDR, not a policy PDF generator. One narrow job, done end to end: proving that endpoints meet the controls you wrote down.

Checks

A control is a query, not a checkbox

Every check is osquery SQL plus one JEXL expression over the JSON result. Anybody who reads SQL can audit what the control actually asserts. Your auditor included.

Catalog

19 macOS checks on first boot

FileVault, firewall, Gatekeeper, SIP, screen lock, automatic updates, remote login, sharing services. Seeded against permanent keys, so upgrades never duplicate or overwrite what you have edited. Windows and Linux are not covered yet.

Blocked apps

One rule per application

An app arrives as a Homebrew cask and an .app bundle; one rule carries every matcher it needs (bundle id, app name, Homebrew formula), so the admin sees one row and one reason rather than four patterns.

Agent

One small binary per machine

A Java CLI that builds to a native image and starts in about 20 ms. It runs osqueryi, evaluates the expressions locally and posts the result. No daemon, no kernel extension, no MDM.

Consent

Checking is free. Reporting is deliberate.

Running the checks tells you where you stand and costs nothing. Filing the report is a separate, explicit yes, so a laptop can be checked and fixed as often as it takes before anything lands on a dashboard.

Identity

Device-flow OIDC, no agent config

The agent carries no identity configuration. It asks the server for the issuer, client id and scopes, then discovers the endpoints itself. The same binary works against Keycloak and Microsoft Entra ID without a rebuild.

Dashboard

The fleet at a glance

Compliant, non-compliant, missing and pending, counted and filterable. Drill into any run to see the raw osquery JSON that produced the verdict.

Cycles

Deadlines that enforce themselves

One reporting workflow runs per device at all times. Report inside the window and the cycle closes; miss it and a MISSING report is filed automatically and the next cycle starts.

Mail

Told, not surprised

People get the outcome of their own run and a reminder before a deadline. Sending is fire-and-forget by design: an SMTP server that is down must never fail a report upload.

Inventory

It knows what is installed

Each run also takes stock of the machine: application bundles a person installed, plus Homebrew formulae and casks. That gives a blocked-app rule something to match, and answers "who still has that?" without anybody being asked.

Security

Deny by default

Unannotated endpoints are refused at build time, not at review time. Two roles: people see their own reports, admins see the fleet and hold the CRUD.

Devices

People have more than one laptop

One account, many machines. Each device reports independently under its own hostname and carries its own compliance state.

Batteries included

19 checks are already there when you start

A fresh deployment seeds its own catalog against permanent keys, mapped to the Annex A families they serve. Nothing here is a placeholder. This is what the first dashboard shows.

A.9 Access Control

  • Screen lock timeout ≤ 300 seconds
  • Screen lock requires a password
  • Automatic login disabled
  • Guest account disabled

A.10 Cryptography

  • FileVault enabled

A.12 Operations Security

  • Firewall enabled
  • Firewall logging enabled
  • Automatic updates enabled
  • Critical security updates installed automatically
  • macOS updates installed automatically
  • Gatekeeper enabled
  • System Integrity Protection enabled
  • Time Machine backup destination configured

A.13 Communications Security

  • Remote login (SSH) disabled
  • Screen sharing disabled
  • File sharing disabled
  • Internet sharing disabled
  • Firewall stealth mode enabled

A.12.6.2 Software installation

  • No blacklisted applications installed

Its SQL is generated from your blocked-app rules at request time.

Straight answer: the seeded catalog is macOS to the last row. The agent runs anywhere osquery runs, and the model has nothing macOS-specific in it. But the Windows and Linux equivalents are not written yet. That is the most useful thing you could contribute.

AI assistance · optional

It writes the check, and it explains the failure

Two jobs at opposite ends of the same problem: getting a control written in the first place, and telling somebody in plain words how to fix the one that just went red. Both are optional, and both run against a model you choose.

Create

Describe the control, get the check

Write what you want in plain English. Back comes a name, an osquery query and the JEXL expression that judges it, generated against the osquery schema that ships with the app rather than from memory.

  • For a blocked application you get the bundle identifiers, Homebrew formula and cask names and .app filenames that recognise it however it arrived.
  • It writes the policy reason too, which is the line your A.12.6.2 audit trail needs.
  • The prompt is told to return an empty list rather than a plausible guess. An invented bundle id silently matches nothing and looks like coverage.
Fix

Explain the failure in plain words

A failed check gets a short headline and a step-by-step remedy written against the osquery output the machine actually returned, not against the check’s description.

  • A blacklisted app gets the exact removal step for how it was really installed: brew uninstall --cask, or the bundle path to move to the Trash.
  • Installed by both routes? It is told to give both steps, because removing one leaves the other behind.
  • Explanations are written on a schedule outside the report transaction, so a slow model never holds up an upload.

Bring your own model

No model ships with it, and none is assumed

Prüfstein talks to an OpenAI-compatible endpoint through LangChain4j. Set the key, name the model, and if you are not using OpenAI point the base URL wherever you like: a hosted provider, vLLM in your own VPC, Ollama on a workstation. Nothing about the fleet leaves your network unless you decide it does.

Set no key and the feature simply stays dark. Explanations never appear; reports, mails, cycles and the dashboard carry on exactly as before. The AI is a convenience on top of the product, never a dependency inside it.

Configuration
# Any OpenAI-compatible endpoint. Your key, your model.
OPENAI_API_KEY=sk-…
QUARKUS_LANGCHAIN4J_OPENAI_CHAT_MODEL_MODEL_NAME=gpt-4o-mini

# Self-hosted instead? Point it somewhere else.
QUARKUS_LANGCHAIN4J_OPENAI_BASE_URL=http://ollama.internal:11434/v1
QUARKUS_LANGCHAIN4J_OPENAI_CHAT_MODEL_MODEL_NAME=llama3.1

100% open source

All of it. Not a core, not a community edition.

Prüfstein is one repository under Apache-2.0. EXP Software GmbH builds it and publishes all of it. There is no second codebase holding the good features back for an edition somebody pays for.

No paid tier

There is no enterprise edition, no seat count and no feature behind a licence key. EXP Software GmbH builds Prüfstein and gives all of it away. What you clone is what exists.

No hosted service

Nobody runs Prüfstein for you, so nobody else holds a list of which of your laptops are unencrypted. That is on purpose.

Your data stays yours

Postgres, your infrastructure, your OIDC provider. The only outbound call the app can make is to a model provider, and only if you configure a key.

Readable by design

A check is SQL and one expression. You can verify what the tool asserts about your fleet without trusting the tool.

Run the whole thing in two terminals

Dev Services start Postgres and a seeded Keycloak realm for you, so there is no infrastructure to arrange before you can see a dashboard. You need JDK 25, Docker or Podman, and osquery on your PATH.

Quickstart
git clone https://github.com/d135-1r43/pruefstein.git
cd pruefstein

# terminal 1: Dev Services bring up Postgres and Keycloak
cd web && ./mvnw quarkus:dev

# terminal 2: build, link and run the agent
./agent/bin/install.sh
pruefstein-agent login --server http://localhost:8080
pruefstein-agent run

Contribute

The best contribution is a check you already know

Prüfstein is small and early, and the gaps are not secret. Here is what would help, roughly in order of how far it gets on how little effort.

No contribution is too small to matter here

A typo in an expression, a check that is wrong on the newest macOS, a paragraph of setup notes. All of it lands. Open an issue before a large pull request so nobody builds the same thing twice.