From the Assure repository at 262612b1426d, rendered as written. Source · Markdown

Route 3: HTTP endpoints

12. Checking HTTP endpoints

http-observed-baseline checks one HEAD response per endpoint you select against the HTTP obligations Assure holds (response framing, transport policy, cookies and similar). Live collection against real endpoints is not in this release: the Action reads response heads you captured, offline. connection is refused for this profile, before anything is read.

Give the Action an artefacts folder that holds:

  • manifest.json, with these ten keys exactly: "schema_version": "http-fixture-001", "mode": "offline_fixture", endpoints, and the seven flags at the values shown below (execution_authorized, hardware_authorized, industrial_release_authorized, release_allowed, physical_validation and self_approved false; simulation true). Any other key, a missing key or another value makes the Action stop before anything is sent (MANIFEST-INVALID). endpoints is a list of 1 to 16 objects with exactly these keys: id (endpoint-NNN, three digits), scope_token (a lower-case name you choose, distinct per endpoint), scheme, host, port, path, response_file (the head file's name in the folder) and complete (true when you captured the whole head);
  • one file per endpoint holding the raw response head as received: the status line and every header line, each ending in CRLF, and the empty line that ends the head.

A complete manifest.json for two endpoints:

{
  "schema_version": "http-fixture-001",
  "mode": "offline_fixture",
  "endpoints": [
    {"id": "endpoint-001", "scope_token": "checkout", "scheme": "https", "host": "shop.example.test", "port": 443,
     "path": "/checkout", "response_file": "head-001.txt", "complete": true},
    {"id": "endpoint-002", "scope_token": "login", "scheme": "https", "host": "shop.example.test", "port": 443,
     "path": "/login", "response_file": "head-002.txt", "complete": true}
  ],
  "execution_authorized": false,
  "hardware_authorized": false,
  "industrial_release_authorized": false,
  "release_allowed": false,
  "physical_validation": false,
  "simulation": true,
  "self_approved": false
}
      - name: Assure check
        uses: Symbolia-Assurance/assure-action@beta
        with:
          api-key: ${{ secrets.ASSURE_API_KEY }}
          profile: http-observed-baseline
          artefacts: http-fixture

The inputs: profile: http-observed-baseline; artefacts, the folder above. scope is optional: a JSON file listing the endpoint ids to check, which must name exactly the manifest's ids (the Action stops before collecting otherwise). Leave scope-binding unset so the Action produces it: before collection it runs the identity producer and then the collector over your manifest, and sends their record as the scope binding with the endpoint tokens. identity-key stays pipe, its default: the Action makes a fresh key for each job and passes it on a pipe, so no key is an input, an environment variable or a file.

A scope file, when you give one, is a JSON list of the manifest's ids, for example ["endpoint-001", "endpoint-002"].

Replace the example endpoint values with the operator's own; keep the top-level keys and the seven flags as shown.

Capturing the response heads

Capture each head with curl, one HEAD request over HTTP/1.1, and write it to the file the manifest names:

mkdir -p http-fixture
curl -sS --http1.1 -I https://shop.example.test/checkout > http-fixture/head-001.txt
curl -sS --http1.1 -I https://shop.example.test/login > http-fixture/head-002.txt
head -n 1 http-fixture/head-001.txt

-I sends a HEAD request and writes the head as received, every line ending in CRLF, with the empty line that ends it. --http1.1 matters: the first line of each head must be an HTTP/1.1 status line, such as HTTP/1.1 200 OK. A head whose first line is another version, such as HTTP/2 200 from a capture without --http1.1, or HTTP/1.0 200 OK from a server that answers in HTTP/1.0, is recorded as not observed. Without -L, curl does not follow a redirect, so a 3xx head is the endpoint's own answer.

The host in each manifest endpoint must be the same host as the URL you capture with curl.

Set complete to true for each head you captured this way.

A complete workflow for the HTTP profile captures the heads in the job. Commit http-fixture/manifest.json alone; one step per endpoint runs curl and writes that endpoint's head beside it. Every run then checks fresh heads, and no captured head is committed to your repository.

name: Assure HTTP

on:
  pull_request:
  push:
    branches:
      - main
  schedule:
    - cron: "17 3 * * *"
  workflow_dispatch:

permissions:
  contents: read

jobs:
  http:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - name: Check out the repository
        uses: actions/checkout@<full commit sha of the release you trust>

      - name: Set up Python 3.14
        uses: actions/setup-python@<full commit sha of the release you trust>
        with:
          python-version: "3.14"

      - name: Capture the head of /checkout
        run: curl -sS --http1.1 -I https://shop.example.test/checkout > http-fixture/head-001.txt

      - name: Capture the head of /login
        run: curl -sS --http1.1 -I https://shop.example.test/login > http-fixture/head-002.txt

      - name: Assure check
        id: assure
        uses: Symbolia-Assurance/assure-action@beta
        with:
          api-key: ${{ secrets.ASSURE_API_KEY }}
          profile: http-observed-baseline
          artefacts: http-fixture
          fail-on: fails
          output: assure-verdict.json

      - name: Keep the verdict
        if: always()
        uses: actions/upload-artifact@<full commit sha of the release you trust>
        with:
          name: assure-verdict
          path: ${{ steps.assure.outputs.verdict-path }}

Each run is a new check, even when the heads are the same as the last run's, byte for byte: the Action makes a fresh identity key for each job, so the endpoint tokens it sends differ from run to run.

You can instead commit the captured heads with the manifest and leave out the capture steps. Use this offline route when you must reproduce a check on a fixture that does not change, for example heads you captured once to keep as a record.

Every reading summary carries the profile's three scope notes:

  • Readings cover one HEAD request per operator-selected endpoint.
  • Evidence freshness and response authenticity are unverified; same-selection replay is not excluded.
  • Per-endpoint coverage qualifications are not displayed in this report.

A full read of the two https endpoints above has the first line "green: 2 of 2 endpoints observed; 4 of 5 machines read, 1 refused; declared premises: 12; version pins: none; nothing disproven; established: 3 of 12; vacuous: 3; not established: 6 — top action: collect the facts HTTP-TLS10-NEGOTIATION reads": the TLS machine (M2) is refused with "no usable observed machine premise", because a captured head carries no TLS session. That is a bound of the result, so the run exits 0. For http:// endpoints M2 reads vacuous and the first line is "green: 2 of 2 endpoints observed; 5 of 5 machines read; declared premises: 12; version pins: none; nothing disproven; established: 3 of 12; vacuous: 6; not established: 3 — top action: declare redirects in raw/HTTP-INTENT.json beside the collected files (docs/customer/VERDICTS.md); requirements.md cannot state an HTTP requirement in this release". A head that carries both Content-Length and Transfer-Encoding fails HTTP-FRAMING-CL-TE, and under the default fail-on: fails the job exits 1, red. Those three not-established rows are HTTP-REDIRECT-POLICY, HTTP-RESPONSE-POLICY-ADEQUACY and HTTP-COOKIE-POLICY: an HTTP-INTENT.json you write beside manifest.json (issue #2381; its shape is in docs/customer/VERDICTS.md) is sent as raw/HTTP-INTENT.json and decides each row whose key it carries (redirects, response_headers, cookies) instead of reading it needs_intent.

policy.reason takes exactly one of these eight values, the serving core's closed list, in the source's order (the policy decides them in the order VERDICTS.md gives):

  • never (0, as the readings give) when your fail-on is never; a prove-class obligation that is not proven still exits 1 as not_proven.
  • could_not_look (3, yellow) when nothing could be read at all: the input gate stopped the check, no machine was read, or no selected unit was observed.
  • disproven (1, red) when a reading on a read machine has a status your fail-on names; a disproof your fail-on leaves out keeps this reason, red, with exit 0.
  • not_proven (1, red) when a prove-class obligation has no holds on a machine that was read, under every fail-on.
  • unresolved_strict (1, red) when under fail-on: unresolved-security, an obligation marked security: true is unresolved.
  • stopped_by_choice (1, red) when a reading has a status that is not a verdict and that your fail-on names, such as not_observed.
  • nothing_disproven (0, green) when nothing is disproven, whatever endpoints or machines could not be read.
  • platform_owned (0, red) when every disproof your fail-on names is platform-owned, someone else's to fix.

A fails reading whose findings are all platform-owned keeps the colour red with exit 0 and policy.reason platform_owned: the disproof is someone else's to fix.

Choose endpoints that answer HEAD with 2xx or 3xx; a 4xx or 5xx endpoint hides policy checks for the whole run and the verdict's warnings names them. No HTTP requirement can be stated in this release: a requirements.md sentence on this profile cannot hold. HTTP-REDIRECT-POLICY, HTTP-RESPONSE-POLICY-ADEQUACY and HTTP-COOKIE-POLICY ask for the HTTP-INTENT.json key that decides each (issue #2381); the other readings inform and ask nothing of you.

The verdict's observed_facts holds, for each observed endpoint, the facts its readings used (the status code and the HSTS, CSP and cookie attributes, never a cookie value or a redirect URL), so you can explain a reading without collecting again.

Before the run, check that the first line of each captured head begins with HTTP/1.1.

What is sent, and what is never read

For a profile whose inputs name no PostgreSQL configuration or pg_hba.conf file, such as the HTTP profile, nothing is withheld: the files are sent as read, and the line says "nothing to withhold — the withholding pass covers PostgreSQL configuration and pg_hba files and the SQL literals in the catalog snapshot and the declaration, and this profile's inputs name none of them".