# 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](../spec/HEART-Part-9-Testing-and-Certification-1.0.md).

---

## 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

```sh
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

```sh
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`:

```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-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:

```sh
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.

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**:

| 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

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:

```sh
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`, `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

```sh
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 |
