The OSCAL export¶
For auditors, and for whoever has to answer them.
qed export --dir .qed --format oscal --out assessment.json
# or
curl -s 'localhost:8710/v1/evidence?format=oscal&from=0&to=999' > assessment.json
The output is an OSCAL 1.1.2 assessment-results document (SPEC §10.8). It
validates against the published NIST schema in CI, which SPEC §10.8 marks
MUST pass.
What maps to what¶
| qed | OSCAL |
|---|---|
| one export | one result |
| one evidence record | one observation |
| the assurance class (ADR-007.1) | the observation type, e.g. assurance-kernel |
| one rule hit × one control reference | one finding |
| the control the rule claims to serve | finding.target.target-id |
| public keys, checkpoint roots | back-matter.resources |
Each finding links back to the observation it came from through
related-observations, so an auditor can walk from a control to the signed
record that justifies the claim.
Read this part before you quote the document¶
The mapping is only as deep as the rules. The export carries the control
references the rules declare in their own compliance: blocks, and nothing
else. Every finding target carries mapping-scope: rule-declared and the
document metadata carries
mapping-scope: baseline-rule-control-references-only, so this is visible in
the file rather than only on this page. What a rule declares it serves is a
claim about that rule, not a statement that the control is covered.
Each control now arrives with its label, its source and its depth. A
finding's target title reads eu_ai_act Art. 14 -- Human oversight, and its
props carry control-label (the full text, which the title truncates at 72
characters so a report table stays readable), catalogue-source (the URL for
the standard itself) and mapping-depth. Those come from
qed/compliance/packs/, documented in compliance.md. A label
is that catalogue's description and not a quotation -- catalogue-source is
where the standard's own words are, which is why it travels with every finding.
mapping-depth is worth reading before you weigh a finding. article
means the rule maps to a specific article or clause; category to a named
category; pack only to the framework. All three produce findings that look
alike in a table and are not equally strong evidence.
The document says nothing about coverage. An export covers the records in
the range you asked for, so a control that no action in that window touched is
simply absent, and absence in an assessment result is not a claim of
compliance. qed packs list is the coverage report -- including the controls
that nothing maps to -- and it refuses to print a percentage for any framework
whose catalogue is only partial.
A finding is about the action, not about qed. A rule that matched means the
action the agent proposed did not satisfy the control objective, so the status is
not-satisfied with reason fail. Whether QED then stopped it is a separate
fact, in the enforced and rule-mode properties. In shadow mode the objective
was equally unsatisfied and nothing was prevented — do not read a shadow finding
as a control that worked.
A rule hit with no control reference is not a finding. finding.target is
required by the schema, and a hit with no compliance: block has no control to
target. Those hits are not dropped quietly: they are named on the observation and
counted in the result properties as rule-hits-without-control, with a sentence
in result.remarks saying so. Adding a control reference is a policy change, not
an export change.
Identifiers are derived, not random¶
Every UUID is a version 5 UUID derived from the thing it names: an observation from the evidence record's hash, a finding from that hash plus the rule and the control. Exporting the same range twice gives byte-identical output, and two auditors working from two copies of the same chain get the same document identifiers.
OSCAL accepts v4 or v5. v4 would have made every export unreproducible, which is a poor property for a document whose job is to be checked.
The only value not derived from the chain is metadata.last-modified, which the
caller passes in.
Control ids are tokenised¶
OSCAL's target-id is an NCName: no spaces, no colons, and it may not start
with a digit. Real control ids break all three rules — Art. 14, A.5.34,
6.2.4. So they are tokenised:
| framework | control | target-id |
|---|---|---|
eu_ai_act |
Art. 14 |
eu_ai_act.art-14 |
soc2 |
CC8.1 |
soc2.cc8.1 |
iso_27001 |
A.5.34 |
iso_27001.a.5.34 |
nist_ai_rmf |
MANAGE-2.2 |
nist_ai_rmf.manage-2.2 |
The verbatim framework and control stay on the target as properties, which is
what a human reads. Single separators survive so a control stays recognisable —
soc2.cc8.1, not soc2-cc8-1.
Verifying the chain the document describes¶
back-matter embeds each signing public key as a base64 resource, which makes
the document convenient to read — and is not what you check it against:
qed export --dir .qed --out chain.jsonl
qed-verify chain.jsonl --keys .qed/public --instance-id <id> --require-complete
The embedded keys and the instance id in the genesis payload were both written by the instance under audit. Checking the document against the key the document carries, and against the id the document states, is checking it against itself: a chain fabricated from scratch, signed with a key made for the purpose, and labelled with any instance id you like passes that test completely. The embedded resources are there so a reader can follow the linkage by eye and so the document stands alone as a record; they are not an evidentiary shortcut.
For the check to mean anything, the instance id and the public key have to reach
the verifier from somewhere the operator does not control the moment the
evidence is produced: the engagement letter, the contract, a key the operator
published before the period under review, or a key fingerprint confirmed out of
band. --require-complete makes qed-verify exit non-zero rather than report a
pass with the binding skipped.
The observations carry record-hash, previous-hash, payload-hash and
signature, so the linkage is readable from the OSCAL document alone, though
qed-verify on the JSONL export is the supported path.
Checkpoints in the range appear as their own back-matter resources with the Merkle root and the number of publication receipts attached to the previous checkpoint (SPEC §10.4).
import-ap¶
OSCAL requires import-ap, a reference to an assessment plan. qed monitors
continuously rather than running a planned assessment, so there is no plan
document. The reference resolves to a back-matter resource that says exactly
that, rather than pointing at a file that does not exist.
A filtered export is refused¶
format=oscal with a type filter is a 400. An assessment-results document
states what was observed over a period; assembled from a subset it would omit
observations without saying so, and this is the document that goes to an
auditor. Export a sequence range instead.
Purged payloads (SPEC §10.6) are fine: the observation says the payload was removed under the retention policy and that the hashes and signature are intact.
How it is checked¶
tests/evidence/test_oscal_schema.py validates the export against
tests/oscal/oscal_assessment-results_schema.json, the published NIST v1.1.2
schema, vendored byte for byte and checked by sha256 before use — an export that
validated against a schema somebody adjusted would prove nothing.
Two details worth knowing if you work on that file:
- The suite swaps
jsonschema's regex engine for theregexmodule. The NIST schema uses ES2018 Unicode property escapes (\p{L}), including in theTokenDatatypethat constrains everytarget-id; CPython'sreraisesbad escape \pon those, so a stock validator cannot read the real schema at all. The swap makes the check stricter, not weaker. - There are negative controls. A validator that accepts everything is worse than
no validator, and this whole claim rests on one, so the suite also feeds it a
version-1 UUID, a
target-idwith spaces, a missing required field and an empty array, and fails if any of them is accepted.