HEART Embedded Systems logoHEART

HEART-Rating#

Test a HEART-Embedded System and rate it from 0 to 10.

The HEART-Rating has two parts:

PartWhat it doesWhere
The serviceA 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 clientA 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#

RatingLabelMeaning
9.5 to 10ExcellentEvery test passed, or almost every one
9.0 to 9.4Very goodEvery required test passed, and nearly all recommended ones
7.0 to 8.9GoodEvery required test passed
5.0 to 6.9FairAt least one required test failed
below 5PoorMany tests failed
3.9 or lessUnsafeA 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)

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#

OptionMeaning
--service URLThe HEART-Rating service. Or set HEART_RATING_SERVICE.
--metrics FILE_OR_URLUse a specific metrics file instead of the service's latest
--config FILETokens and hands-on results
--ca FILETrust this CA certificate, such as the owner's or maker's authority
--insecureSkip certificate checks. The TLS test is then reported as not run.
--allow-changesAllow tests that change the system (see below)
--json FILEAlso 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-changes on 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#

EndpointPurpose
GET /What this is, and how to use it
GET /v1/metrics/latestThe current metrics. Has an ETag and is cached for 5 minutes.
GET /v1/metrics/versionsEvery published version, newest first
GET /v1/metrics/{version}One version
GET /v1/client/heart_rating.pyThe client
GET /v1/client/versionClient version, and the client version the latest metrics need
POST /v1/scoreCompute a rating from {metricsVersion?, results: [{id, result}]}. Nothing is stored.
PUT /v1/metricsPublish new metrics. Needs the admin token.
GET /v1/healthLiveness

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.

  1. In the Cloudflare dashboard: Workers & Pages > Create > Worker, then Edit code.
  2. 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:

SettingTypeValue
ADMIN_TOKENSecretA long random value, for example from openssl rand -hex 32
METRICSKV namespace bindingCreate 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#

  1. Edit metrics/heart-metrics.json: add or change tests, and raise version (for example 2026.11.1 to 2026.12.1). Versions must always go up.
  2. If a test uses a new builtin the old client does not know, add it to the client, raise CLIENT_VERSION, and set minClient in the metrics to match.
  3. 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#

FieldMeaning
formatAlways heart-metrics/1
versionYYYY.M.N, always increasing
minClientOldest client that can run these tests
rating.weightsWeight for critical, must, should and may
rating.capscriticalFailed, mustFailed, requiredUntested, each with a maximum
rating.bandsLabels by minimum rating
testsThe 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, header with matches, or json (a dotted path) with equals, in, type, matches, exists, absent or onlyKeys. 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, with howTo instructions. 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.

PathWhat it is
worker/worker.jsThe Worker. One file to paste into Cloudflare. Generated.
metrics/heart-metrics.jsonThe metrics. The single source of truth.
client/heart_rating.pyThe client
src/index.js, src/rating.js, src/zip.jsThe Worker's code
tools/build-worker.mjsJoins the code, metrics, client and templates into worker/worker.js
test/Tests