HEART-Rating#
Test a HEART-Embedded System and rate it from 0 to 10.
The HEART-Rating has two parts:
| Part | What it does | Where |
|---|---|---|
| The service | A Cloudflare Worker. It publishes the latest metrics (the list of tests and the rating formula), hands out the client, and can compute a rating from results. | worker/ |
| The client | A single Python file. It downloads the latest metrics, runs the tests against your system from your own network, and prints the rating. | client/heart_rating.py |
The metrics live in one file, metrics/heart-metrics.json. They change often, as the standard and its threats change, and each version has a number such as 2026.11.1. A rating always names its metrics version.
The rules for the rating itself are in Part 9, Section 8.1.
What the numbers mean#
| Rating | Label | Meaning |
|---|---|---|
| 9.5 to 10 | Excellent | Every test passed, or almost every one |
| 9.0 to 9.4 | Very good | Every required test passed, and nearly all recommended ones |
| 7.0 to 8.9 | Good | Every required test passed |
| 5.0 to 6.9 | Fair | At least one required test failed |
| below 5 | Poor | Many tests failed |
| 3.9 or less | Unsafe | A critical security test failed |
In short: each test has a weight (critical 5, must 3, should 1, may 0.5). The score is the share of weight passed, times 10. Then caps apply: one failed critical test caps the rating at 3.9, one failed must test at 6.9, and any required test not run at 8.9 (marked provisional).
For testers: rate a system#
1. Get the client#
curl -O https://<your-service>/v1/client/heart_rating.py
It needs only Python 3.9 or later. Optionally, pip install cryptography lets it verify Card signatures. Without it, that test is reported as not run.
2. Run it#
python3 heart_rating.py https://device.local:43278 --service https://<your-service>
The target can be https://host:port, http://127.0.0.1:43278 for a system on your own machine, or heart://host, which tries port 43278 and then 7685.
The output lists every test as PASS, FAIL, ---- (not run) or n/a (does not apply), with the reason for anything that did not pass. The last line is the rating:
HEART-Rating: 8.4 / 10 Good (provisional)
3. Add tokens and hands-on results (recommended)#
Many tests need login tokens, and some can only be done by a person. Put both in a config file and pass it with --config tester.json:
{
"tokens": {
"reader": "token: internal clearance, heart.read, aud = the system",
"wrongAudience": "same, but aud = a different system",
"expired": "same, but already expired",
"boundNoProof": "a DPoP-bound token (with cnf.jkt), sent without its proof",
"ai": "heart_caller ai, scopes heart.read heart.admin heart.invoke",
"agent": "heart_caller human, but with an act claim (an AI agent), scope heart.approve",
"human": "heart_caller human, scopes heart.read heart.admin heart.approve"
},
"manual": {
"P2-A1-1": "pass",
"P6-SURVIVE": "fail",
"P4-LINE-BRIDGE": "not-applicable"
}
}
Get the tokens from the system owner's authorisation server. A test whose token is missing is reported as not run, never as passed. Hands-on results are taken as you write them (pass, fail, or not-applicable for a test that does not apply to the product), so record only tests you actually did. The test IDs and weights are those of Part 9, Section 6.
Options#
| Option | Meaning |
|---|---|
--service URL | The HEART-Rating service. Or set HEART_RATING_SERVICE. |
--metrics FILE_OR_URL | Use a specific metrics file instead of the service's latest |
--config FILE | Tokens and hands-on results |
--ca FILE | Trust this CA certificate, such as the owner's or maker's authority |
--insecure | Skip certificate checks. The TLS test is then reported as not run. |
--allow-changes | Allow tests that change the system (see below) |
--json FILE | Also write the full report as JSON |
Tests that change the system#
Three security tests (SEC-05, SEC-06, SEC-07) need an action waiting for approval. With --allow-changes, the client asks the system to add two disabled probe Playbooks, named heart-rating-probe-a and -b, whose trigger never happens. It then tries to approve them in ways that should be refused. Afterwards it cancels anything still waiting and deletes the probe rules.
Without --allow-changes these tests are reported as not run, and the rating is provisional.
Only test systems you own, or have the owner's written permission to test. Never run
--allow-changeson a system controlling real equipment that is in use.
Checking a rating#
The service can recompute a rating from the client's JSON report, so anyone can check the arithmetic:
python3 -c "import json; r=json.load(open('report.json')); print(json.dumps({'metricsVersion': r['metricsVersion'], 'results': [{'id': x['id'], 'result': x['result']} for x in r['results']]}))" > results.json
curl -X POST --data @results.json https://<your-service>/v1/score
The service#
| Endpoint | Purpose |
|---|---|
GET / | What this is, and how to use it |
GET /v1/metrics/latest | The current metrics. Has an ETag and is cached for 5 minutes. |
GET /v1/metrics/versions | Every published version, newest first |
GET /v1/metrics/{version} | One version |
GET /v1/client/heart_rating.py | The client |
GET /v1/client/version | Client version, and the client version the latest metrics need |
POST /v1/score | Compute a rating from {metricsVersion?, results: [{id, result}]}. Nothing is stored. |
PUT /v1/metrics | Publish new metrics. Needs the admin token. |
GET /v1/health | Liveness |
The service never contacts the systems being tested, and keeps no record of results.
Deploy it#
The whole service is one file, worker/worker.js, with the metrics, client and templates built in.
- In the Cloudflare dashboard: Workers & Pages > Create > Worker, then Edit code.
- Paste the whole of
worker/worker.js, and Deploy.
No variables or secrets are needed to serve metrics, the client, templates and ratings.
To publish new metrics without pasting the file again, add both of these under the Worker's Settings:
| Setting | Type | Value |
|---|---|---|
ADMIN_TOKEN | Secret | A long random value, for example from openssl rand -hex 32 |
METRICS | KV namespace binding | Create a KV namespace, and bind it with the variable name METRICS |
The Worker reads no other variables. Do not add keys it does not use.
Publish new metrics#
- Edit
metrics/heart-metrics.json: add or change tests, and raiseversion(for example2026.11.1to2026.12.1). Versions must always go up. - If a test uses a new builtin the old client does not know, add it to the client, raise
CLIENT_VERSION, and setminClientin the metrics to match. - Publish:
curl -X PUT https://<your-service>/v1/metrics \
-H "Authorization: Bearer $ADMIN_TOKEN" \
--data @metrics/heart-metrics.json
The service checks the file before accepting it. Old versions stay available, so old ratings can still be checked.
The metrics file#
| Field | Meaning |
|---|---|
format | Always heart-metrics/1 |
version | YYYY.M.N, always increasing |
minClient | Oldest client that can run these tests |
rating.weights | Weight for critical, must, should and may |
rating.caps | criticalFailed, mustFailed, requiredUntested, each with a maximum |
rating.bands | Labels by minimum rating |
tests | The tests (below) |
Each test has an id, a title, the part of the standard it checks, a requirement and a kind:
http: one request, and a list of expectations:status,headerwithmatches, orjson(a dotted path) withequals,in,type,matches,exists,absentoronlyKeys. Most new tests can be added this way, with no client update.builtin: a check written in the client, for things one request cannot test, such as TLS or signature checks.manual: a hands-on test, withhowToinstructions. The result comes from the tester's config file.
Optional fields: appliesTo (minLevel, module, binding, minAutonomy, network), requires (tests that must pass first), and changesState.
Development#
npm test # rebuilds worker/worker.js, then runs 16 tests against it
npm run build # just rebuilds worker/worker.js
worker/worker.js is generated: edit the sources below, then rebuild. The tests load the generated file itself, the one you paste, and include a check that its formula and the Python client's give identical ratings for 200 random sets of results.
| Path | What it is |
|---|---|
worker/worker.js | The Worker. One file to paste into Cloudflare. Generated. |
metrics/heart-metrics.json | The metrics. The single source of truth. |
client/heart_rating.py | The client |
src/index.js, src/rating.js, src/zip.js | The Worker's code |
tools/build-worker.mjs | Joins the code, metrics, client and templates into worker/worker.js |
test/ | Tests |