Evidence bundles and audience reports
Pro. For “what do I give my auditor?”Penthera can write the results of one scan as an evidence bundle: every report, a manifest with the SHA-256 of each file, and an optional SSH signature over the manifest. An auditor checks the bundle with one command. No hosted service is involved. The auditor needs only Node (or
shasum and
jq) and OpenSSH 8.0 or later.
Start here: one run for NIS2 and the CRA
NIS2 puts a duty of care on in-scope organizations, written into national law (the Cyberbeveiligingswet in the Netherlands, the NIS2-wet in Belgium). The Cyber Resilience Act adds reporting duties for manufacturers, with deadlines measured in hours. A small company has to show that it tests, fixes and records, without a compliance team.NEEDS HUMAN INPUT instead of guessing. The details are in the
NIS2 and CRA and AI-BOM guides.
What it does not do:
- It is readiness tooling, not a certification and not legal advice. Whether you are in scope, and what is proportionate for you, is for you and your advisors to decide.
- It covers the technical, testable part of the duty of care. Policies, training, backups and MFA come from your self-assessment. Penthera records what you attest. It cannot check it.
- It does not file anything. Reports to a CSIRT or ENISA are drafts for you to review and submit.
- Jurisdiction details ship for the Netherlands only.
For auditors: verify a bundle
You need the bundle directory and an allowed signers file that names the key you expect the bundle to be signed with. Get that public key from the organization through a channel other than the bundle itself (their security page, an email you can authenticate, or in person).0 means every file matches the manifest and the manifest was signed
by a key in allowed_signers. Exit code 1 means something changed, is
missing, was added, or the signature does not check out. The output says which.
An allowed_signers file is one line per key:
Without Penthera
The bundle does not depend on Penthera to be checked. With standard tools:penthera evidence verify does the same three checks, and also rejects
manifest paths that would leave the directory (../, absolute paths) and
symlinked files.
What the checks prove, and what they do not
- Hashes prove that the files have not changed since the manifest was written. Without a signature they prove nothing about who wrote it: anyone can rewrite a file and its hash together.
- The signature proves that the holder of the signing key produced this
manifest. It does not prove that the scan was run against the system you
care about, or with the options you expect. Read the manifest’s
command,target,scanandgitfields, and the auditor report’s scope section. - A signed bundle verifies only with
--allowed-signers. Without it,verifycannot tell whose key made the signature (anyone can re-sign a rewritten manifest with their own key), so it fails with statusunverified. A manifest that declares a signature with no.sigfile fails too. - An unsigned bundle passes with an
UNSIGNEDwarning. Pass--require-signature(with--allowed-signers) to make it fail. - A scan is a snapshot. The bundle is evidence of what Penthera saw at
generated_at, not a statement that the system is secure.
For the team producing evidence
--evidence <dir>writes the bundle. The directory must be new or empty, so a bundle never mixes two runs.--sign-key <key>signsmanifest.jsonwithssh-keygen -Y sign -n penthera-evidence. Use the private key path, or the.pubpath when the key lives inssh-agentor on a hardware token. A passphrase prompt appears in the terminal.--sbom <file>and--readinessadd their outputs to the bundle.- Give auditors the public key out of band, as an
allowed_signersline.
What is in a bundle
The command line in the manifest has the values of
--auth-cookie,
--auth-bearer and any token, secret or password flag replaced with
[REDACTED], and credentials in URLs masked.
Audience reports
--audience developer|owner|auditor picks who the Markdown report is for. It
applies to --markdown <file>. Without --markdown, the report goes to stdout.
The default report (no --audience) is unchanged.
--memory, the owner report shows the trend across recent
scans. When the result has an agent loop run (loopRun) or memory has fix
events, the developer, owner and auditor reports show a fix timeline
(found, fixed, verified) with the time to fix.
Your own template
--report-template file.md renders your own Markdown from the same data the
audience reports use. The template language is small on purpose:
{{path.to.value}}prints a value. Values are HTML- and Markdown-escaped, and newlines become spaces, so a finding title cannot add HTML, a heading, a list item or a table cell.{{#each list}}…{{/each}}loops; inside,{{this}}is the item and{{@index}}its 0-based index. Names not found on the item are looked up in the enclosing scope.{{#if value}}…{{else}}…{{/if}}branches. An empty list counts as false.
__proto__, constructor or prototype are
rejected, and only own properties are read. A template that uses anything else
fails with a line number before the scan starts.
Top-level fields: tool, target, timestamp, duration, modes,
profile, offline, probes, summary (critical, high, medium,
low, info, serious, total, suppressed, inconclusive), findings
(fid, title, severity, confidence, source, method, collectedAt,
category, location, file, line, url, description, recipe,
fixText, sources, owasp, wstg), suppressed (title, severity,
reason, actor, by, expires), inconclusive, egress, actions,
topActions, ownerActions, nis2, owasp, trend and timeline. The
model is built in src/report/model.js.