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.
| Format | For | Contents |
|---|---|---|
| JSON | Evidence, automation, verify-report, evidence attest | The complete report, with the integrity block. |
| HTML | People: engineers, partners, auditors | One self-contained file: inline styles, no scripts, nothing loaded from the network. |
| SARIF | Code scanning and SARIF viewers | Failing 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:
{
"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"
}
}
| Field | Type and rules | Description |
|---|---|---|
| schemaVersion | 1 (constant), required | Version of the report format. |
| reportId | string, required 8 lowercase hex characters | Random identifier of the report, also used in the file names. |
| product | "Settlens" (constant), required | Always Settlens. |
| productVersion | string, required | Version of the CLI that produced the report. |
| protocol | "x402" (constant), required | Always x402. |
| protocolVersion | 2 (constant), required | x402 protocol version assessed. |
| generatedAt | string, required | ISO 8601 time at which the report was assembled. |
| integrations | object, required | Versions of external components used during the run. |
| integrations.officialX402Core | string, required | Pinned @x402/core version used for SDK schema validation. |
| scope | object, required | What the run touched. |
| scope.targetCount | integer, required ≥ 1 | Number of targets in the report. |
| scope.activeTargetCount | integer, required ≥ 0 | Targets that received the authorized active probe. |
| scope.settlementCalls | 0 (constant), required | Calls to a facilitator's /settle. Always 0. |
| scope.privateKeysUsed | 0 (constant), required | Private keys used. Always 0. |
| metadata | object, required | How the run started: {"source":"cli"}, {"source":"config","configName":"settlens.config.json"}, {"source":"cli","assessmentType":"facilitator"} or {"source":"settlens-web-api"}. |
| summary | object (see summary), required | Result counts across every target. |
| targets | array of object (see targetResult), required 1 to any number of items | One result per assessed target. |
| integrity | object, required | Digest of the report without this block. See Integrity below. |
| integrity.algorithm | "sha256" (constant), required | Hash function. |
| integrity.canonicalization | one of settlens-stable-json-v1, settlens-stable-json-v2, required | How the report is serialized before hashing. |
| integrity.digest | string, required 64 lowercase hex characters | SHA-256 of the canonical report, in hex. |
Target result
| Field | Type and rules | Description |
|---|---|---|
| id | string, required | Random identifier of this target result. |
| name | string, required | Target label (config name, --name or the hostname). |
| target | object, required | The request: sanitized url, method and requestHeaderNames (names only, never values). |
| mode | string, required | passive, active-authorized (the probe ran) or passive-facilitator (facilitator command). |
| startedAt | string, required | ISO 8601 start time. |
| completedAt | string, required | ISO 8601 end time. |
| summary | object (see summary), required | Result counts for this target. |
| request | object or null | First response: status, durationMs, responseBytes, responseDigest (SHA-256 of the body read), responseBodyTruncated and responseHeaderNames. null when the endpoint could not be reached. |
| checks | array of object (see check), required | Every check result for this target. |
| supported | object or null | Facilitator command only: the /supported answer as parsed. |
| integrations | object | When a facilitator was cross-checked: its URL and the number of kinds it supports. |
| authorization | object | Only when the active probe ran: { confirmed: true, reference } from --authorization-ref. |
Check result
| Field | Type and rules | Description |
|---|---|---|
| id | string, required matches ^[a-z0-9_.-]+$ | Check id, documented in the check catalog. |
| title | string, required | Short name of the property checked. |
| status | one of pass, fail, warn, skip, required | Outcome of the check. |
| severity | one of info, low, medium, high, critical, required | Impact if the check fails. |
| message | string, required | What was observed in this run. |
| evidence | any JSON value | Check-specific observations, such as expected and actual status codes. Values come from the assessed server. |
| recommendation | string | How 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
| Field | Type and rules | Description |
|---|---|---|
| pass | integer, required ≥ 0 | Checks that passed. |
| fail | integer, required ≥ 0 | Checks that failed. |
| warn | integer, required ≥ 0 | Checks that raised a warning. |
| skip | integer, 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_keyorsignature.
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 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 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:
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.
| Check result | SARIF level | security-severity |
|---|---|---|
| fail, critical | error | 9.5 |
| fail, high | error | 8.0 |
| fail, medium | warning | 5.0 |
| fail, low | warning | 2.0 |
| warn (any severity) | warning | rule severity: 9.5, 8.0, 5.0, 2.0 or 0.1 (info) |
{
"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
Last updated