Skip to content

Evidence and attestation

Signing reports, binding them to an audit trail, and knowing what a report can leak.

On this page

Reports are compliance evidence. They leave the runner, pass through object stores, tickets, and inboxes, and are read months later. This guide covers what binds a report to its contents, how to verify one, and what a report is allowed to carry.

What is attested

Every non-terminal report format carries an attestation.

{
  "v": "1.1",
  "sha256": "9c1f...",
  "audit_log_sha256": "4ab0...",
  "run_id": "a263...",
  "created": "2026-09-03T10:04:11Z",
  "key_id": "3f2a9c118d4e77b0",
  "sig": "base64..."
}

The SHA-256 is always present, taken over the report body with surrounding whitespace trimmed. Trimming is the single canonicalization rule, so JSON value re-extraction and comment-trailer splitting agree byte for byte.

An Ed25519 signature is added when --signing-key is given. The signed payload is a fixed byte sequence covering the version, report hash, audit log hash, run id, and creation time, so everything that gives the report meaning is bound and a report cannot be re-labeled, re-dated, or attributed to another run without breaking it.

run_id names the invocation the report covers. It is derived from the profiles, targets and times of the run rather than generated, so re-rendering a stored report yields the same id, and every entry of a json report or event of a wazuh stream carries it. v is the payload layout, major.minor: a minor appends a line and leaves the earlier layout readable, so an artifact written before the run id still verifies, carrying 1 as a number rather than a string.

With --audit-log, that file’s hash is included, so forging results also means fabricating a consistent command-by-command trail.

The run’s scope, meaning the filter terms and the count of excluded controls, sits inside the signed body, so a narrowed run cannot be presented as a complete one without breaking verification. An attestation proves a report has not changed. It does not by itself make what was written complete.

Embedding per format

The shapes are chosen so verification re-hashes exactly the attested bytes.

Format Shape
json, gitlab {"report": <verbatim body>, "attestation": {...}}
junit, html Trailing <!-- refpoint-attestation ... --> comment
gha Trailing ::notice title=refpoint-attestation::... line
wazuh Final NDJSON line with "event_type": "attestation"
terminal Not attested

NDJSON has no comment syntax, so wazuh carries the attestation as an event of its own, anchored to the last line rather than searched for — so target output inside a report cannot supply one.

The JSON wrapper is assembled by hand rather than marshalled, because re-marshalling a json.RawMessage re-indents it and silently changes the bytes the hash covers. The JUnit, HTML, and gha shapes stay parseable by existing tools, since a comment or a notice line is inert to them.

Signing and verifying

refpoint keygen --out refpoint-signing
key id:      3f2a9c118d4e77b0
private key: refpoint-signing.key
public key:  refpoint-signing.pub
refpoint run baseline --format json --output result.json \
  --audit-log audit.ndjson \
  --signing-key refpoint-signing.key
refpoint verify result.json --pubkey refpoint-signing.pub --audit-log audit.ndjson
sha256:  OK (9c1f...)
audit log: OK (audit.ndjson matches 4ab0...)
run:     a263...
created: 2026-09-03T10:04:11Z
signature: OK (key 3f2a9c118d4e77b0)

Without --pubkey, a signed report is reported as “signature present but NOT verified” rather than printing OK and implying more than was proven. Passing --audit-log against a report whose attestation covers no audit log is an error, not a pass.

The trust model, stated plainly

The hash alone is integrity, not authenticity. Anyone who edits the report can recompute it. It protects against corruption and casual edits.

The signature proves origin and post-signing integrity. The keyholder can always modify and re-sign, and no in-file cryptography prevents that. What the signature buys is that tampering by anyone else is detectable, and tampering by the keyholder is attributable. Verifiers hold only the public key and cannot forge. The private key therefore belongs in the runner’s secret store, not on workstations.

Keyless cosign signatures over the release checksums.txt cover the binaries themselves. That is a separate mechanism from report attestation.

Re-renderings produced by refpoint report are derivative artifacts and are not attested. The artifact of record is what run wrote.

The audit trail

refpoint run baseline --targets-file fleet.rptargets --audit-log audit.ndjson

One NDJSON line is appended per transport operation on every target.

{"time":"2026-09-03T10:00:01Z","target":"admin@web-01","control_id":"ssh-01",
 "op":"exec","detail":"sshd -T 2>/dev/null || /usr/sbin/sshd -T 2>/dev/null",
 "exit_code":0,"duration_ms":42}
Field Notes
time Operation start
target Target address
control_id The control the operation ran for, carried on the context
op exec, read_file, read_dir, stat, platform
detail The command line or path
exit_code Present for exec
error Present on failure
duration_ms

No stdout or stderr is recorded. Command output routinely contains secrets, the contents of /etc/shadow or a private key among them. What auditors need is what was run, where, for which control, and whether it succeeded.

The log is wrapped at the transport boundary, so every current and future transport is covered by one implementation. It is opened append-only at 0600, with the mode set explicitly as well as at open, because a pre-created file keeps its own permissions. A dry run writes nothing, since it executes no commands and the platform probe is connection setup rather than evidence.

The file grows without bound across appends. Rotate it yourself if you keep long-running scan history, remembering that a report’s attestation binds the hash of the file as it stood at the end of that run.

What a report can leak

Reports hold what the target said, captured with whatever privilege the run used.

A command check stores its full stdout as the check’s observed value. That is deliberate, since a verdict without the output it was based on is not evidence, but nothing removes secrets from it. A command that prints a secret puts that secret in the report.

HTTP header assertions record the verdict rather than the value, so a bearer token or a Set-Cookie is not stored. Body matches store a pass or fail, never the body.

Reports and audit logs are created 0600 and re-chmodded, because an existing file keeps its previous permissions.

Before publishing a report as a CI artifact, consider what the command checks in the profile actually print.