From d5d4f3e88c842035a57bf666a7a93384dd009346 Mon Sep 17 00:00:00 2001 From: Antonios Voulvoulis Date: Sun, 20 Sep 2026 20:27:36 +0300 Subject: [PATCH 1/2] A sample nobody needs the machine to reproduce IQ-011. The previous sample report was withdrawn, not corrected: it described a retired storage schema, its source VM no longer existed, and the fields that replaced the retired ones had never been collected from it. Writing plausible values for a host nobody can re-observe would have been inventing evidence. This replaces it with real output that does not depend on anyone still having the host. A disposable Debian 12 VM was built for the purpose. The released 0.1.0-alpha1 DEB was installed on it - so the sample also demonstrates that the published artifact installs and runs - and an unprivileged user collected under ISEDRAF_STATE_ROOT. Nothing was typed by hand into either document. What is committed is the evidence, not just the output: evidence/collected_inventory.json the inventory as collected evidence/state-root/ the snapshot, manifest and ledger as written evidence/environment.txt version, interpreter, kernel, distribution scripts/docs/sample_report.py rebuilds both documents from those bytes by running the real report model and the real renderers. It re-verifies the ledger chain while doing so, so the sample demonstrates verification rather than asserting it. The published sample is byte-identical to what the VM produced, except for the evidence root, which now honestly names the committed directory the bytes were read from. Two identities had to be pinned or the digest would not have been a function of the evidence: the report id and generation timestamp, which NON_REPRODUCIBLE already names as values that must differ between renderings, and the inventory artifact's collection id and timestamp, which are stamped when a report is built rather than when the inventory is collected. All four are the real values from the collection that produced the committed evidence, taken from that run's own report. The artifact digest of the published sample equals the one the VM computed, which is what proves the bytes are the same collection. D-114 is visible in the sample, and the case is a good one: the virtio disk reports queue_rotational=true while its backing store is a file on an SSD array. The retired schema would have labelled it ROTATIONAL. The sample records the kernel flag, names the source in the column header, and claims no medium. What committed bytes cannot reproduce is the collection itself - reading /sys and /proc needs the machine - so the evidence is captured once and the report is reproducible from it. The sample and its evidence cannot drift apart in either direction: two injections prove the gate fires when the document is edited by hand and when the evidence changes underneath it. Implements: IQ-011, D-88, D-89, GOV-002 Assisted-by: Claude (VM provisioning, capture, generator, gate authoring) --- Makefile | 8 +- docs/CURRENT_STATE.md | 4 +- docs/IMPLEMENTATION_QUESTIONS.md | 2 +- docs/reference/CONTROL_EVIDENCE_MAP.md | 23 +- docs/reference/samples/SAMPLE_REPORT.json | 430 ++++++++++++++++++ docs/reference/samples/SAMPLE_REPORT.md | 179 ++++++++ .../samples/evidence/collected_inventory.json | 2 + .../samples/evidence/environment.txt | 7 + .../state-root/ledger/segment-000001.jsonl | 1 + .../manifest.json | 1 + .../method/host_identity.json | 1 + .../state/host_identity.json | 1 + scripts/ci/falsifiable.sh | 20 + scripts/ci/gate_coverage.json | 9 + scripts/docs/sample_report.py | 143 ++++++ 15 files changed, 819 insertions(+), 12 deletions(-) create mode 100644 docs/reference/samples/SAMPLE_REPORT.json create mode 100644 docs/reference/samples/SAMPLE_REPORT.md create mode 100644 docs/reference/samples/evidence/collected_inventory.json create mode 100644 docs/reference/samples/evidence/environment.txt create mode 100644 docs/reference/samples/evidence/state-root/ledger/segment-000001.jsonl create mode 100644 docs/reference/samples/evidence/state-root/snapshots/SDS-20260920T171541Z-4810e53a3805777d/manifest.json create mode 100644 docs/reference/samples/evidence/state-root/snapshots/SDS-20260920T171541Z-4810e53a3805777d/method/host_identity.json create mode 100644 docs/reference/samples/evidence/state-root/snapshots/SDS-20260920T171541Z-4810e53a3805777d/state/host_identity.json create mode 100644 scripts/docs/sample_report.py diff --git a/Makefile b/Makefile index 2896938..4b877e7 100644 --- a/Makefile +++ b/Makefile @@ -6,9 +6,9 @@ # CI invokes these same targets rather than re-implementing them in YAML, which is # what prevents a gate silently degrading into a warning. There is no warning tier. -.PHONY: check check-provider-alignment check-native-catalog check-licensing check-public-claims check-deb-ordering check-reproducible check-sbom check-tests check-python-floor check-packaging check-storage-vocabulary check-native-catalog check-licensing check-public-claims check-privacy check-docs-truth check-current-state check-headers check-docs check-scope check-shell check-refs check-paths check-index check-freeze check-vectors check-vectors-negative check-vectors-crossversion check-gate-coverage check-falsifiable help +.PHONY: check check-provider-alignment check-native-catalog check-licensing check-public-claims check-deb-ordering check-reproducible check-sbom check-tests check-python-floor check-packaging check-storage-vocabulary check-native-catalog check-licensing check-public-claims check-privacy check-docs-truth check-current-state check-sample-report check-headers check-docs check-scope check-shell check-refs check-paths check-index check-freeze check-vectors check-vectors-negative check-vectors-crossversion check-gate-coverage check-falsifiable help -check: check-scope check-headers check-python-floor check-packaging check-storage-vocabulary check-native-catalog check-licensing check-public-claims check-privacy check-docs-truth check-current-state check-shell check-refs check-paths check-index check-freeze check-vectors check-vectors-negative check-vectors-crossversion check-tests check-docs +check: check-scope check-headers check-python-floor check-packaging check-storage-vocabulary check-native-catalog check-licensing check-public-claims check-privacy check-docs-truth check-current-state check-sample-report check-shell check-refs check-paths check-index check-freeze check-vectors check-vectors-negative check-vectors-crossversion check-tests check-docs @echo "make check: all gates passed" ## check-scope D-96: no product implementation before architecture freeze @@ -85,6 +85,10 @@ check-python-floor: check-packaging: @python3 scripts/ci/check_packaging.py +## check-sample-report IQ-011: the published sample regenerates from committed evidence +check-sample-report: + @python3 scripts/docs/sample_report.py check + ## check-storage-vocabulary D-114: a retired inference does not return as vocabulary check-storage-vocabulary: @python3 scripts/ci/check_storage_vocabulary.py --self-test diff --git a/docs/CURRENT_STATE.md b/docs/CURRENT_STATE.md index 40a1f84..10be720 100644 --- a/docs/CURRENT_STATE.md +++ b/docs/CURRENT_STATE.md @@ -111,8 +111,8 @@ Not asserted. Each number is counted at generation time. | | | |---|---| -| Gates | 19 | -| Falsification injections | 111 | +| Gates | 20 | +| Falsification injections | 113 | | Golden vector cases | 15 | | Frozen artifacts | 7 | | Test files | 3 | diff --git a/docs/IMPLEMENTATION_QUESTIONS.md b/docs/IMPLEMENTATION_QUESTIONS.md index 8275a7d..7994088 100644 --- a/docs/IMPLEMENTATION_QUESTIONS.md +++ b/docs/IMPLEMENTATION_QUESTIONS.md @@ -22,4 +22,4 @@ Assumptions only — **never authority**. The owner resolves; resolutions become | IQ-008 | CONFLICT | STORE-001 | `/run/isedraf/` | Implemented the lock at `/var/lib/isedraf/.lock` | The sketch placed the lock at `/run/isedraf/isedraf.lock`. `STORE-001` puts `.lock` inside the state root, which also makes the lock and the store it protects share a filesystem — a lock on a different filesystem cannot guarantee exclusion if the store is mounted elsewhere. `/run/isedraf/` remains available for future ephemeral state. | `STORE-001` | OPEN | | IQ-009 | GAP | SCOPE-077, SNAP-015 | `lib/isedraf/cli.py`, `lib/isedraf/verify.py` | Verification runs in-process at the end of `identity`; a failure exits `64` (engine error) | `SNAP-015` assigns `exit 5` to `integrity_failure`, and `SCOPE-077` states `5` is not reachable in W1-A. A standalone `isedraf verify` therefore has **no frozen exit code for "verification failed"**. No exit code was invented: verification stays in-process, and the standalone command is deferred until the W1-A exit set is extended or `verify` is scoped to a later set. | discovered implementing W1-B | OPEN | | IQ-010 | CONFLICT | SCOPE-070, SCOPE-071, SCOPE-072, STORE-001, PRIV-005 | `lib/isedraf/stateroot.py` | `resolve()` refuses to fall back to a production root this slice cannot reach, and names the requirements | **Not a contradiction between frozen rules — an implementation defect.** `SCOPE-070` already states W1 *"runs under `ISEDRAF_STATE_ROOT`"*, and `PRIV-005`'s Mode A — `sudo isedraf`, root supervisor inside a sandbox, state-root writability preflight — is the only execution topology that ever owns `/var/lib/isedraf`. Mode A is `DEFERRED_TO_FREEZE_SET_2` by `SCOPE-072`, so **W1 has no production mode by design**. The implementation was offering one and failing with a bare permission error. Answers for Freeze Set 2, when Mode A lands: the **root supervisor** creates and owns the root at mode `0700`; packaging creates it at install time; no group-readable relaxation, since `STORE-001` freezes `0700`; `sudo -u` is not a path, because `SCOPE-071` refuses anything reached through sudo and `PRIV-004` refuses when `SUDO_USER` is set | `env -u ISEDRAF_STATE_ROOT isedraf identity` now explains rather than failing on `EACCES` | **CLOSED_IMPLEMENTATION_FIX** | -| IQ-011 | GAP | D-114, STORAGE-SEMANTICS-001 | `docs/reference/samples/` (withdrawn), `docs/reference/CONTROL_EVIDENCE_MAP.md` | The rendered sample report was **withdrawn** rather than edited. It was real output from disposable lab VM `isd-a9` (AlmaLinux 9.7, KVM) and reported a storage device `type` of `ROTATIONAL` / `OPTICAL` — the exact inference D-114 retired, published as an example of what the tool claims. | It could not be regenerated: `isd-a9` no longer exists, the surviving lab corpus belongs to another project and is read-only, and `kernel_subsystem`, `queue_rotational`, `kernel_removable` and `scsi_peripheral_type` were never collected from that host, so re-rendering the old collection cannot produce them. Writing plausible values would be inventing evidence for a machine nobody can re-observe, which is the one failure this project exists to prevent. **Proposal:** regenerate after the RC on a disposable VM created for the purpose, and add a documentation generator so the sample is reproducible from committed bytes rather than from a VM that only one person ever had. A `check_storage_vocabulary` gate now rejects the retired vocabulary if it returns. | `git log 6ca913e`; storage semantics proofs, this milestone | OPEN | +| IQ-011 | GAP | D-114, STORAGE-SEMANTICS-001 | `docs/reference/samples/` (withdrawn), `docs/reference/CONTROL_EVIDENCE_MAP.md` | The rendered sample report was **withdrawn** rather than edited. It was real output from disposable lab VM `isd-a9` (AlmaLinux 9.7, KVM) and reported a storage device `type` of `ROTATIONAL` / `OPTICAL` — the exact inference D-114 retired, published as an example of what the tool claims. | It could not be regenerated: `isd-a9` no longer exists, the surviving lab corpus belongs to another project and is read-only, and `kernel_subsystem`, `queue_rotational`, `kernel_removable` and `scsi_peripheral_type` were never collected from that host, so re-rendering the old collection cannot produce them. Writing plausible values would be inventing evidence for a machine nobody can re-observe, which is the one failure this project exists to prevent. **Proposal:** regenerate after the RC on a disposable VM created for the purpose, and add a documentation generator so the sample is reproducible from committed bytes rather than from a VM that only one person ever had. A `check_storage_vocabulary` gate now rejects the retired vocabulary if it returns. | `git log 6ca913e`; storage semantics proofs, this milestone | **CLOSED_IMPLEMENTED** — a disposable Debian 12 VM was built for the purpose, the released 0.1.0-alpha1 package was installed on it, and an unprivileged collection produced the sample. The evidence is committed under `docs/reference/samples/evidence/` and the sample is generated from it by `scripts/docs/sample_report.py`, gated by `make check-sample-report` and two falsification injections. No value was supplied by hand. | diff --git a/docs/reference/CONTROL_EVIDENCE_MAP.md b/docs/reference/CONTROL_EVIDENCE_MAP.md index 6710b6b..8638928 100644 --- a/docs/reference/CONTROL_EVIDENCE_MAP.md +++ b/docs/reference/CONTROL_EVIDENCE_MAP.md @@ -202,13 +202,22 @@ Report | Collection completeness | both | per-subdomain status | | Limitations | both | what the evidence does not support | -A rendered sample of the real output is **not published with this preview**. The previous sample was -generated on a disposable lab VM that no longer exists, and it describes the retired storage schema: -it reports a device `type` of `ROTATIONAL` or `OPTICAL`, which is exactly the inference the storage -observation model now refuses to make. It could not be migrated, because the fields that replaced -that one - `kernel_subsystem`, `queue_rotational`, `kernel_removable`, `scsi_peripheral_type` - were -never collected from that host, and writing plausible values for them would be inventing evidence. -A sample returns when it can be produced by running the tool, not by editing a document. +A rendered sample of the real output is in [`samples/SAMPLE_REPORT.md`](samples/SAMPLE_REPORT.md), +with the same report as JSON beside it. It is real `isedraf report` output, collected by an +unprivileged user on a disposable Debian 12 VM using the released `0.1.0-alpha1` package. + +It is **generated from committed evidence**, not pasted in. The collection it came from is in +[`samples/evidence/`](samples/evidence/) — the inventory as collected, and the snapshot, manifest +and ledger as written — and `python3 scripts/docs/sample_report.py generate` rebuilds both +documents from those bytes. Regenerating re-verifies the ledger chain as it goes, so the sample +demonstrates the verification rather than asserting it. `make check` fails if the published +sample and its evidence disagree. + +The previous sample was withdrawn because it described a retired storage schema and its source +machine no longer existed, so it could not be regenerated. That cannot happen again: the sample +no longer depends on anyone still having the host. What committed bytes cannot reproduce is the +collection itself — reading `/sys` and `/proc` needs the machine — so the evidence is captured +once and the report is reproducible from it. ## Domains not yet mapped diff --git a/docs/reference/samples/SAMPLE_REPORT.json b/docs/reference/samples/SAMPLE_REPORT.json new file mode 100644 index 0000000..a3b1c5b --- /dev/null +++ b/docs/reference/samples/SAMPLE_REPORT.json @@ -0,0 +1,430 @@ +{ + "assessment": null, + "collection_summary": [ + { + "collection_status": "COLLECTED", + "method": "/proc/sys/kernel/hostname + /etc/hosts", + "reason": null, + "subdomain": "host" + }, + { + "collection_status": "COLLECTED", + "method": "/etc/os-release + uname(2) + /proc/1/comm", + "reason": null, + "subdomain": "platform" + }, + { + "collection_status": "COLLECTED", + "method": "systemd-detect-virt | /sys/hypervisor + /sys/class/dmi/id", + "reason": null, + "subdomain": "machine" + }, + { + "collection_status": "COLLECTED", + "method": "/proc/cpuinfo", + "reason": null, + "subdomain": "compute" + }, + { + "collection_status": "COLLECTED", + "method": "/proc/meminfo", + "reason": null, + "subdomain": "memory" + }, + { + "collection_status": "COLLECTED", + "method": "/sys/block + /proc/self/mounts + statvfs(2)", + "reason": null, + "subdomain": "storage" + }, + { + "collection_status": "COLLECTED", + "method": "ip -json addr/route", + "reason": null, + "subdomain": "network" + }, + { + "collection_status": "COLLECTED", + "method": "/etc/resolv.conf", + "reason": null, + "subdomain": "dns" + }, + { + "collection_status": "COLLECTED", + "method": "timedatectl show", + "reason": null, + "subdomain": "time" + } + ], + "identity_evidence": { + "available": true, + "collection_status": "COLLECTED", + "created_at": "2026-09-20T17:15:41Z", + "evidence_root": "docs/reference/samples/evidence/state-root", + "host_id": "sha256:4d3e3c2abb1f71c13a45ba590cda91e3ded6c7ccdc3f28dc63b5ac20f74dcfd1", + "ledger_record_hash": "sha256:1dab505f28bdb65d63fe62e1ecaeecb4f099e6ef4b4109bd6784b2a319f12698", + "ledger_sequence": 1, + "manifest_hash": "sha256:32f41a2a89693063b85580791a257363833f0438f32732a645ec63fbcdd09c8f", + "maturity": "W1-A CERTIFIED — snapshot-bound, manifest-hashed, ledger-chained", + "reason": null, + "run_id": "RUN-20260920T171541Z-706d3c21624288c6", + "snapshot_id": "SDS-20260920T171541Z-4810e53a3805777d", + "state_hash": "sha256:8a69abe5f51d61d333818427118548224c0bc9d3efb31415e7355caf60e8d5b4", + "state_root_class": "DEV", + "verification": { + "problems": [], + "verified": true + } + }, + "inventory": { + "artifact_digest": "sha256:267f19bf54a7532559919a57e6b6b12c34db87d8c80141369e64c237b7e8f010", + "collected_at": "2026-09-20T17:15:48Z", + "collection_id": "INV-20260920T171548Z-11885ac7cb292572", + "collection_status": "COLLECTED", + "maturity": "W1-C1 — collected and normalized, bound by an artifact digest. NOT part of the frozen W1-A snapshot contract (SNAP-021)", + "schema_version": 1, + "subdomains": { + "compute": { + "collection_status": "COLLECTED", + "data": { + "cores_per_socket": 1, + "cpu_model": "Intel(R) Xeon(R) CPU E5-2640 0 @ 2.50GHz", + "cpu_vendor": "GenuineIntel", + "logical_cpus": 2, + "sockets": 2 + }, + "method": "/proc/cpuinfo", + "reason": null, + "state_dimension": "ACTIVE" + }, + "dns": { + "collection_status": "COLLECTED", + "data": { + "method": "/etc/resolv.conf", + "note": null, + "servers": [ + "192.168.122.1" + ] + }, + "method": "/etc/resolv.conf", + "reason": null, + "state_dimension": "RESOLVED" + }, + "host": { + "collection_status": "COLLECTED", + "data": { + "fqdn": null, + "fqdn_source": "NOT_AVAILABLE_LOCALLY", + "hostname": "isd-sample" + }, + "method": "/proc/sys/kernel/hostname + /etc/hosts", + "reason": null, + "state_dimension": "ACTIVE" + }, + "machine": { + "collection_status": "COLLECTED", + "data": { + "hypervisor": "kvm", + "product": "Ubuntu 24.04 PC (Q35 + ICH9, 2009)", + "vendor": "QEMU", + "virtualized": true + }, + "method": "systemd-detect-virt | /sys/hypervisor + /sys/class/dmi/id", + "reason": null, + "state_dimension": "ACTIVE" + }, + "memory": { + "collection_status": "COLLECTED", + "data": { + "swap_total_bytes": 0, + "total_bytes": 2075668480 + }, + "method": "/proc/meminfo", + "reason": null, + "state_dimension": "ACTIVE" + }, + "network": { + "collection_status": "COLLECTED", + "data": { + "default_routes": [ + { + "family": "inet", + "gateway": "192.168.122.1", + "interface": "enp1s0" + } + ], + "interfaces": [ + { + "loopback": true, + "mtu": 65536, + "name": "lo", + "state": "UNKNOWN" + }, + { + "loopback": false, + "mtu": 1500, + "name": "enp1s0", + "state": "UP" + } + ], + "ipv4": [ + { + "address": "127.0.0.1", + "interface": "lo", + "loopback": true, + "prefix_length": 8, + "scope": "host" + }, + { + "address": "192.168.122.128", + "interface": "enp1s0", + "loopback": false, + "prefix_length": 24, + "scope": "global" + } + ], + "ipv6_stable": [ + { + "address": "::1", + "classification": "LOOPBACK", + "interface": "lo", + "prefix_length": 128, + "scope": "host" + }, + { + "address": "fe80::5054:ff:fe9c:1a7c", + "classification": "LINK_LOCAL", + "interface": "enp1s0", + "prefix_length": 64, + "scope": "link" + } + ], + "ipv6_volatile": [] + }, + "method": "ip -json addr/route", + "reason": null, + "state_dimension": "ACTIVE" + }, + "platform": { + "collection_status": "COLLECTED", + "data": { + "architecture": "x86_64", + "family": null, + "id": "debian", + "init_system": "systemd", + "kernel_release": "6.1.0-53-cloud-amd64", + "pretty_name": "Debian GNU/Linux 12 (bookworm)", + "version_id": "12" + }, + "method": "/etc/os-release + uname(2) + /proc/1/comm", + "reason": null, + "state_dimension": "ACTIVE" + }, + "storage": { + "collection_status": "COLLECTED", + "data": { + "devices": [ + { + "kernel_removable": false, + "kernel_subsystem": "virtio", + "model": null, + "name": "vda", + "queue_rotational": true, + "scsi_peripheral_type": null, + "size_bytes": 6442450944, + "vendor": "0x1af4" + } + ], + "filesystems": [ + { + "fstype": "tmpfs", + "mount_point": "/run", + "options": [ + "inode64", + "mode=755", + "nodev", + "noexec", + "nosuid", + "relatime", + "rw", + "size=202704k" + ], + "read_only": false, + "source": "tmpfs" + }, + { + "fstype": "ext4", + "mount_point": "/", + "options": [ + "discard", + "errors=remount-ro", + "relatime", + "rw" + ], + "read_only": false, + "source": "/dev/vda1" + }, + { + "fstype": "tmpfs", + "mount_point": "/dev/shm", + "options": [ + "inode64", + "nodev", + "nosuid", + "rw" + ], + "read_only": false, + "source": "tmpfs" + }, + { + "fstype": "tmpfs", + "mount_point": "/run/lock", + "options": [ + "inode64", + "nodev", + "noexec", + "nosuid", + "relatime", + "rw", + "size=5120k" + ], + "read_only": false, + "source": "tmpfs" + }, + { + "fstype": "vfat", + "mount_point": "/boot/efi", + "options": [ + "codepage=437", + "dmask=0022", + "errors=remount-ro", + "fmask=0022", + "iocharset=ascii", + "relatime", + "rw", + "shortname=mixed", + "utf8" + ], + "read_only": false, + "source": "/dev/vda15" + }, + { + "fstype": "tmpfs", + "mount_point": "/run/user/1000", + "options": [ + "gid=1000", + "inode64", + "mode=700", + "nodev", + "nosuid", + "nr_inodes=50675", + "relatime", + "rw", + "size=202700k", + "uid=1000" + ], + "read_only": false, + "source": "tmpfs" + } + ], + "utilisation": [ + { + "available_bytes": 207032320, + "mount_point": "/run", + "total_bytes": 207568896, + "used_permille": 3 + }, + { + "available_bytes": 5068783616, + "mount_point": "/", + "total_bytes": 6130061312, + "used_permille": 173 + }, + { + "available_bytes": 1037832192, + "mount_point": "/dev/shm", + "total_bytes": 1037832192, + "used_permille": 0 + }, + { + "available_bytes": 5242880, + "mount_point": "/run/lock", + "total_bytes": 5242880, + "used_permille": 0 + }, + { + "available_bytes": 117364736, + "mount_point": "/boot/efi", + "total_bytes": 129718272, + "used_permille": 95 + }, + { + "available_bytes": 207564800, + "mount_point": "/run/user/1000", + "total_bytes": 207564800, + "used_permille": 0 + } + ] + }, + "method": "/sys/block + /proc/self/mounts + statvfs(2)", + "reason": null, + "state_dimension": "ACTIVE" + }, + "time": { + "collection_status": "COLLECTED", + "data": { + "ntp_enabled": true, + "offset_nanoseconds": null, + "provider": "systemd-timesyncd", + "source": null, + "stratum": null, + "synchronized": true, + "timezone": "Etc/UTC", + "uptime_seconds": 53 + }, + "method": "timedatectl show", + "reason": null, + "state_dimension": "RESOLVED" + } + } + }, + "limitations": [ + "Host identity derives from a locally readable machine identity. It is not remote attestation, and a local root adversary can change the source it derives from.", + "Cloned systems share a machine identity when cloning did not regenerate it, so two hosts can legitimately present the same host_id.", + "Hardware facts — CPU model, core counts, memory capacity, disk topology — are observations, not configuration. They change legitimately on virtualized hosts and do not by themselves indicate drift.", + "Network addresses and routes describe local configuration. They do not prove that anything outside this host can reach it.", + "Configured DNS servers are what this host is told to use. No query was performed, so nothing here describes resolution behaviour.", + "Host inventory is not part of the frozen snapshot contract. It is bound to this report by an artifact digest, which is a content digest and not a snapshot hash.", + "Assessment details identify the person or organization declared in the report metadata. They are declarative and are NOT cryptographically authenticated; this report is not signed." + ], + "provenance": { + "collection_methods": { + "compute": "/proc/cpuinfo", + "dns": "/etc/resolv.conf", + "host": "/proc/sys/kernel/hostname + /etc/hosts", + "machine": "systemd-detect-virt | /sys/hypervisor + /sys/class/dmi/id", + "memory": "/proc/meminfo", + "network": "ip -json addr/route", + "platform": "/etc/os-release + uname(2) + /proc/1/comm", + "storage": "/sys/block + /proc/self/mounts + statvfs(2)", + "time": "timedatectl show" + }, + "engine_version": "0.1.0-alpha1", + "inventory_schema_version": 1, + "report_schema_version": 1 + }, + "report": { + "engine_version": "0.1.0-alpha1", + "generated_at": "2026-09-20T17:15:48Z", + "report_id": "RPT-20260920T171548Z-71072fdd5e6cd97e", + "status": "COMPLETE" + }, + "report_schema_version": 1, + "target": { + "fqdn": null, + "fqdn_source": "NOT_AVAILABLE_LOCALLY", + "host_id": "sha256:4d3e3c2abb1f71c13a45ba590cda91e3ded6c7ccdc3f28dc63b5ac20f74dcfd1", + "hostname": "isd-sample" + } +} + diff --git a/docs/reference/samples/SAMPLE_REPORT.md b/docs/reference/samples/SAMPLE_REPORT.md new file mode 100644 index 0000000..1e4648b --- /dev/null +++ b/docs/reference/samples/SAMPLE_REPORT.md @@ -0,0 +1,179 @@ + + + +# ISEDRAF System Assurance Report + +| | | +|---|---| +| Report ID | `RPT-20260920T171548Z-71072fdd5e6cd97e` | +| Generated (UTC) | 2026-09-20T17:15:48Z | +| ISEDRAF version | 0.1.0-alpha1 | +| Report status | **COMPLETE** | + +> This report describes what was **observed**. It contains no secure/insecure verdict, because host inventory is evidence and a score would be a judgement the data does not support. + +## Target + +| | | +|---|---| +| Hostname | isd-sample | +| FQDN | *not resolvable locally — no resolver was consulted* | +| Host ID | `sha256:4d3e3c2abb1f71c13a45ba590cda91e3ded6c7ccdc3f28dc63b5ac20f74dcfd1` | + +## Evidence + +### Host identity — W1-A CERTIFIED — snapshot-bound, manifest-hashed, ledger-chained + +| | | +|---|---| +| Snapshot ID | `SDS-20260920T171541Z-4810e53a3805777d` | +| Manifest hash | `sha256:32f41a2a89693063b85580791a257363833f0438f32732a645ec63fbcdd09c8f` | +| State hash | `sha256:8a69abe5f51d61d333818427118548224c0bc9d3efb31415e7355caf60e8d5b4` | +| Ledger record | sequence 1, `sha256:1dab505f28bdb65d63fe62e1ecaeecb4f099e6ef4b4109bd6784b2a319f12698` | +| Collection status | COLLECTED | +| Evidence root | `docs/reference/samples/evidence/state-root` (DEV) | +| Independent verification | **PASS** — every hash recomputed from the stored preimages | + +### Host inventory — W1-C1 — collected and normalized, bound by an artifact digest. NOT part of the frozen W1-A snapshot contract (SNAP-021) + +| | | +|---|---| +| Collection ID | `INV-20260920T171548Z-11885ac7cb292572` | +| Artifact digest | `sha256:267f19bf54a7532559919a57e6b6b12c34db87d8c80141369e64c237b7e8f010` | +| Collected (UTC) | 2026-09-20T17:15:48Z | +| Inventory schema | 1 | +| Collection status | COLLECTED | + +The artifact digest is a content SHA-256 over the deterministic inventory artifact. It is **not** a snapshot hash and the inventory is **not** inside the frozen snapshot. + +## Platform + +| | | +|---|---| +| Operating system | Debian GNU/Linux 12 (bookworm) | +| Version | 12 | +| Family | — | +| Kernel | `6.1.0-53-cloud-amd64` | +| Architecture | x86_64 | +| Init system | systemd | +| Machine | virtual | +| Hypervisor | kvm | +| Vendor / product | QEMU / Ubuntu 24.04 PC (Q35 + ICH9, 2009) | + +## Compute and memory + +*Hardware observations. They change legitimately on virtualized hosts and do not by themselves indicate configuration drift.* + +| | | +|---|---| +| CPU | Intel(R) Xeon(R) CPU E5-2640 0 @ 2.50GHz | +| Vendor | GenuineIntel | +| Sockets / cores per socket | 2 / 1 | +| Logical CPUs | 2 | +| Memory | 1.9 GiB | +| Swap | — | + +## Storage + +| Device | Size | Kernel subsystem | Queue rotational | Kernel removable flag | Vendor | Model | +|---|---|---|---|---|---|---| +| `vda` | 6.4 GB | virtio | true | false | 0x1af4 | — | + +*Kernel-reported block-device attributes. `Queue rotational` and `Kernel removable flag` describe how Linux presents the block queue; neither establishes the physical storage medium, and a block device does not necessarily correspond to one physical disk (D-114).* + +| Mount point | Source | Type | Mode | Options | +|---|---|---|---|---| +| `/run` | `tmpfs` | tmpfs | read-write | `inode64,mode=755,nodev,noexec,nosuid,relatime` | +| `/` | `/dev/vda1` | ext4 | read-write | `discard,errors=remount-ro,relatime,rw` | +| `/dev/shm` | `tmpfs` | tmpfs | read-write | `inode64,nodev,nosuid,rw` | +| `/run/lock` | `tmpfs` | tmpfs | read-write | `inode64,nodev,noexec,nosuid,relatime,rw` | +| `/boot/efi` | `/dev/vda15` | vfat | read-write | `codepage=437,dmask=0022,errors=remount-ro,fmask=0022,iocharset=ascii,relatime` | +| `/run/user/1000` | `tmpfs` | tmpfs | read-write | `gid=1000,inode64,mode=700,nodev,nosuid,nr_inodes=50675` | + +**Utilisation** — a volatile observation, not configuration: + +| Mount point | Used | +|---|---| +| `/run` | 0.3% | +| `/` | 17.3% | +| `/dev/shm` | 0.0% | +| `/run/lock` | 0.0% | +| `/boot/efi` | 9.5% | +| `/run/user/1000` | 0.0% | + +## Network + +| Interface | State | MTU | +|---|---|---| +| `enp1s0` | UP | 1500 | + +**IPv4** + +| Address | Interface | Scope | +|---|---|---| +| `192.168.122.128/24` | `enp1s0` | global | + +**IPv6** + +| Address | Interface | Classification | +|---|---|---| +| `fe80::5054:ff:fe9c:1a7c/64` | `enp1s0` | LINK_LOCAL | + +| Default route | Gateway | Interface | +|---|---|---| +| inet | `192.168.122.1` | `enp1s0` | + +**DNS** — `192.168.122.1` + +Source: `/etc/resolv.conf` + +Addresses and routes describe local configuration. They do not prove that anything outside this host can reach it. + +## Time + +| | | +|---|---| +| Timezone | Etc/UTC | +| NTP provider | systemd-timesyncd | +| NTP enabled | yes | +| **Clock synchronized** | yes | + +*NTP enabled and clock synchronized are different facts.* Configuration says what was intended; synchronization says what is true now. + +## Collection completeness + +| Subdomain | Status | Method | Reason | +|---|---|---|---| +| host | COLLECTED | `/proc/sys/kernel/hostname + /etc/hosts` | — | +| platform | COLLECTED | `/etc/os-release + uname(2) + /proc/1/comm` | — | +| machine | COLLECTED | `systemd-detect-virt | /sys/hypervisor + /sys/class/dmi/id` | — | +| compute | COLLECTED | `/proc/cpuinfo` | — | +| memory | COLLECTED | `/proc/meminfo` | — | +| storage | COLLECTED | `/sys/block + /proc/self/mounts + statvfs(2)` | — | +| network | COLLECTED | `ip -json addr/route` | — | +| dns | COLLECTED | `/etc/resolv.conf` | — | +| time | COLLECTED | `timedatectl show` | — | + +An incomplete observation is reported as incomplete. A missing tool is `NOT_TESTED`, a present tool that failed is `ERROR`, and neither is rendered as an empty success. + +## Limitations + +- Host identity derives from a locally readable machine identity. It is not remote attestation, and a local root adversary can change the source it derives from. +- Cloned systems share a machine identity when cloning did not regenerate it, so two hosts can legitimately present the same host_id. +- Hardware facts — CPU model, core counts, memory capacity, disk topology — are observations, not configuration. They change legitimately on virtualized hosts and do not by themselves indicate drift. +- Network addresses and routes describe local configuration. They do not prove that anything outside this host can reach it. +- Configured DNS servers are what this host is told to use. No query was performed, so nothing here describes resolution behaviour. +- Host inventory is not part of the frozen snapshot contract. It is bound to this report by an artifact digest, which is a content digest and not a snapshot hash. +- Assessment details identify the person or organization declared in the report metadata. They are declarative and are NOT cryptographically authenticated; this report is not signed. + diff --git a/docs/reference/samples/evidence/collected_inventory.json b/docs/reference/samples/evidence/collected_inventory.json new file mode 100644 index 0000000..cc658b8 --- /dev/null +++ b/docs/reference/samples/evidence/collected_inventory.json @@ -0,0 +1,2 @@ +{"collection_status":"COLLECTED","schema_version":1,"subdomains":{"compute":{"collection_status":"COLLECTED","data":{"cores_per_socket":1,"cpu_model":"Intel(R) Xeon(R) CPU E5-2640 0 @ 2.50GHz","cpu_vendor":"GenuineIntel","logical_cpus":2,"sockets":2},"method":"/proc/cpuinfo","reason":null,"state_dimension":"ACTIVE"},"dns":{"collection_status":"COLLECTED","data":{"method":"/etc/resolv.conf","note":null,"servers":["192.168.122.1"]},"method":"/etc/resolv.conf","reason":null,"state_dimension":"RESOLVED"},"host":{"collection_status":"COLLECTED","data":{"fqdn":null,"fqdn_source":"NOT_AVAILABLE_LOCALLY","hostname":"isd-sample"},"method":"/proc/sys/kernel/hostname + /etc/hosts","reason":null,"state_dimension":"ACTIVE"},"machine":{"collection_status":"COLLECTED","data":{"hypervisor":"kvm","product":"Ubuntu 24.04 PC (Q35 + ICH9, 2009)","vendor":"QEMU","virtualized":true},"method":"systemd-detect-virt | /sys/hypervisor + /sys/class/dmi/id","reason":null,"state_dimension":"ACTIVE"},"memory":{"collection_status":"COLLECTED","data":{"swap_total_bytes":0,"total_bytes":2075668480},"method":"/proc/meminfo","reason":null,"state_dimension":"ACTIVE"},"network":{"collection_status":"COLLECTED","data":{"default_routes":[{"family":"inet","gateway":"192.168.122.1","interface":"enp1s0"}],"interfaces":[{"loopback":true,"mtu":65536,"name":"lo","state":"UNKNOWN"},{"loopback":false,"mtu":1500,"name":"enp1s0","state":"UP"}],"ipv4":[{"address":"127.0.0.1","interface":"lo","loopback":true,"prefix_length":8,"scope":"host"},{"address":"192.168.122.128","interface":"enp1s0","loopback":false,"prefix_length":24,"scope":"global"}],"ipv6_stable":[{"address":"::1","classification":"LOOPBACK","interface":"lo","prefix_length":128,"scope":"host"},{"address":"fe80::5054:ff:fe9c:1a7c","classification":"LINK_LOCAL","interface":"enp1s0","prefix_length":64,"scope":"link"}],"ipv6_volatile":[]},"method":"ip -json addr/route","reason":null,"state_dimension":"ACTIVE"},"platform":{"collection_status":"COLLECTED","data":{"architecture":"x86_64","family":null,"id":"debian","init_system":"systemd","kernel_release":"6.1.0-53-cloud-amd64","pretty_name":"Debian GNU/Linux 12 (bookworm)","version_id":"12"},"method":"/etc/os-release + uname(2) + /proc/1/comm","reason":null,"state_dimension":"ACTIVE"},"storage":{"collection_status":"COLLECTED","data":{"devices":[{"kernel_removable":false,"kernel_subsystem":"virtio","model":null,"name":"vda","queue_rotational":true,"scsi_peripheral_type":null,"size_bytes":6442450944,"vendor":"0x1af4"}],"filesystems":[{"fstype":"tmpfs","mount_point":"/run","options":["inode64","mode=755","nodev","noexec","nosuid","relatime","rw","size=202704k"],"read_only":false,"source":"tmpfs"},{"fstype":"ext4","mount_point":"/","options":["discard","errors=remount-ro","relatime","rw"],"read_only":false,"source":"/dev/vda1"},{"fstype":"tmpfs","mount_point":"/dev/shm","options":["inode64","nodev","nosuid","rw"],"read_only":false,"source":"tmpfs"},{"fstype":"tmpfs","mount_point":"/run/lock","options":["inode64","nodev","noexec","nosuid","relatime","rw","size=5120k"],"read_only":false,"source":"tmpfs"},{"fstype":"vfat","mount_point":"/boot/efi","options":["codepage=437","dmask=0022","errors=remount-ro","fmask=0022","iocharset=ascii","relatime","rw","shortname=mixed","utf8"],"read_only":false,"source":"/dev/vda15"},{"fstype":"tmpfs","mount_point":"/run/user/1000","options":["gid=1000","inode64","mode=700","nodev","nosuid","nr_inodes=50675","relatime","rw","size=202700k","uid=1000"],"read_only":false,"source":"tmpfs"}],"utilisation":[{"available_bytes":207032320,"mount_point":"/run","total_bytes":207568896,"used_permille":3},{"available_bytes":5068783616,"mount_point":"/","total_bytes":6130061312,"used_permille":173},{"available_bytes":1037832192,"mount_point":"/dev/shm","total_bytes":1037832192,"used_permille":0},{"available_bytes":5242880,"mount_point":"/run/lock","total_bytes":5242880,"used_permille":0},{"available_bytes":117364736,"mount_point":"/boot/efi","total_bytes":129718272,"used_permille":95},{"available_bytes":207564800,"mount_point":"/run/user/1000","total_bytes":207564800,"used_permille":0}]},"method":"/sys/block + /proc/self/mounts + statvfs(2)","reason":null,"state_dimension":"ACTIVE"},"time":{"collection_status":"COLLECTED","data":{"ntp_enabled":true,"offset_nanoseconds":null,"provider":"systemd-timesyncd","source":null,"stratum":null,"synchronized":true,"timezone":"Etc/UTC","uptime_seconds":53},"method":"timedatectl show","reason":null,"state_dimension":"RESOLVED"}}} + diff --git a/docs/reference/samples/evidence/environment.txt b/docs/reference/samples/evidence/environment.txt new file mode 100644 index 0000000..3d4bdd6 --- /dev/null +++ b/docs/reference/samples/evidence/environment.txt @@ -0,0 +1,7 @@ +isedraf_version: isedraf 0.1.0-alpha1 +deb_version: 0.1.0~alpha1 +python: Python 3.11.2 (main, May 12 2026, 05:17:27) [GCC 12.2.0] +kernel: 6.1.0-53-cloud-amd64 x86_64 +PRETTY_NAME="Debian GNU/Linux 12 (bookworm)" +VERSION_ID="12" +ID=debian diff --git a/docs/reference/samples/evidence/state-root/ledger/segment-000001.jsonl b/docs/reference/samples/evidence/state-root/ledger/segment-000001.jsonl new file mode 100644 index 0000000..16ba6c4 --- /dev/null +++ b/docs/reference/samples/evidence/state-root/ledger/segment-000001.jsonl @@ -0,0 +1 @@ +{"record_core":{"event":"snapshot_committed","event_id":"EVT-20260920T171541Z-928303e6ea57f903","ledger_schema_version":1,"manifest_hash":"sha256:32f41a2a89693063b85580791a257363833f0438f32732a645ec63fbcdd09c8f","occurred_at":"2026-09-20T17:15:41Z","previous_record_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000","sequence":1,"snapshot_id":"SDS-20260920T171541Z-4810e53a3805777d","state_root":"DEV"},"record_hash":"sha256:1dab505f28bdb65d63fe62e1ecaeecb4f099e6ef4b4109bd6784b2a319f12698"} diff --git a/docs/reference/samples/evidence/state-root/snapshots/SDS-20260920T171541Z-4810e53a3805777d/manifest.json b/docs/reference/samples/evidence/state-root/snapshots/SDS-20260920T171541Z-4810e53a3805777d/manifest.json new file mode 100644 index 0000000..18d9b04 --- /dev/null +++ b/docs/reference/samples/evidence/state-root/snapshots/SDS-20260920T171541Z-4810e53a3805777d/manifest.json @@ -0,0 +1 @@ +{"manifest_core":{"created_at":"2026-09-20T17:15:41Z","engine_version":"0.1.0-alpha1","host_id":"sha256:4d3e3c2abb1f71c13a45ba590cda91e3ded6c7ccdc3f28dc63b5ac20f74dcfd1","run_id":"RUN-20260920T171541Z-706d3c21624288c6","schema_version":1,"sections":{"host_identity":{"classification_table_version":1,"collection_status":"COLLECTED","collector_id":"host_identity","collector_version":1,"parser_version":1,"reason":null,"source_id":"file:/etc/machine-id","state_hash":"sha256:8a69abe5f51d61d333818427118548224c0bc9d3efb31415e7355caf60e8d5b4"}},"snapshot_id":"SDS-20260920T171541Z-4810e53a3805777d","state_root":"DEV"},"manifest_hash":"sha256:32f41a2a89693063b85580791a257363833f0438f32732a645ec63fbcdd09c8f"} diff --git a/docs/reference/samples/evidence/state-root/snapshots/SDS-20260920T171541Z-4810e53a3805777d/method/host_identity.json b/docs/reference/samples/evidence/state-root/snapshots/SDS-20260920T171541Z-4810e53a3805777d/method/host_identity.json new file mode 100644 index 0000000..7132606 --- /dev/null +++ b/docs/reference/samples/evidence/state-root/snapshots/SDS-20260920T171541Z-4810e53a3805777d/method/host_identity.json @@ -0,0 +1 @@ +{"classification_table_version":1,"collector_id":"host_identity","collector_version":1,"identity_scheme":"machine-id-sha256-v1","parser_version":1,"source_id":"file:/etc/machine-id"} diff --git a/docs/reference/samples/evidence/state-root/snapshots/SDS-20260920T171541Z-4810e53a3805777d/state/host_identity.json b/docs/reference/samples/evidence/state-root/snapshots/SDS-20260920T171541Z-4810e53a3805777d/state/host_identity.json new file mode 100644 index 0000000..e524fd3 --- /dev/null +++ b/docs/reference/samples/evidence/state-root/snapshots/SDS-20260920T171541Z-4810e53a3805777d/state/host_identity.json @@ -0,0 +1 @@ +{"host_id":"sha256:4d3e3c2abb1f71c13a45ba590cda91e3ded6c7ccdc3f28dc63b5ac20f74dcfd1"} diff --git a/scripts/ci/falsifiable.sh b/scripts/ci/falsifiable.sh index 4917f62..8c26b87 100755 --- a/scripts/ci/falsifiable.sh +++ b/scripts/ci/falsifiable.sh @@ -507,6 +507,26 @@ inject "D-86 the rpm changelog states a weekday the date never fell on" \ 'sed -i "s|^\* Fri Sep 18 2026|* Thu Sep 18 2026|" packaging/rpm/isedraf.spec.in' \ 'which was a|packaging metadata gate FAILED' +# IQ-011. The previous sample described a retired storage schema for three versions and +# nothing noticed, because no gate compared a published document against the evidence it +# came from. These two prove the sample cannot drift from its evidence in either +# direction: the document edited by hand, and the evidence changed underneath it. +inject "IQ-011 the published sample drifts from the evidence it is generated from" \ + 'python3 scripts/docs/sample_report.py check' \ + 'sed -i "s/| Kernel subsystem |/| Class |/" docs/reference/samples/SAMPLE_REPORT.md' \ + 'SAMPLE_REPORT.md|does not match the committed evidence' + +inject "IQ-011 the committed evidence changes without the sample being regenerated" \ + 'python3 scripts/docs/sample_report.py check' \ + 'python3 - < +# +# Purpose: Render the published sample report from committed evidence bytes. +# Implements: IQ-011, D-88, D-89, GOV-002 +# +# The previous sample was real output from a lab VM, and when the storage schema changed +# it could not be regenerated: the machine was gone, and the fields that replaced the +# retired ones had never been collected from it. It was withdrawn rather than corrected, +# because writing plausible values for a host nobody can re-observe is inventing evidence. +# +# This generator makes that failure impossible to repeat. The evidence is committed: +# +# evidence/collected_inventory.json verbatim `isedraf inventory --json` output +# evidence/state-root/ the snapshot, manifest and ledger, as written +# evidence/environment.txt what produced them, and on what +# +# Regenerating re-runs the real report model and the real renderers over those bytes, and +# re-verifies the ledger chain while doing it. Anyone can reproduce the published sample +# from a clone. Nobody needs the VM, which is the point - it no longer exists. +# +# What this does NOT reproduce is the collection itself. Reading /sys and /proc requires +# the host, and no committed byte can stand in for it. The sample therefore reproduces +# the REPORT from captured evidence, not the act of collecting - and says so. +# +# meta:type="doc-generator" +# meta:owner="Antonios Voulvoulis / ITCMS" +# meta:stability="EXPERIMENTAL" +# meta:privilege="unprivileged" +# meta:mutates="docs/reference/samples/SAMPLE_REPORT.{json,md}" +# meta:binaries="python3" +# ============================================================================= + +"""usage: sample_report.py [generate|check]""" +import json +import os +import subprocess +import sys + +ROOT = subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip() +sys.path.insert(0, os.path.join(ROOT, "lib")) + +from isedraf.report import artifact # noqa: E402 +from isedraf.report import model as report_model # noqa: E402 +from isedraf.report import render # noqa: E402 + +SAMPLES = os.path.join(ROOT, "docs", "reference", "samples") +EVIDENCE = os.path.join(SAMPLES, "evidence") +INVENTORY = os.path.join(EVIDENCE, "collected_inventory.json") + +# Repo-relative, and resolved from the repository root, because the report records the +# evidence root it read. An absolute path would bake one machine's checkout location into +# a published document and make the sample regenerate differently on every clone - which +# is the opposite of the property this file exists to provide. +STATE_ROOT = os.path.join("docs", "reference", "samples", "evidence", "state-root") + +# Pinned so the sample is stable. Both are recorded in NON_REPRODUCIBLE precisely because +# they MUST differ between two renderings of the same evidence; pinning them here is what +# lets the rest of the document be compared byte for byte. These are the real values from +# the collection that produced the committed evidence, not invented ones. +REPORT_ID = "RPT-20260920T171548Z-71072fdd5e6cd97e" +GENERATED_AT = "2026-09-20T17:15:48Z" + +# The inventory artifact identity is stamped when a report is built, not when the +# inventory is collected, so rendering twice would otherwise produce two collection_ids +# and two artifact_digests for the same bytes. These are the real values from the +# collection that produced the committed evidence - taken from that run's own report, +# not chosen - and pinning them is what makes the digest a function of the evidence. +COLLECTION_ID = "INV-20260920T171548Z-11885ac7cb292572" +COLLECTED_AT = "2026-09-20T17:15:48Z" + +HEADER = """ + + +""" + + +def build(): + os.chdir(ROOT) + with open(INVENTORY, "rb") as fh: + inventory = json.loads(fh.read().decode("utf-8")) + art = artifact.build(inventory, collected_at=COLLECTED_AT, + collection_id=COLLECTION_ID) + return report_model.build(STATE_ROOT, inventory, generated_at=GENERATED_AT, + report_id=REPORT_ID, inventory_artifact=art) + + +def rendered(): + m = build() + return render.to_json(m) + "\n", HEADER + render.to_markdown(m) + + +def main(argv): + action = argv[1] if len(argv) > 1 else "generate" + js, md = rendered() + paths = ((os.path.join(SAMPLES, "SAMPLE_REPORT.json"), js), + (os.path.join(SAMPLES, "SAMPLE_REPORT.md"), md)) + if action == "generate": + for path, text in paths: + with open(path, "w") as fh: + fh.write(text) + print(" generated %s" % os.path.relpath(path, ROOT)) + return 0 + if action == "check": + stale = [] + for path, text in paths: + try: + with open(path) as fh: + current = fh.read() + except OSError: + stale.append(os.path.relpath(path, ROOT) + " (missing)") + continue + if current != text: + stale.append(os.path.relpath(path, ROOT)) + if stale: + print("=== the published sample does not match the committed evidence ===") + for s in stale: + print(" FAIL %s" % s) + print(" The sample is generated from docs/reference/samples/evidence/.") + print(" run: python3 scripts/docs/sample_report.py generate") + return 1 + print(" OK sample report regenerates byte-identically from committed evidence") + return 0 + print(__doc__.strip(), file=sys.stderr) + return 64 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) From 5d8ff4855bbc4c6ff1f9d88239f595b5647ea560 Mon Sep 17 00:00:00 2001 From: Antonios Voulvoulis Date: Sun, 20 Sep 2026 20:27:36 +0300 Subject: [PATCH 2/2] The sample keeps what it observed, because that is the point of a sample Owner decision, 2026-09-20. The lab disclosure review of the IQ-011 sample accepts all three of the facts it discloses about the machine it ran on: the QEMU machine description, the CPU model string, and the libvirt default network 192.168.122.0/24. None is a credential, customer data, unique private addressing, restricted content or secret infrastructure information. They are observations from a disposable VM. No re-capture. Re-collecting behind a synthetic CPU and machine type purely to make the output less specific would turn the sample into a cosmetically sanitized synthetic host, which is not what it is for. It exists to show what ISEDRAF actually observes. The virtio disk is the clearest argument for keeping it as captured: kernel_subsystem=virtio with queue_rotational=true, over storage that is SSD-backed. The queue flag is evidence. "HDD" would have been interpretation beyond the evidence, and a sanitized sample would have hidden the exact case that made D-114 necessary. Implements: IQ-011, D-90 Assisted-by: Claude (recording the owner decision) --- docs/IMPLEMENTATION_QUESTIONS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/IMPLEMENTATION_QUESTIONS.md b/docs/IMPLEMENTATION_QUESTIONS.md index 7994088..c1a056b 100644 --- a/docs/IMPLEMENTATION_QUESTIONS.md +++ b/docs/IMPLEMENTATION_QUESTIONS.md @@ -22,4 +22,4 @@ Assumptions only — **never authority**. The owner resolves; resolutions become | IQ-008 | CONFLICT | STORE-001 | `/run/isedraf/` | Implemented the lock at `/var/lib/isedraf/.lock` | The sketch placed the lock at `/run/isedraf/isedraf.lock`. `STORE-001` puts `.lock` inside the state root, which also makes the lock and the store it protects share a filesystem — a lock on a different filesystem cannot guarantee exclusion if the store is mounted elsewhere. `/run/isedraf/` remains available for future ephemeral state. | `STORE-001` | OPEN | | IQ-009 | GAP | SCOPE-077, SNAP-015 | `lib/isedraf/cli.py`, `lib/isedraf/verify.py` | Verification runs in-process at the end of `identity`; a failure exits `64` (engine error) | `SNAP-015` assigns `exit 5` to `integrity_failure`, and `SCOPE-077` states `5` is not reachable in W1-A. A standalone `isedraf verify` therefore has **no frozen exit code for "verification failed"**. No exit code was invented: verification stays in-process, and the standalone command is deferred until the W1-A exit set is extended or `verify` is scoped to a later set. | discovered implementing W1-B | OPEN | | IQ-010 | CONFLICT | SCOPE-070, SCOPE-071, SCOPE-072, STORE-001, PRIV-005 | `lib/isedraf/stateroot.py` | `resolve()` refuses to fall back to a production root this slice cannot reach, and names the requirements | **Not a contradiction between frozen rules — an implementation defect.** `SCOPE-070` already states W1 *"runs under `ISEDRAF_STATE_ROOT`"*, and `PRIV-005`'s Mode A — `sudo isedraf`, root supervisor inside a sandbox, state-root writability preflight — is the only execution topology that ever owns `/var/lib/isedraf`. Mode A is `DEFERRED_TO_FREEZE_SET_2` by `SCOPE-072`, so **W1 has no production mode by design**. The implementation was offering one and failing with a bare permission error. Answers for Freeze Set 2, when Mode A lands: the **root supervisor** creates and owns the root at mode `0700`; packaging creates it at install time; no group-readable relaxation, since `STORE-001` freezes `0700`; `sudo -u` is not a path, because `SCOPE-071` refuses anything reached through sudo and `PRIV-004` refuses when `SUDO_USER` is set | `env -u ISEDRAF_STATE_ROOT isedraf identity` now explains rather than failing on `EACCES` | **CLOSED_IMPLEMENTATION_FIX** | -| IQ-011 | GAP | D-114, STORAGE-SEMANTICS-001 | `docs/reference/samples/` (withdrawn), `docs/reference/CONTROL_EVIDENCE_MAP.md` | The rendered sample report was **withdrawn** rather than edited. It was real output from disposable lab VM `isd-a9` (AlmaLinux 9.7, KVM) and reported a storage device `type` of `ROTATIONAL` / `OPTICAL` — the exact inference D-114 retired, published as an example of what the tool claims. | It could not be regenerated: `isd-a9` no longer exists, the surviving lab corpus belongs to another project and is read-only, and `kernel_subsystem`, `queue_rotational`, `kernel_removable` and `scsi_peripheral_type` were never collected from that host, so re-rendering the old collection cannot produce them. Writing plausible values would be inventing evidence for a machine nobody can re-observe, which is the one failure this project exists to prevent. **Proposal:** regenerate after the RC on a disposable VM created for the purpose, and add a documentation generator so the sample is reproducible from committed bytes rather than from a VM that only one person ever had. A `check_storage_vocabulary` gate now rejects the retired vocabulary if it returns. | `git log 6ca913e`; storage semantics proofs, this milestone | **CLOSED_IMPLEMENTED** — a disposable Debian 12 VM was built for the purpose, the released 0.1.0-alpha1 package was installed on it, and an unprivileged collection produced the sample. The evidence is committed under `docs/reference/samples/evidence/` and the sample is generated from it by `scripts/docs/sample_report.py`, gated by `make check-sample-report` and two falsification injections. No value was supplied by hand. | +| IQ-011 | GAP | D-114, STORAGE-SEMANTICS-001 | `docs/reference/samples/` (withdrawn), `docs/reference/CONTROL_EVIDENCE_MAP.md` | The rendered sample report was **withdrawn** rather than edited. It was real output from disposable lab VM `isd-a9` (AlmaLinux 9.7, KVM) and reported a storage device `type` of `ROTATIONAL` / `OPTICAL` — the exact inference D-114 retired, published as an example of what the tool claims. | It could not be regenerated: `isd-a9` no longer exists, the surviving lab corpus belongs to another project and is read-only, and `kernel_subsystem`, `queue_rotational`, `kernel_removable` and `scsi_peripheral_type` were never collected from that host, so re-rendering the old collection cannot produce them. Writing plausible values would be inventing evidence for a machine nobody can re-observe, which is the one failure this project exists to prevent. **Proposal:** regenerate after the RC on a disposable VM created for the purpose, and add a documentation generator so the sample is reproducible from committed bytes rather than from a VM that only one person ever had. A `check_storage_vocabulary` gate now rejects the retired vocabulary if it returns. | `git log 6ca913e`; storage semantics proofs, this milestone | **CLOSED_IMPLEMENTED** — a disposable Debian 12 VM was built for the purpose, the released 0.1.0-alpha1 package was installed on it, and an unprivileged collection produced the sample. The evidence is committed under `docs/reference/samples/evidence/` and the sample is generated from it by `scripts/docs/sample_report.py`, gated by `make check-sample-report` and two falsification injections. No value was supplied by hand. **Lab disclosure review, owner decision 2026-09-20:** the sample is kept exactly as captured. The QEMU machine description (`Ubuntu 24.04 PC (Q35 + ICH9, 2009)`), the CPU model string (`Intel(R) Xeon(R) CPU E5-2640 0 @ 2.50GHz`) and the libvirt default network `192.168.122.0/24` are ACCEPTED: disposable lab observations, containing no credentials, customer data, unique private addressing, restricted content or secret infrastructure information. No re-capture. Re-collecting behind `--cpu qemu64` and a synthetic machine type purely to make the output less specific would make the sample a cosmetically sanitized synthetic host, which is not what it is for: it demonstrates what ISEDRAF actually observes. The virtio result is the clearest evidence of that - `kernel_subsystem=virtio` with `queue_rotational=true` over SSD-backed storage is a real-world demonstration of why D-114 was necessary, because the queue flag is evidence and "HDD" would have been interpretation beyond it. |