Documentation menu

Reports

What a Settlens run writes, field by field, and how the SHA-256 integrity digest lets anyone detect a modified report.

In short

Each scan, facilitator or check run writes up to three files: a JSON report whose integrity block holds a SHA-256 digest of its canonical form, a self-contained HTML report for people and a SARIF 2.1.0 file for code scanning. verify-report recomputes the digest offline.

On this page

Every scan, facilitator and check run builds one report object and writes it in up to three formats, chosen with --format (all three by default), into the --out directory (reports by default). Files are named settlens-<reportId>.json, .html and .sarif. --no-write skips the files and prints the console summary only.

Report formats
FormatForContents
JSONEvidence, automation, verify-report, evidence attestThe complete report, with the integrity block.
HTMLPeople: engineers, partners, auditorsOne self-contained file: inline styles, no scripts, nothing loaded from the network.
SARIFCode scanning and SARIF viewersFailing and warning checks as SARIF 2.1.0 results.

JSON report

The JSON report follows the schema in schemas/settlens-report.schema.json, from which the tables below are generated. This excerpt keeps one of the 13 checks of a passive scan of the demo endpoint:

JSONsettlens-613cdc9b.json (excerpt)
{
  "schemaVersion": 1,
  "reportId": "613cdc9b",
  "product": "Settlens",
  "productVersion": "0.5.0",
  "protocol": "x402",
  "protocolVersion": 2,
  "integrations": { "officialX402Core": "2.28.0" },
  "generatedAt": "2026-10-13T11:14:32.642Z",
  "scope": { "targetCount": 1, "activeTargetCount": 0, "settlementCalls": 0, "privateKeysUsed": 0 },
  "metadata": { "source": "cli" },
  "summary": { "pass": 13, "fail": 0, "warn": 0, "skip": 0 },
  "targets": [
    {
      "id": "23c370f9",
      "name": "127.0.0.1",
      "target": { "url": "http://127.0.0.1:4402/secure", "method": "GET", "requestHeaderNames": [] },
      "mode": "passive",
      "request": {
        "status": 402,
        "durationMs": 12,
        "responseBytes": 28,
        "responseDigest": "bd3559c1cc8ba756860ad6f192140e6ecd6bc00fe6becbccdb6f14bc23d0b319",
        "responseBodyTruncated": false,
        "responseHeaderNames": ["connection", "content-type", "date", "keep-alive", "payment-required", "transfer-encoding"]
      },
      "checks": [
        {
          "id": "binding.resource_url",
          "title": "Payment requirement is bound to the requested resource",
          "status": "pass",
          "severity": "high",
          "message": "resource.url exactly matches the protected request URL.",
          "evidence": {
            "requested": "http://127.0.0.1:4402/secure",
            "advertised": "http://127.0.0.1:4402/secure"
          }
        }
      ]
    }
  ],
  "integrity": {
    "algorithm": "sha256",
    "canonicalization": "settlens-stable-json-v2",
    "digest": "cb591281b5875d2bb9a27c4d2ca290dd5da43c3ec60a9ef49cfa7878a6a39cbd"
  }
}
Top-level fields
FieldType and rulesDescription
schemaVersion1 (constant), requiredVersion of the report format.
reportIdstring, required
8 lowercase hex characters
Random identifier of the report, also used in the file names.
product"Settlens" (constant), requiredAlways Settlens.
productVersionstring, requiredVersion of the CLI that produced the report.
protocol"x402" (constant), requiredAlways x402.
protocolVersion2 (constant), requiredx402 protocol version assessed.
generatedAtstring, requiredISO 8601 time at which the report was assembled.
integrationsobject, requiredVersions of external components used during the run.
integrations.officialX402Corestring, requiredPinned @x402/core version used for SDK schema validation.
scopeobject, requiredWhat the run touched.
scope.targetCountinteger, required
≥ 1
Number of targets in the report.
scope.activeTargetCountinteger, required
≥ 0
Targets that received the authorized active probe.
scope.settlementCalls0 (constant), requiredCalls to a facilitator's /settle. Always 0.
scope.privateKeysUsed0 (constant), requiredPrivate keys used. Always 0.
metadataobject, requiredHow the run started: {"source":"cli"}, {"source":"config","configName":"settlens.config.json"}, {"source":"cli","assessmentType":"facilitator"} or {"source":"settlens-web-api"}.
summaryobject (see summary), requiredResult counts across every target.
targetsarray of object (see targetResult), required
1 to any number of items
One result per assessed target.
integrityobject, requiredDigest of the report without this block. See Integrity below.
integrity.algorithm"sha256" (constant), requiredHash function.
integrity.canonicalizationone of settlens-stable-json-v1, settlens-stable-json-v2, requiredHow the report is serialized before hashing.
integrity.digeststring, required
64 lowercase hex characters
SHA-256 of the canonical report, in hex.

Target result

Fields of each entry in targets
FieldType and rulesDescription
idstring, requiredRandom identifier of this target result.
namestring, requiredTarget label (config name, --name or the hostname).
targetobject, requiredThe request: sanitized url, method and requestHeaderNames (names only, never values).
modestring, requiredpassive, active-authorized (the probe ran) or passive-facilitator (facilitator command).
startedAtstring, requiredISO 8601 start time.
completedAtstring, requiredISO 8601 end time.
summaryobject (see summary), requiredResult counts for this target.
requestobject or nullFirst response: status, durationMs, responseBytes, responseDigest (SHA-256 of the body read), responseBodyTruncated and responseHeaderNames. null when the endpoint could not be reached.
checksarray of object (see check), requiredEvery check result for this target.
supportedobject or nullFacilitator command only: the /supported answer as parsed.
integrationsobjectWhen a facilitator was cross-checked: its URL and the number of kinds it supports.
authorizationobjectOnly when the active probe ran: { confirmed: true, reference } from --authorization-ref.

Check result

Fields of each entry in checks
FieldType and rulesDescription
idstring, required
matches ^[a-z0-9_.-]+$
Check id, documented in the check catalog.
titlestring, requiredShort name of the property checked.
statusone of pass, fail, warn, skip, requiredOutcome of the check.
severityone of info, low, medium, high, critical, requiredImpact if the check fails.
messagestring, requiredWhat was observed in this run.
evidenceany JSON valueCheck-specific observations, such as expected and actual status codes. Values come from the assessed server.
recommendationstringHow to fix it, on results that did not pass.

What each id means, and how to fix a failure, is in the check catalog.

Summary

Fields of summary objects
FieldType and rulesDescription
passinteger, required
≥ 0
Checks that passed.
failinteger, required
≥ 0
Checks that failed.
warninteger, required
≥ 0
Checks that raised a warning.
skipinteger, required
≥ 0
Checks that could not run.

What reports leave out

  • Values of the request headers you configure: only their names are kept.
  • Response bodies: only their size and SHA-256 digest.
  • Credentials in URLs, and the values of common secret query parameters such as token, api_key or signature.

Evidence fields do contain what the assessed server sent, for example the payment terms it advertised. Treat them as data from that server, not as instructions.

Integrity

The JSON report ends with an integrity block. Its digest is the SHA-256 of the whole report except that block, serialized with the canonicalization named in integrity.canonicalization. New reports use settlens-stable-json-v2: object keys sorted by UTF-16 code unit, arrays kept in order, values serialized as JSON, no whitespace. Reports written by Settlens 0.4.0 and earlier name settlens-stable-json-v1 and still verify. Pretty-printing or reordering keys in the file does not change the digest; changing any value does.

The digest is computed once when the report is created and recomputed by anyone who receives the file.

The digest proves that the content is unchanged since it was computed. It does not say who produced the report or when: anyone can edit a report and compute a new digest. To bind a report to a signing identity and a time, sign it with Settlens Evidence. Each run also gets a new reportId and timestamps, so two scans of the same endpoint never share a digest.

Verify a report

settlens verify-report (exit 0, then exit 1 for an edited copy)
settlens verify-report reports/settlens-613cdc9b.json
VALID Report content matches its SHA-256 evidence digest.
Expected: cb591281b5875d2bb9a27c4d2ca290dd5da43c3ec60a9ef49cfa7878a6a39cbd
Actual:   cb591281b5875d2bb9a27c4d2ca290dd5da43c3ec60a9ef49cfa7878a6a39cbd
settlens verify-report tampered.json
INVALID Report content was modified after the evidence digest was created.
Expected: cb591281b5875d2bb9a27c4d2ca290dd5da43c3ec60a9ef49cfa7878a6a39cbd
Actual:   8db417669d0df8842b0478668786774a7b17cf8a98e377a23b28575cf67c66bf

The same verification is available offline to AI agents through the verify_report tool of the MCP server. Without the CLI, these lines of Node.js reproduce it; run against the report above, they print VALID and the same digest:

JavaScriptverify-digest.mjs (node verify-digest.mjs report.json)
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";

// settlens-stable-json-v2: object keys sorted by UTF-16 code unit (the default sort),
// arrays kept in order, other values serialized with JSON.stringify, no whitespace.
// Reports whose integrity.canonicalization is settlens-stable-json-v1 sort keys with
// (a, b) => a.localeCompare(b, "en") instead.
function stable(value) {
  if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
  if (value !== null && typeof value === "object") {
    const keys = Object.keys(value).sort();
    return `{${keys.map((key) => `${JSON.stringify(key)}:${stable(value[key])}`).join(",")}}`;
  }
  return JSON.stringify(value);
}

const report = JSON.parse(readFileSync(process.argv[2], "utf8"));
const { integrity, ...content } = report;
const digest = createHash("sha256").update(stable(content)).digest("hex");
console.log(digest === integrity.digest ? "VALID" : "INVALID", digest);

Key order is UTF-16 code-unit order

settlens-stable-json-v2 sorts keys the way JavaScript's default sort() does, by UTF-16 code unit, with no locale involved. For ASCII keys this is byte order; a reimplementation in another language must compare UTF-16 code units, not code points, for characters outside the Basic Multilingual Plane. The older settlens-stable-json-v1 sorted with localeCompare under the en collation, which puts _ before letters and compares letters case-insensitively first. When in doubt, verify with settlens verify-report.

HTML report

The HTML file is meant for people. It shows the digest, the pass, fail and warning counts, then each target with its checks sorted by status (fail, warn, pass, skip) and severity. Each check shows its id, severity, message, recommendation and a collapsible evidence block. The file has inline styles only, no scripts and a noindex robots tag, so it can be attached to a ticket or opened offline. Its footer states that the report describes observed behavior and is not a guarantee of security or legal compliance.

SARIF

The SARIF file (version 2.1.0) lists failing and warning checks as results, with one rule per check id. Passing and skipped checks are left out.

How check results map to SARIF
Check resultSARIF levelsecurity-severity
fail, criticalerror9.5
fail, higherror8.0
fail, mediumwarning5.0
fail, lowwarning2.0
warn (any severity)warningrule severity: 9.5, 8.0, 5.0, 2.0 or 0.1 (info)
JSONsettlens-56940334.sarif (excerpt: one rule, one result)
{
  "version": "2.1.0",
  "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
  "runs": [
    {
      "tool": {
        "driver": {
          "name": "Settlens",
          "version": "0.5.0",
          "informationUri": "https://settlens.com",
          "rules": [
            {
              "id": "policy.network",
              "name": "policy_network",
              "shortDescription": { "text": "Networks match the local policy" },
              "fullDescription": { "text": "Unexpected networks: eip155:84532." },
              "help": { "text": "Restore the approved networks or update the reviewed Settlens policy intentionally." },
              "properties": { "tags": ["security", "x402", "agentic-payments"], "security-severity": "8.0" }
            }
          ]
        }
      },
      "automationDetails": { "id": "settlens/56940334" },
      "invocations": [
        { "executionSuccessful": true, "properties": { "reportDigest": "d5576349e7b85404e354b15b17635e2f31cd2a3d63a42569ca5c0ac957fa2252" } }
      ],
      "results": [
        {
          "ruleId": "policy.network",
          "level": "error",
          "message": { "text": "Unexpected networks: eip155:84532. Target: GET http://127.0.0.1:4402/drift" },
          "locations": [
            {
              "physicalLocation": {
                "artifactLocation": { "uri": "settlens.config.json", "uriBaseId": "%SRCROOT%" },
                "region": { "startLine": 1 }
              }
            }
          ],
          "partialFingerprints": { "primaryLocationLineHash": "651267a8258e3446:1" },
          "properties": {
            "status": "fail",
            "severity": "high",
            "target": "http://127.0.0.1:4402/drift",
            "recommendation": "Restore the approved networks or update the reviewed Settlens policy intentionally.",
            "reportId": "56940334"
          }
        }
      ]
    }
  ]
}

Findings concern a URL, not a line of code, but code scanning only displays results that have a location. Each result is therefore anchored at line 1 of the configuration file that declared the target: the --config path relative to the working directory for check, and settlens.config.json for an ad hoc scan such as this one. partialFingerprints.primaryLocationLineHash is computed from the check id and the target, so the same rule failing on two targets stays two alerts. The target URL is in the message and in properties.target, and the run's invocations[0].properties.reportDigest ties the SARIF file to its JSON report. How to upload it from a GitHub workflow is covered on the GitHub Action page.

Console summary

Every run also prints a summary: the report id, the scope counters, then for each target its counts and every check that did not pass, failures first. Colors are used only on a terminal and never when NO_COLOR is set. Examples are on the CLI reference.

Sources

  1. Static Analysis Results Interchange Format (SARIF) Version 2.1.0OASIS · docs.oasis-open.org
  2. SARIF support for code scanningGitHub Docs · docs.github.com
  3. FIPS 180-4: Secure Hash Standard (SHS)NIST · nvlpubs.nist.gov

Last updated