Evidence package and auditor access
Overview
Section titled “Overview”An evidence package is one ZIP file with everything an auditor asks for about one environment, for one framework and one period. It contains the Statement of Applicability, control status and daily history, the AI risk register, findings and their corrective actions, internal audits, management reviews, an AI inventory summary, audit-log counts and the policy settings in force, with a readable report as HTML and as a PDF to file. A manifest lists the SHA-256 of every file and is signed with Ed25519. An auditor can check the package without trusting Shield360.
Auditor access lets an external auditor sign in and read the same evidence for one environment, read-only, until a date you choose.
Prerequisites
Section titled “Prerequisites”- To generate a package, you need the Generate evidence packages permission (
compliance.evidence.export). Owners, administrators, the Compliance officer role and Auditors have it. - To invite an Auditor, you need the Manage members permission (
organisation.members). - To sign packages, the server needs an evidence signing key. See Configure the signing key. Without one, packages are still produced, but they are unsigned.
Generate a package
Section titled “Generate a package”Choose the environment in the header. The package is always for the current environment; a request cannot name another one.
The package covers the framework shown on the page: ISO/IEC 42001, the EU AI Act, NIST AI RMF or OWASP Agentic (ASI). Choose From and To under Evidence package. The period is a range of UTC days, at most 366 days long, ending no later than yesterday and within the control history retention. The default is the last 90 days.
The ZIP downloads. The panel says whether packages are signed, and with which key id, and lists the packages generated before for this environment.
To get only the report as a PDF, select Download PDF report instead. See Download the PDF report alone.
Every package is recorded with its id, environment, framework, period, manifest hash, signing key id, who generated it and when. Its generation is written to the audit log. The contents are never stored.
The API returns the same ZIP:
curl -H "Cookie: <session>" -o evidence.zip \ "https://<your-shield360>/api/governance/evidence-package?framework=iso_42001&from=2026-07-01&to=2026-09-30"A period that is not two real days, is longer than 366 days, ends today or later, or starts before the history kept returns 400, and so does a format other than zip or pdf. A package that would be larger than 64 MB returns 413. If gathering the evidence takes longer than 60 seconds, the request returns 503 with a message instead of timing out. Choose a shorter period and try again.
What is in the package
Section titled “What is in the package”| File | Contents |
|---|---|
README.txt | What the package is, the “not a certification” statement, whether it is signed, and how to verify it. |
report.html | A readable summary of everything below. A single file that opens offline. |
report.pdf | The same report as report.html, with the same sections in the same order, as a PDF to file. See The PDF report. |
manifest.json | Every other file with its SHA-256 and size; the package id, environment, framework, period, when it was generated, the user id of the person who generated it, and whether and how it is signed. |
manifest.sig | The Ed25519 signature over manifest.json, 64 raw bytes. Only in signed packages. |
signature.json | The same signature as base64, with its key id and the manifest’s SHA-256. Only in signed packages. |
public-key.pem | The signing key’s public half, for convenience. Compare it with the published key before you rely on it. |
SHA256SUMS | Every listed file’s SHA-256, for sha256sum -c. |
statement-of-applicability.json | Each control’s applicability, justification, owner, evidence reference and review, with the framework’s coverage (controls with a status, observed or attested only, decided, not applicable, partly met) and, for the NIST AI RMF, its source (NIST AI 100-1, AI RMF 1.0). Empty for frameworks without a Statement of Applicability (ISO/IEC 42001 and the NIST AI RMF have one). |
eu-ai-act-statement.json | EU AI Act packages only: every Article with its status (met, attested, partial, gap, no evidence, not applicable), its checks, its attestation and whether it applies (recorded or derived), the coverage summary, your recorded roles, the serious incidents (category, dates, deadline and status; no free text), the AI literacy coverage, and the source of the Regulation’s text. |
control-status.json, control-status.csv | Each control’s status and checks when the package was generated, measured over the last 7, 30 or 90 days (the shortest that covers the period). |
control-history.json | Each control’s recorded status for every day of the period. A day without a record is not recorded, never met. |
risk-register.json, risk-register.csv | Every AI risk with its score, owner, treatment and evidence summary, as the risk register export has them. |
findings.json, findings.csv | Each finding raised before the end of the period and not closed before it began: its status at the end of the period, what was recorded at each step (containment, root cause, corrective action, effectiveness, acceptance) and who took each step and when. With counts of findings raised, closed, open, overdue and accepted as risks over the period. report.html has the same summary and a table. |
internal-audits.json, internal-audits.csv | Each internal audit planned or reported in the period: scope, criteria, lead auditor and independence, status, results, and the conclusion on each control in scope with the finding raised from it. The CSV has one row per control in scope. report.html has a summary table. |
management-reviews.json, management-reviews.csv | Each management review that met in the period: attendees, status, notes, sign-off, decisions and actions, and the input pack kept at sign-off. The CSV has one row per decision or action. report.html has a summary table. |
ai-requests.json, ai-requests.csv | Each AI request submitted by the end of the period and not already final before it began: what was asked for and why, its status at the end of the period, the decision and what it changed, and its steps up to the end. With counts of requests submitted, approved, rejected and withdrawn in the period, and of those open and overdue at its end. report.html and report.pdf have an AI requests and approvals section with the same summary and a table. |
impact-assessments.json, impact-assessments.csv | Every version of each AI system’s impact assessment live during the period, with its facts (counts, never names), judgements and steps, and today’s coverage of the systems in scope. The CSV has one row per version, with the choices but not the text. report.html has a summary table. |
red-team.json, red-team.csv | Each agent test red-team run finished in the period: its target, suite, strategies and seed, and its results counted by attack category and strategy, with failures by severity; each target’s last red-team run; and, as of the period’s end, how many targets were red-teamed in the last 30 days and how many fail a critical test. The CSV has one row per run, category and strategy. Counts and the names people gave targets and suites only: no prompt, answer or transcript. Only where the environment has agent test targets. |
data-lineage-summary.json | The period’s data lineage through the data gateway, the MCP gateway and the guards: flows and events counted by classification, outcome and origin, how many sources, consumers and destinations, and the sensitive classifications seen without an owner. No source, consumer, destination, person or value is named. Left out when lineage could not be read. |
dataset-register.json | The dataset registry as it stood when the package was built: each dataset’s purpose, status and approval (and whether a second person approved it), which governed items are recorded (owner, provenance, license, quality and bias examination, preparation, retention, location), its data classes, sensitive-data kinds, legal basis, collection period, version, SHA-256 and linked AI systems, dictionary tables and lineage sources, and what it lacks. Free text is left out. Left out when the registry could not be read. |
ai-inventory-summary.json, ai-inventory.csv | Each AI asset with its owner, decision and risk rating, and how many people and machines use it. Not their names, and no billed amounts. |
audit-log-summary.json | Audit-log record counts by action over the period, for the organisation and for the records that name this environment. A package an Auditor generates has the environment’s counts only. No record contents, people or addresses. |
policy-settings.json | The enforcement policy and data retention settings in force when the package was generated. Settings only, never a key. |
The package carries no telemetry, prompts, responses or credentials. People can be named in it in two ways:
- By user id: who generated the package, who took each finding step, who led, started and reported each internal audit, who attended, held and signed off each management review. These are Shield360’s own users, named by id only.
- In text people typed, which may name anyone:
risk-register.json,risk-register.csv, and the risk table inreport.htmlandreport.pdf(title and owner only): each risk’s title, description, owner, treatment plan and acceptance rationale.findings.json,findings.csvand the findings table inreport.htmlandreport.pdf: each finding’s title, description, owner, approver and the text recorded at each step.internal-audits.json,internal-audits.csvand the audit table inreport.htmlandreport.pdf: each audit’s title, scope, criteria, external lead auditor’s name, independence reason, results and conclusion notes.management-reviews.json,management-reviews.csvand the review table inreport.htmlandreport.pdf: each review’s title, external attendees’ names and reviewers’ notes, and each action’s description and owner.ai-requests.json,ai-requests.csvand the AI requests table inreport.htmlandreport.pdf: each request’s purpose, use case, users, justification, question and answer, owner, conditions and rejection reason. The requester, reviewer and each step’s actor are user ids.impact-assessments.json: each assessment’s purpose, context and notes, harm notes included.statement-of-applicability.json,ai-inventory-summary.json,ai-inventory.csv: owners, justifications, reviewers and other recorded fields, as their exports have them. Never the names of the people or machines using an AI asset, only how many.
The cover of report.html and report.pdf also names the organisation, project and environment.
A package an Auditor generates follows the same rules and holds the same files for their one environment; only the audit-log counts are narrowed to that environment. Erasing a person clears their name from these records, so packages generated afterwards no longer carry it; packages already downloaded are outside Shield360.
The PDF report
Section titled “The PDF report”report.pdf is drawn from the same content as report.html, so the two always agree. It has:
- A cover with the organisation, project and environment, the framework and its version, the period, when the package was generated and by which user id, the package id, whether the package is signed and with which key id, and the “not a certification” statement.
- A table of contents with the page each section starts on. Each entry links to its section, and the sections are also bookmarks.
- The sections of
report.html, in its order: controls, the AI risk register, findings, internal audits, management reviews, AI requests and approvals, the AI inventory, audit-log counts and the files in the package. - A footer on every page with the package id and “Page N of M”.
Tables wrap long text and repeat their header row on every page they run onto. A status is always written as a word, such as Met or Gap; the coloured square beside it only repeats it. Tables stop at the same row limits as report.html: 200 risks, 500 findings, 500 internal audits, 200 management reviews, 500 AI requests and 100 audit-log actions. When rows are left out, the report says so and names the CSV or JSON file that has them all. A single cell shows at most 2,000 characters.
The PDF is set in Noto Sans, embedded in the file, which covers Latin-1 and Latin Extended-A. A letter with an accent the font lacks is shown without the accent, and any other character it cannot show, such as Chinese, Hebrew or an emoji, is shown as ?. The JSON and CSV files keep the exact text.
The PDF has no scripts, form fields, attachments or links to the internet, and loads nothing when it opens. Text people typed is drawn as text. report.pdf is listed in manifest.json and SHA256SUMS like every other file, so the signature covers it.
Download the PDF report alone
Section titled “Download the PDF report alone”Download PDF report on the Evidence package panel returns only report.pdf, for the same environment, framework and period. It needs the same permission as a package, and an Auditor’s PDF follows the same rules as their package. To fill in the PDF, Shield360 generates and records a complete package, so the package id in the PDF’s footer is a recorded package. The download is written to the audit log as evidence.report.download, with the PDF’s SHA-256.
The API takes format=pdf:
curl -H "Cookie: <session>" -o evidence-report.pdf \ "https://<your-shield360>/api/governance/evidence-package?framework=iso_42001&from=2026-07-01&to=2026-09-30&format=pdf"The response’s X-Shield360-Evidence-Package header names the package, and X-Shield360-Evidence-Report-Sha256 gives the SHA-256 that package’s manifest lists for report.pdf. A PDF downloaded alone cannot be checked against the signature, because the manifest is not included. To hand an auditor something they can verify, download the package.
Verify a package offline
Section titled “Verify a package offline”Verification needs only OpenSSL 3 (or Node.js) and the public key. It does not need an account or the Shield360 verify page.
Open https://<your-shield360>/api/evidence/keys. It lists every evidence signing key with its key id: the current key and any earlier ones kept after a rotation. Save the key whose kid matches signature.keyId in manifest.json as key.pem:
KID=$(jq -r .signature.keyId manifest.json)curl -s "https://<your-shield360>/api/evidence/keys" \ | jq -r --arg kid "$KID" '.data.keys[] | select(.kid == $kid) | .publicKey' > key.pemDo not rely only on public-key.pem inside the package: anyone who could change the package could change that file too. Compare it with the published key, or get the key from your contact at the organisation.
unzip evidence.zip -d evidence && cd evidenceopenssl pkeyutl -verify -pubin -inkey key.pem -rawin \ -in manifest.json -sigfile manifest.sigOpenSSL prints Signature Verified Successfully. Any change to manifest.json, even one space, makes the check fail.
sha256sum -c SHA256SUMSsha256sum SHA256SUMSEach file prints OK. The hash of SHA256SUMS itself must match its entry in manifest.json. On macOS, use shasum -a 256 in place of sha256sum.
This Node.js script does all of it in one step:
// node verify-evidence.mjs <unzipped package folder> <key.pem>import { createHash, createPublicKey, verify } from "node:crypto";import { readFileSync, readdirSync } from "node:fs";import { join } from "node:path";
const [dir, keyFile] = process.argv.slice(2);const manifestBytes = readFileSync(join(dir, "manifest.json"));const manifest = JSON.parse(manifestBytes.toString("utf8"));let ok = true;
if (manifest.signature.signed) { const signature = readFileSync(join(dir, "manifest.sig")); const valid = verify(null, manifestBytes, createPublicKey(readFileSync(keyFile)), signature); console.log(`signature (${manifest.signature.keyId}): ${valid ? "valid" : "INVALID"}`); ok &&= valid;} else { console.log("signature: the package is unsigned");}
const listed = new Set(manifest.files.map((f) => f.path));for (const file of manifest.files) { const data = readFileSync(join(dir, file.path)); const match = createHash("sha256").update(data).digest("hex") === file.sha256 && data.length === file.bytes; if (!match) console.log(`CHANGED: ${file.path}`); ok &&= match;}const own = ["manifest.json", "manifest.sig", "signature.json", "public-key.pem"];for (const name of readdirSync(dir)) { if (!listed.has(name) && !own.includes(name)) { console.log(`NOT LISTED: ${name}`); ok = false; }}console.log(ok ? "The package checks out." : "The package does NOT check out.");process.exit(ok ? 0 : 1);Verify on the web
Section titled “Verify on the web”https://<your-shield360>/evidence/verify checks a package without signing in. Upload manifest.json and manifest.sig (or signature.json). The page reports:
- whether the signature is valid under the key the manifest names;
- whether this server generated a package with that id and exactly that manifest (known), has no record of it (unknown), or generated one with that id but a different manifest (changed).
File contents are not uploaded. Check them with sha256sum -c as above. Nothing you upload is stored.
The page calls a public API:
curl -X POST -H "Content-Type: application/json" \ -d "{\"manifest\": $(jq -Rs . < manifest.json), \"signature\": \"$(base64 < manifest.sig | tr -d '\n')\"}" \ "https://<your-shield360>/api/evidence/verify"Malformed JSON, a body without a manifest, or a manifest that is not an evidence package manifest returns 400. A body over 2 MB returns 413, however it is sent.
Unsigned packages
Section titled “Unsigned packages”When no signing key is configured, packages are still produced with the SHA-256 of every file. manifest.json has "signed": false. The README, the report and the panel on Govern → Compliance program → Framework controls all say the package is unsigned. An unsigned package can be checked only against this server’s record of its manifest hash, so configure a signing key before you hand packages to an auditor.
Configure the signing key
Section titled “Configure the signing key”The evidence signing key is its own Ed25519 key. It is not the licence signing key, which never belongs on a server.
-
Generate the key pair on a trusted machine:
Terminal window openssl genpkey -algorithm ed25519 -out evidence-signing-key.pemopenssl pkey -in evidence-signing-key.pem -pubout -out evidence-public-key.pem -
Store the private key in your secret store, for example a Kubernetes secret, and set these environment variables on the Shield360 server:
Variable Value SHIELD360_EVIDENCE_SIGNING_KEYThe private key: the PKCS#8 PEM, or that PEM base64-encoded onto one line ( base64 < evidence-signing-key.pem | tr -d '\n').SHIELD360_EVIDENCE_SIGNING_KEY_IDA key id of your choosing, such as 2026-10. Letters, digits,.,_and-.SHIELD360_EVIDENCE_PUBLIC_KEYSOptional. Earlier keys’ public halves, as JSON: [{"kid":"2026-09","publicKey":"-----BEGIN PUBLIC KEY-----\n…"}]. -
Restart the server. The panel on Govern → Compliance program → Framework controls shows the key id, and
/api/evidence/keyspublishes the public key.
A key that cannot be read, is not Ed25519, or has no valid key id is treated as no key. The server logs the variable’s name, never its value.
Rotate the key
Section titled “Rotate the key”- Generate a new key pair.
- Add the old key’s public half to
SHIELD360_EVIDENCE_PUBLIC_KEYSwith its old key id. - Set
SHIELD360_EVIDENCE_SIGNING_KEYandSHIELD360_EVIDENCE_SIGNING_KEY_IDto the new key and a new key id.
New packages are signed with the new key. Packages signed with the old key still verify, because its public key is still published under its key id.
Auditor access
Section titled “Auditor access”An Auditor is an external person who can read one environment’s compliance evidence, and nothing else, until a date you choose.
What an Auditor can do
Section titled “What an Auditor can do”| Can read | Cannot see or do |
|---|---|
| Govern → Compliance program → Framework controls, including the Statement of Applicability and daily history | Costs, budgets and pricing |
| The AI risk register | Telemetry, requests, prompts and responses |
| Findings and corrective actions, with each finding’s timeline | Raise, change or close a finding, or see suggested findings |
| AI impact assessments, with their facts and history, and their PDF or HTML | Start, change, submit or approve an assessment |
| Internal audits and management reviews, with each review’s input pack | Plan, conclude or report an audit; create, hold or sign off a review; change their settings |
| AI requests, with each request’s timeline and risk view, by user id | Submit, change, review or decide an AI request |
| The AI inventory list, with counts of people and machines | An asset’s page or the inventory export, which name people and machines and carry spend; usage figures |
| The audit log, narrowed to records about their environment | API keys, the vault and install keys; billing and the subscription |
| Evidence packages: generate and download | Members, roles and organisation settings |
| Change anything, including the Statement of Applicability |
Invite an Auditor
Section titled “Invite an Auditor”The Auditors section is below the members list.
The end date is required and can be at most 180 days away. Access ends at the end of that day (UTC).
Someone who already has an account is added at once. Anyone else is invited and becomes an Auditor when they sign up or accept the invitation. An invitation accepted after its end date gives no access.
The invitation is written to the audit log with the environment and end date. The auditor’s email address is not. An organisation can have up to 10 Auditors with access at a time, invitations included; the limit holds even for invitations sent at the same moment. An existing member cannot also be an Auditor, and an identity-provider group mapping never gives the Auditor role.
An Auditor who signs in lands on Govern → Compliance program → Framework controls.
The API takes the same fields:
curl -X POST -H "Cookie: <session>" -H "Content-Type: application/json" \ -d '{"email":"auditor@firm.example","databaseConfigId":"<environment id>","expiresOn":"2027-01-31"}' \ "https://<your-shield360>/api/organisation/auditors"Malformed JSON, a missing or invalid email, or an end date that is in the past or more than 180 days away returns 400. An environment of another organisation returns 404. An existing member, an existing invitation, or more than 10 active Auditors returns 409.
How access is enforced
Section titled “How access is enforced”The server enforces access on every request, not only in the interface:
- An Auditor holds exactly the Auditor role’s read-only permissions, whatever role or project role their membership carries. Their role cannot be changed, and the Auditor role cannot be given to a member.
- Routes that check no permission treat an Auditor as signed out unless the route is one of the evidence routes above.
- Every request reads the Auditor’s own environment, whatever environment the request names.
- When the end date passes, the Auditor’s permissions are removed on their next request. They can still sign out and switch to another organisation they belong to. The members list shows them as expired. Their first request after the end date writes the expiry to the audit log. Each day an Auditor reads evidence, their first access that day is written to the audit log.
To end access early, remove the Auditor from the members list. To extend it, remove them and invite them again with a new end date.
Auditors do not use a seat. Access is read-only, limited to one environment, at most 180 days long, and at most 10 Auditors at a time. Someone who is both an Auditor in one organisation and a member of another counts once, as a member.
Next steps
Section titled “Next steps”The control status, Statement of Applicability and daily history in the package.
FindingsNonconformities, their corrective actions and effectiveness reviews.
Internal auditThe audit programme and each audit’s conclusions.
Management reviewReviews, their input pack, decisions and sign-off.
Risk registerThe AI risks the package exports.
AI inventoryThe register behind the inventory summary.