From ca6b655b86ea02a46144cda7e9d9b6d1b18b6268 Mon Sep 17 00:00:00 2001 From: Arena Agent Date: Sun, 25 Oct 2026 20:11:12 +0000 Subject: [PATCH 1/3] docs(security): publish the recon, colour-team assessment and ultraplan Adds docs/security/: a static review of the request surface, data handling and deployment assumptions at d6e8671, mapped against GDPR, the HIPAA Security Rule and the UK Government Security Classifications TOP SECRET (Tier 3) baseline behaviours. - recon-report.md: 17 findings (SEC-01..17), each with file-and-line evidence; 12 existing controls recorded as sound so the plan does not disturb them; standards matrices (GDPR article by article, HIPAA specification by specification, 33 TOP SECRET behaviours classified as application, inherited or no-software-expression); an evidence log of 20 reproducible greps; and an explicit list of what the review could not run. - colour-teams.md: white/red/blue/purple/green/gold exercise over CyBOK v1.1 knowledge areas, including where the teams disagree and how each conflict resolves under an eliminate/substitute/notify rule. - ultraplan.md: W0-W8 sequenced into 18 revertible pull requests, with the acceptance test, ledger events, standards clauses and friction cost of each, and a four-rung ladder (OFFICIAL -> OFFICIAL-SENSITIVE -> SECRET-candidate -> TOP SECRET) so partial delivery stays a truthful claim. - SECURITY.md points at the dossier; .gitignore publishes docs/security/ under the existing documentation allowlist. No source file is changed and no test is claimed: the review environment has no Julia, R or bun, and the clone is shallow. The dossier says so in its own limitations section. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- .gitignore | 3 +- SECURITY.md | 15 + docs/security/README.md | 45 ++ docs/security/colour-teams.md | 289 ++++++++++ docs/security/recon-report.md | 979 ++++++++++++++++++++++++++++++++++ docs/security/ultraplan.md | 725 +++++++++++++++++++++++++ 6 files changed, 2055 insertions(+), 1 deletion(-) create mode 100644 docs/security/README.md create mode 100644 docs/security/colour-teams.md create mode 100644 docs/security/recon-report.md create mode 100644 docs/security/ultraplan.md diff --git a/.gitignore b/.gitignore index 1794badb..f780b9f6 100644 --- a/.gitignore +++ b/.gitignore @@ -248,12 +248,13 @@ vignettes/*.pdf .Renviron ## Documentation kept local except published release, cladistics, format, maintenance, -## statistics, and schema notes +## statistics, schema notes, and the published security dossier docs/* !docs/release-notes/ !docs/cladistics/ !docs/format-migration.md !docs/maintenance/ +!docs/security/ !docs/statistics/ !docs/schemas/ diff --git a/SECURITY.md b/SECURITY.md index 4419f9a1..3b625096 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -42,3 +42,18 @@ A green workflow is not a claim that all security findings are resolved. Missing tools, inaccessible GitHub alert APIs, absent fleet report-backs and unverified K9 L3 signatures must remain visible as missing or inapplicable evidence. + +## Security dossier + +`docs/security/` holds a full static review of the request surface, the data +handling, and the deployment assumptions: findings with file-and-line +evidence, a mapping against GDPR, the HIPAA Security Rule and the UK +Government Security Classifications TOP SECRET (Tier 3) baseline behaviours, a +CyBOK-referenced colour-team exercise, and a phased plan +(`docs/security/ultraplan.md`) with a zero-friction rule: each exposure is +eliminated, substituted or notified, never pushed back onto the user as an extra +step. + +Start at [`docs/security/README.md`](docs/security/README.md). The dossier is a +gap analysis and a plan. It is explicitly not an accreditation, and it does not +claim that any control it describes has been implemented. diff --git a/docs/security/README.md b/docs/security/README.md new file mode 100644 index 00000000..96aa2577 --- /dev/null +++ b/docs/security/README.md @@ -0,0 +1,45 @@ + + + +# Security dossier + +A read-first set for anyone who has to decide whether this tool may hold data +belonging to other people, and what it would take for it to hold the most +sensitive material in the country. + +| Document | Answers | +| --- | --- | +| [`recon-report.md`](recon-report.md) | What the system is, what protects it today, what does not, and how that scores against GDPR, the HIPAA Security Rule, and the UK TOP SECRET (Tier 3) baseline behaviours. Every finding cites a file and line, and every absence claim has the grep that proves it. | +| [`colour-teams.md`](colour-teams.md) | The CyBOK-referenced colour-team exercise: white (governance), red (adversary), blue (detection), purple (validation), green (build/crypto), gold (human/insider) — including where the teams disagree and how each conflict resolves. | +| [`ultraplan.md`](ultraplan.md) | The plan to close the gaps, ordered by rung on a capability ladder, under a hard **zero-friction** rule (eliminate, substitute, or notify — never "ask the user to be careful"). Each workstream names its files, its ledger events, its acceptance tests and its PR decomposition. | + +## Read in this order + +1. `recon-report.md` §1 (executive summary) and §5 (standards matrices). +2. `colour-teams.md` §7 (conflicts) and §8 (heat map) — this is the argument for + the plan's ordering. +3. `ultraplan.md` §1 (the four rules) and §5 (PR order). +4. `recon-report.md` §9 to re-run the evidence yourself. + +## The three sentences that matter + +1. The scientific code is well built; the **architecture** has no identity, no + audit, no encryption and no classification, and every serious finding follows + from one of those four. +2. **TOP SECRET is an accreditation of a system, not a property of an + application.** The achievable statement today is a *rung* on a ladder, not a + classification. +3. The first PR (`ultraplan.md` W1) removes six live attack paths with security + headers, size caps and refusals, adds no user friction, and does not touch a + single line of analysis code. + +## Provenance of this dossier + +Written against `main` at `d6e8671`. The review was **static**: no Julia, R or +bun runtime was available, the clone is shallow, and no request was sent to a +running server. `recon-report.md` §0.3 lists what that means for each claim, and +§10 lists what is explicitly *not* claimed. Do not cite this dossier as a +penetration test, an accreditation, or a clean bill of health. + +Corrections are welcome as ordinary pull requests: if a finding is wrong, the +grep that proves it wrong belongs in the same commit. diff --git a/docs/security/colour-teams.md b/docs/security/colour-teams.md new file mode 100644 index 00000000..993d232c --- /dev/null +++ b/docs/security/colour-teams.md @@ -0,0 +1,289 @@ + + + +# CyBOK-referenced colour-team exercise + +Companion to [`recon-report.md`](recon-report.md) and +[`ultraplan.md`](ultraplan.md). Finding IDs (`SEC-nn`) refer to the recon +report. + +## 0. What this is, and what it is not + +CyBOK is a **body of knowledge** (21 knowledge areas, v1.1 July 2021; v1.2 +September 2026 revised *Secure Software Lifecycle* and *Software Security*). It +is not an assessment methodology and it issues no certificates. The **colour-team +structure used here is an analytic device I am imposing** on the CyBOK material, +because it forces each knowledge area to produce a different kind of claim: + +| Team | Colour convention | Question it must answer | CyBOK KAs exercised | +| --- | --- | --- | --- | +| White | Referee / governance | *Is this lawful, documented, and accreditable?* | RMG, LR, POR | +| Red | Adversary | *Can I make it do something it should not?* | AB, WAM, SS, MAT, NS | +| Blue | Defender | *Would I know? Could I reconstruct it?* | SOIM, F | +| Purple | Validator | *Does the defence actually detect the attack?* | AB + SOIM | +| Green | Builder | *Is the thing we ship and run trustworthy?* | SSL, SS, C, AC, OSV, DSS | +| Gold | Human / insider | *What will real people actually do?* | HF, AAA | + +KA acronyms: RMG Risk Management & Governance · LR Law & Regulation · POR Privacy +& Online Rights · AB Adversarial Behaviours · WAM Web & Mobile Security · SS +Software Security · MAT Malware & Attack Technologies · NS Network Security · +SOIM Security Operations & Incident Management · F Forensics · SSL Secure +Software Lifecycle · C Cryptography · AC Applied Cryptography · OSV Operating +Systems & Virtualisation · DSS Distributed Systems Security · HF Human Factors · +AAA Authentication, Authorisation & Accountability. + +Scoring: **C**ritical / **H**igh / **M**edium / **L**ow / **P**ass. +"Evidence" means a resolvable location in the repository at `d6e8671`. + +Rules of engagement for this simulation: no exploitation, no live system, no +data. Every red-team result is a source-level reasoning step that a future test +must confirm (see `ultraplan.md` W7). + +--- + +## 1. White team — governance, law, risk (RMG, LR, POR) + +**Charter.** Decide whether the system can *lawfully and documentedly* hold the +data the brief proposes, before anyone writes code. + +### 1.1 Findings + +| ID | Finding | Sev | Evidence | +| --- | --- | --- | --- | +| W-01 | No threat model, no data-flow map, no asset inventory anywhere in the repo. `SECURITY.md` is a *policy* document (how to report a vulnerability), not a model of what is being protected. | H | `SECURITY.md`; `grep -rn 'threat model' SECURITY.md README.md docs/` → none before this change (only this dossier matches afterwards) | +| W-02 | No DPIA and no screening decision. The system processes microbiome data; host-associated studies routinely carry participant/site codes in sample names and metadata columns, which is at least pseudonymised personal data and may be Art. 9 health data. | H | `config/composition.yml` accepts arbitrary metadata keys; no purpose/basis field exists | +| W-03 | Controller/processor position undocumented. Today the operator is the controller; if the tool is ever hosted for others, the author becomes a processor and needs Art. 28 terms. | M | No terms, no hosting model in `README.md` | +| W-04 | No record of processing (Art. 30) and no retention schedule. Deletion is manual `rm`, so retention is "whatever the operator remembers". | H | `studies.jl:233`, `runs.jl:465` | +| W-05 | No incident-response procedure and no breach-assessment path, while the app has a one-request public-disclosure route. | H | `doi.jl:58-88` | +| W-06 | No classification policy, so no way for a user to know what a study *is* in handling terms. | C for target | `grep -rniE 'classification\|top secret' src/ config/` → none | +| W-07 | No vulnerability-disclosure SLA or response-time commitment. `SECURITY.md` says only that fixes target `main` and implies no commitment — honest, but an assessor will note it. | L | `SECURITY.md` §"Supported versions" | +| W-08 | Accreditation artefacts absent: no security target, no controls matrix, no evidence index, no residual-risk statement, no independent review record. | C for target | `docs/` has no `security/` before this change | + +### 1.2 The determination the white team must make first + +**Can MetaManifold-WebUI be accredited at TOP SECRET?** Not as a general-purpose +application, and not by adding features. Three independent reasons: + +1. **Assurance, not features.** TOP SECRET requires extremely high assurance of + protection against extremely capable actors. Assurance is bought with + evaluated components, reproducible and attributable builds, independent + assessment, and configuration control at a level a research codebase with a + dynamic dependency graph cannot demonstrate. Nothing in the ultraplan changes + that; it makes the code *not the blocker*. +2. **The stack is inherited whole.** The trust boundary is the host + (`recon-report.md` §2.2). TOP SECRET would inherit the OS, the browser, the R + and Julia runtimes, CRAN and the Julia registry; each is a supply-chain + surface with its own assurance story. +3. **The behaviours are environmental.** Sixteen of the thirty-three Tier 3 + behaviours have no software expression at all + (`recon-report.md` §5.3): rooms, TSCM, couriers, DV escorts, secure + destruction products, the protected registry book. + +The achievable ladder — and the one the ultraplan is built on — is: + +| Rung | What it means | What it requires that does not exist today | +| --- | --- | --- | +| **R0 — as-is** | Trusted local tool | nothing | +| **R1 — OFFICIAL-ready** | May hold OFFICIAL data on a managed workstation | headers (SEC-10), audit (SEC-02), restrictive modes (SEC-03a), two-phase delete (SEC-08), publication gate (SEC-05) | +| **R2 — OFFICIAL-SENSITIVE-ready** | May hold sensitive personal data | identity (SEC-01), FDE attested (SEC-03), retention + DSAR (SEC-14), export scrubber (SEC-13), incident procedure (SEC-17) | +| **R3 — SECRET-candidate** | Deployable inside an accredited SECRET environment | everything in R2 + marking/copy numbers/registry (SEC-04) + egress allowlist (SEC-30/TS-30) + signed reproducible builds + an independent assessment | +| **R4 — TOP SECRET** | Inside an accredited Tier 3 SIN, single-classification instance, no remote access, hardware-attested | R3 + platform accreditation, personnel controls, formal assurance evidence, cryptographic approval of the at-rest scheme | + +**White-team verdict:** the honest target statement for the author is *"R2 in +this release, R3 in the next, R4 only as a certified component inside an +accredited environment"*. Any communication that says "TOP SECRET compliant" +would be false, and would be the first thing an assessor dismantles. + +--- + +## 2. Red team — adversarial behaviours (AB, WAM, SS, MAT, NS) + +**Charter.** Break it. Preconditions are stated because the severity depends +entirely on them. + +| ID | Scenario | Precondition | Result | Sev | CyBOK | +| --- | --- | --- | --- | --- | --- | +| R-01 | **Delete a study** with `DELETE /api/v1/studies/{study}` | reach the port; no active job | **Succeeds.** No auth, no confirmation, no record. Raw FASTQ and all results gone. | C | WAM, AB | +| R-02 | **Clickjack** the operator into R-01 or a pipeline run | operator visits any attacker page | **Succeeds.** SPA sets no `frame-ancestors`/`X-Frame-Options`; the click is same-origin so the allowlist (G3) is irrelevant. | H | WAM, HF | +| R-03 | **Rewrite reference databases** then run the pipeline | reach the port | **Succeeds.** `PUT /api/v1/databases` accepts a new `uri` with no `sha256`; `_database_warnings` only advises; download then fetches attacker content. Scientific conclusions become attacker-controlled. | H | SS, DSS | +| R-04 | **Publish to the world** | `DOI_ENABLED=true` + token (the documented configuration) | **Succeeds.** `confirm:true` is the only gate. Irreversible. | H | AB, LR | +| R-05 | **Read another study / another user's data from the host** | a login on the same machine | **Succeeds.** No auth, default umask, no at-rest encryption. | H | OSV, AAA | +| R-06 | **Deny service** with a large body or a burst of job submissions | reach the port | **Succeeds.** No global body cap (`server.jl:96-118`); no running-job cap; one embedded R interpreter. | H | MAT, NS | +| R-07 | **Path traversal to read `/etc/passwd`** via `/files/…`, `{file}`, `{study}`, or a report file name | reach the port | **Fails.** `_unsafe_path_segment` (`server.jl:190`), `SAFE_NAME_RE` (`validate.jl:21`), `realpath` containment (`server.jl:404-410`, `doi.jl:31-45`), extension allowlist (`trees.jl`). | P | WAM | +| R-08 | **SQL injection** through `{table}`/`{column}`/`sort`/filters | reach the port | **Fails today** — but by consequence, not by validation: a bogus table yields no columns and the route 404s before the interpolated statement (`duckdb_helpers.jl:261-270,313-315`). A refactor re-opens it. | M (latent) | SS, DSS | +| R-09 | **Shell / R code execution** via config values or a formula | config write (which is unauthenticated but shape-checked) | **Fails.** `_SAFE_ARGS_RE` (`tools.jl:86`), `_sq` (`tools.jl:19`), `is_shell_safe`/`is_shell_safe_arg` (`validate.jl:50-68`), remote target validation (`remote_exec.jl:19-48`), strict formula AST (`ordination.jl:216-260`). This is well done. | P | SS | +| R-10 | **XSS** into the operator's browser via a study name, tree file, figure title, or error message | plant content, get it viewed | **Fails.** React escapes; no `dangerouslySetInnerHTML`/`innerHTML`/`eval` in `frontend/src`; errors go through JCS (`server.jl:73-91`); uploaded bytes are served with a sandboxing CSP (`report.jl:140`). Residual: no CSP on the app shell, so *if* a sink ever appears there is no second line of defence (SEC-10). | L | WAM | +| R-11 | **Secret extraction** from the browser or the repo | — | **Fails.** Token is env-only and redacted (`core/doi.jl:112-120`); gitleaks runs full history in CI with an estate baseline that fails closed; commits must be signed. | P | SS, SSL | +| R-12 | **Supply-chain substitution** of a GitHub Action or installer download | CI or install | **Fails.** Actions are SHA-pinned with version comments; `install.sh` uses `--proto '=https'` plus recorded sha256; Julia `Manifest.toml` and `bun.lock` pin trees. Remaining gap: no SBOM and no reproducible build, so an *artifact* substitution at release time is not detectable. | M | SSL | + +**Red-team summary.** Ten of twelve attack paths against the *code surface* are +closed and were closed deliberately — this is a well-built application. Every one +of the successful paths is an **architecture** failure, not a coding failure: +there is no identity, so the attacker does not need to defeat anything. + +--- + +## 3. Blue team — operations and forensics (SOIM, F) + +**Charter.** If red wins, what does the defender see, and what can they +reconstruct afterwards? + +### 3.1 Telemetry inventory (what actually exists) + +| Source | Contains | Actor attribution | Tamper-evident | Retention | +| --- | --- | --- | --- | --- | +| HTTP access log | **does not exist** (`access_log=nothing`, `server.jl:498`) | — | — | — | +| `pipeline.log` | narrative, tool-log dumps, resolved command lines (`log.jl:27-33,68`, `tools.jl:45-52`) | **no** | no (append-only file, no hash chain) | unbounded | +| `attestation.yml` | host, versions, config sha256, run identity (`provenance.jl:520-545,790-830`) | **no** (host only) | config hash yes | with the run | +| config hashes | per-stage invalidation (`config.jl:_STAGE_SECTIONS`) | no | yes (sha256) | with the run | +| `derived/` records | content-addressed, refuse tampering (`derived_analyses.jl:69`) | no | **yes** | with the run | +| Julia `@info`/`@warn` to the console | operational noise | no | no | console only | +| CI: gitleaks, Scorecard, Hypatia | repository-level | per-commit | yes (GitHub) | GitHub | + +### 3.2 Findings + +| ID | Finding | Sev | +| --- | --- | --- | +| B-01 | **No request telemetry.** Nothing records method, path, status, size, time or source for any request. Every red scenario R-01 to R-06 is invisible. | C | +| B-02 | **No authentication events** because there is no authentication. No failed-access signal exists, so there is nothing to alert on. | C | +| B-03 | **Destruction leaves no trace.** Delete → gone, with no ledger entry, no hash of what was removed, no count. | C | +| B-04 | **No integrity baseline for `data/` or `config/`.** Results have hashes; inputs do not. An out-of-band edit of a FASTQ or `pipeline.yml` between runs is undetectable. | H | +| B-05 | **No log egress/retention policy.** Nothing ships logs anywhere; nothing rotates them; `pipeline.log` grows unbounded (`log.jl` appends, `finalise_log` strips only its own section). | M | +| B-06 | **Forensic readiness is better than detection.** Unusually, provenance is genuinely useful *after* an incident: `log_written` hashes every artefact (`log.jl:27-33`), config hashes pin the inputs per stage, and `derived/` refuses a mismatched record. A post-hoc "what changed" question is answerable *if* the attacker did not delete the run directory. | strength | +| B-07 | **Blast radius is the host.** No process isolation between studies, no per-study filesystem boundary, no seccomp/container guidance in the docs. | M | + +### 3.3 What blue needs, in order + +1. Structured request log to a file (`access_log=DevNull`-style replacement is a + one-line change; a structured writer is a small one) — enables everything + else. +2. Hash-chained audit ledger with a fixed event vocabulary (the same records + satisfy GDPR accountability, HIPAA audit controls, and the TOP SECRET + registry analogue — one control, three standards). +3. Input integrity baseline: record sha256 of every input file at run start and + re-verify at run start of the next run. +4. Alert surface: a single “unexpected” channel (startup warnings, failed + authorisation, classification breach attempt) written to both the ledger and + stderr so an operator sees it without a dashboard. + +--- + +## 4. Purple team — does the defence detect the attack? (AB + SOIM) + +For each red scenario: what would we see today, and what does the proposed +control add? This is the check that stops the ultraplan from shipping controls +that look good and detect nothing. + +| Red | Detected today? | Proposed control | Event it emits | +| --- | --- | --- | --- | +| R-01 delete | **No** | two-phase delete (SEC-08) | `delete.requested` (actor, study, item, hash of manifest), `purge.committed` | +| R-02 clickjack | **No** | `frame-ancestors 'none'` (SEC-10) | attack is eliminated; no event needed | +| R-03 database swap | **No** | require https+sha256 (SEC-06) | `config.write databases` with old/new sha256 and diff summary | +| R-04 publish | **No** | classification gate (SEC-05) | `publish.requested`, `publish.refused`, `publish.completed` (with DOI and file hashes) | +| R-05 peer read | **No** | identity + modes + FDE (SEC-01/03) | `authz.denied`; filesystem access is out of scope for the app and must be OS-audited | +| R-06 DoS | **No** (only a console error) | caps (SEC-07, SEC-15) | `limit.exceeded` (route, principal, limit) | +| R-08 SQLi attempt | **No** | identifier validation (SEC-12) | `bad_request` with the rejected identifier (ledger, sanitised) | +| R-09 shell injection attempt | **No** | already blocked; emit on refusal | `config.write refused` with the offending value | +| R-10 XSS | **No** | CSP (SEC-10) | `csp.violation` if a report-uri is added later | +| R-12 supply chain | **Partly** (CI-level) | SBOM + reproducible build + signed release | release attestation stored in-repo | + +**Purple verdict:** 8 of 10 red scenarios are undetected today. Every proposed +control in the ultraplan names the event it emits, and the ledger schema is +designed once (fixed vocabulary) so that later controls do not invent new +telemetry. + +--- + +## 5. Green team — build, crypto, platform (SSL, SS, C, AC, OSV, DSS) + +| ID | Finding | Sev | Evidence | +| --- | --- | --- | --- | +| G-01 | **Crypto is absent, not weak.** No at-rest encryption, no in-transit encryption in the server, no key management, no signing of artefacts. | C for target | `grep -rniE 'encrypt\|cipher' src/` → none; `grep -rniE '\bTLS\b\|certfile' src/` → none | +| G-02 | **No SBOM and no reproducible build.** Julia `Manifest.toml` and `renv.lock` pin trees with hashes, `bun.lock` pins the frontend, but nothing emits a machine-readable inventory per release, and two builds of the same commit are not shown to be identical. | H | absence | +| G-03 | **No signed release artefact.** Commits must be Verified (good, `notes/commit-signing.md`), but no release bundle is signed and there is no provenance attestation (SLSA-style) for the built frontend. | H | `.github/workflows/` | +| G-04 | **No security test lane** beyond `test/unit/test_server_security.jl` (Origin/Host/headers). No property tests over the HTTP surface, no fuzzing of the JSON/YAML/NEWICK parsers, no ZAP-style baseline. | M | `test/unit/` | +| G-05 | **Runtime privilege is the operator's.** Documentation does not recommend a dedicated low-privilege service account, a container, or a systemd sandbox. | M | `start.sh`, `README.md` | +| G-06 | **Third-party code executes with full privilege**: `bash -lc` (`tools.jl:50`), RCall in-process (`r_runtime.jl`), DuckDB extensions implied by `read_csv_auto`. A compromise of any tool is a compromise of everything, including the data. | M | code | +| G-07 | **No dependency advisory gate in CI.** Dependabot is configured and Scorecard runs; no `osv-scanner`/`cargo-audit`-equivalent step fails the build on a known-vulnerable package. | M | `.github/workflows/ci.yml` | +| G-08 | **Build hygiene is genuinely strong.** SHA-pinned actions with version comments, `permissions: contents: read`, no `pull_request_target`, `persist-credentials: false`, hashed lockfiles, sha256-verified installer, full-history secret scanning with a fail-closed baseline, signed-commit policy. | strength | see `recon-report.md` G12 | + +--- + +## 6. Gold team — human factors, insider, trust (HF, AAA) + +**Charter.** The plan is only real if people follow it without being asked to do +anything extra. Gold's job is to veto controls that tax the user and to find the +insider paths. + +### 6.1 Findings + +| ID | Finding | Sev | Note | +| --- | --- | --- | --- | +| Y-01 | **The single-user assumption is invisible and routinely false.** Two analysts on one workstation, or a shared postdoc machine, is the normal lab case. Nothing tells either of them that they share full destructive authority. | H | `jobs.jl:7` | +| Y-02 | **Copy-and-send is the real dissemination path.** A publication table exported as XLSX and emailed is the TOP SECRET "verbal/hard-copy" failure in modern dress, and the export carries no marking, no copy number, no handling instruction. Marking is the single highest-leverage *human* control available, and it is free. | C for target | export routes | +| Y-03 | **No "who am I" affordance.** With no identity, an operator cannot tell whether an action will be attributed later. Attribution pressure is itself a control (AAA). | M | — | +| Y-04 | **Insider with legitimate access is indistinguishable from an attacker** — same routes, same authority, no anomaly baseline. | H | — | +| Y-05 | **Friction risk in the obvious fixes.** The instinctive responses (a confirm dialog on delete, a password per session, a classification dropdown on every export) all add recurring friction and would be ignored within a week — click-through blindness is a documented HF failure mode. | — | vetoed in `ultraplan.md` §2 | +| Y-06 | **The TOP SECRET "chair declares the classification at the start" behaviour has a software analogue that costs nothing**: a persistent classification banner in the UI and on every artefact. It does the chair's job without a prompt. | — | mapping to TS-01/TS-32 | + +### 6.2 Gold's veto list (do not build) + +- A confirmation dialog before deletion. (Substitute: two-phase delete with a + visible "recently deleted" list — costs zero clicks in the happy path, and the + recovery is *there* when it is needed.) +- A password or MFA prompt per session on a single-user localhost tool. (Substitute: + identity from the OS account or a proxy-asserted principal.) +- A classification dropdown on every export. (Substitute: inherit from the + study; the user sets it once.) +- A warning banner on every page. (Substitute: one banner, showing the truth.) +- Periodic re-authentication for a tool that is used in multi-hour sessions. + +### 6.3 Friction accounting for the whole ultraplan + +| Workstream | Added user actions | Added user decisions | +| --- | --- | --- | +| W1 hardening | 0 | 0 | +| W2 audit ledger | 0 | 0 | +| W3 identity | 0 (proxy/OS) | 0 | +| W4 classification | 0 | 1 per study, at creation (defaults to OFFICIAL) | +| W5 privacy | 0 | 1 per study, only if it declares personal data | +| W6 assurance | 0 | 0 | +| W7 testing | 0 | 0 | +| **Total** | **0** | **≤2, both once per study** | + +--- + +## 7. Where the teams disagree, and how the conflict resolves + +This section is the point of running the exercise. Four conflicts surfaced; each +one is resolved by the eliminate/substitute/notify rule rather than by +compromise. + +| Conflict | Red says | Blue says | Gold says | Resolution | +| --- | --- | --- | --- | --- | +| Deletion | "one request destroys a study" | "we would never know" | "no confirm dialogs" | **Two-phase delete** (SEC-08): attack needs two recorded requests; the ledger gives blue the event; the honest user sees the item vanish immediately, with a recovery window they never have to think about. | +| Sign-in | "no identity = no defence" | "no auth events = nothing to alert on" | "no passwords, no MFA prompts" | **Identity asserted by the local proxy or the OS account** (SEC-01): red gets an attributable principal, blue gets an event, gold gets zero prompts. A password database is explicitly rejected. | +| Classification | "unmarked exports are the leak" | "we cannot tell what left" | "no dropdowns" | **Inherited attribute + banner + manifest** (SEC-04): set once at study creation (default OFFICIAL), rendered by the server, never asked again. | +| Publication | "irreversible egress in one request" | "no record of disclosure" | "don't block my DOI workflow" | **Gate on the existing classification + record the approval** (SEC-05). Publication is already exceptional; adding a recorded approval costs nothing in routine use, and the route already refuses unless `DOI_ENABLED=true`. | + +--- + +## 8. Heat map + +| Area | Red | Blue | Green | Gold | White | Overall | +| --- | --- | --- | --- | --- | --- | --- | +| Identity & authorisation | C | C | M | H | C | **C** | +| Audit & forensics | — | C | — | — | H | **C** | +| Data at rest / in transit | H | M | C | M | C | **C** | +| Classification & dissemination | H | C | — | C | C | **C** | +| Input validation (paths, SQL, shell, R, XSS) | P | — | M | — | — | **P** | +| Integrity & provenance | M | strength | M | — | H | **M** | +| Availability & resource control | H | M | M | M | M | **H** | +| Build & supply chain | M | — | H | — | M | **H** | +| Governance & documentation | — | — | — | M | C | **C** | + +The shape of this map is the argument for the ultraplan's ordering: the code +surface is strong (one green row out of nine), and every red row is +architectural. That is a good problem to have — architectural controls are +additive and do not risk the scientific correctness that the rest of this +repository is built to protect. diff --git a/docs/security/recon-report.md b/docs/security/recon-report.md new file mode 100644 index 00000000..1787e6fe --- /dev/null +++ b/docs/security/recon-report.md @@ -0,0 +1,979 @@ + + + +# Security recon: GDPR / HIPAA baseline, TOP SECRET target + +Status: static review of `main` at `d6e8671` (`fix(governance): follow the +standards transfer …`). This is an evidence document and a gap analysis. It is +not a certification, an accreditation, or a penetration test, and nothing below +claims a control is "met" unless the evidence for it is on disk and cited. + +Companion documents: + +- [`ultraplan.md`](ultraplan.md) — the plan to close what this report finds. +- [`colour-teams.md`](colour-teams.md) — the CyBOK-referenced colour-team + exercise (white, red, blue, purple, green, gold). + +--- + +## 0. Scope, method, and what I could not do + +### 0.1 Scope + +The whole first-party surface: Julia server (`src/server/`), analysis and +pipeline (`src/analysis/`, `src/pipeline/`, `src/core/`), React/TypeScript +frontend (`frontend/src/`), configuration (`config/`), installers and CI +(`install.sh`, `start.sh`, `.github/workflows/`, `scripts/ci/`), and the +governance artefacts (`SECURITY.md`, `0-AI-MANIFEST.a2ml`, `.gitleaks.toml`, +`.machine_readable/`). + +Out of scope by nature: the physical building, the network perimeter, the +operating system, the DV-cleared people, and the cryptographic accreditation of +the platform. Those are *inherited* controls — see §5.3, which says explicitly +which TOP SECRET behaviours software can carry and which it can only depend on. + +### 0.2 Method + +1. Asset and trust-boundary reconstruction from the code that owns them: + `src/server/server.jl` (middleware, static serving, startup), + `src/server/routes/*.jl` (155 routes), `src/server/jobs.jl` (execution). +2. Control-by-control reading of every place where a request value reaches a + filesystem path, a shell, SQL, R, another host, or an outbound HTTP call. +3. Absence checks. For "there is no X" claims I grepped for X's vocabulary and + report the zero (or the only hits, which are unrelated). An absence claim is + only as good as the grep, so the greps are in §9 and can be re-run. +4. Mapping to three standards: GDPR (Reg. (EU) 2016/679), the HIPAA Security + Rule (45 CFR Part 164 Subpart C), and the UK Government Security + Classifications TOP SECRET tier (Tier 3) baseline behaviours supplied with + this brief. +5. Colour-team simulation against CyBOK v1.1/v1.2 knowledge areas. + +### 0.3 What I could not run, and therefore do not claim + +| Limitation | Consequence | +| --- | --- | +| No Julia, R, bun or DuckDB in the review environment | No test was executed; no runtime behaviour is claimed. Every finding is from source reading and is reproducible by resolution reference. | +| Clone is shallow (`git rev-parse --is-shallow-repository` → `true`, 1 commit) | No Git-history secret scan and no historical-integrity claim. Gitleaks runs full-history in CI (`.github/workflows/secret-scanner.yml`); I assert its *design*, not its *result*. | +| No network access to the running server | No live request was sent. Exploit chains in §7 are reasoned from source and are labelled as such until a test proves them. | +| No access to the author's deployment (OS, disk encryption, network) | All environment-level controls in §5.3 are marked **inherited / to be attested**, never assumed present. | + +--- + +## 1. Executive summary + +### 1.1 What this system is, in security terms + +MetaManifold-WebUI is a **single-operator, loopback-bound, unauthenticated +analysis appliance** that runs a shell- and R-driven bioinformatics pipeline, +stores results in DuckDB, and serves a browser UI that can start, cancel and +delete work. That is not a criticism: it is the design, stated plainly in the +code and in `SECURITY.md` ("Treat the Julia server as a trusted local analysis +service, not a public, multi-tenant hosting boundary"). `src/server/jobs.jl:7` +says it in one line: `# In-memory job store. Single-user local server - no +persistence needed.` + +The security question in this brief is therefore not "is the appliance +vulnerable" but **"what has to change before this appliance may hold data whose +compromise could kill people"**. Those are very different questions, and the +answer to the second is: a great deal, and almost none of it in the scientific +code. + +### 1.2 Verdicts + +| Target | Verdict at `d6e8671` | One sentence | +| --- | --- | --- | +| **GDPR baseline** | **Not met** for any multi-user, network-exposed, or human-derived-data deployment. Defensible only as a single-user tool on a whole-disk-encrypted, access-controlled workstation with no personal data beyond sample labels. | There is no way to answer "who accessed or deleted this subject's data", which is Art. 5(2)/30/32 in one sentence. | +| **HIPAA baseline** | **Not met.** Three *required* implementation specifications fail outright (unique user identification, person/entity authentication, audit controls), and the two *addressable* encryption specifications have no implemented equivalent and no documented risk decision. | §164.312(a)(2)(i), (b), (d) are required, not addressable, and there is no identity anywhere in the process. | +| **TOP SECRET (Tier 3)** | **Not reachable by the application alone, and not reachable as deployed today even inside a perfect environment.** The environment can supply the Secure Isolated Network, the accredited rooms, the DV-cleared staff and the couriers; the application must still supply marking, need-to-know, the protected-document-registry equivalent, the witnessed-destruction record, and the audit trail — and supplies none of them. | The application cannot label, cannot attribute, cannot erase-with-evidence, and cannot prove it was not tampered with. | + +### 1.3 What is genuinely good (do not rewrite it) + +This codebase is unusually security-literate for a research tool. Twelve +controls are already implemented *well*, several of them better than commercial +products: parameterised SQL with an explicit identifier guard, a shell-quoting +helper plus metacharacter gating plus remote-target validation, a strict AST +parser for R formulas that refuses arbitrary call expressions, realpath +containment on three separate file-serving paths, a CORS/Origin allowlist that +also defeats DNS rebinding and is unit-tested, SHA-256 verification of every +reference-database download, content-addressed analysis records that refuse +tampering, SHA-pinned GitHub Actions with least privilege, and a sha256-verified +installer. §3 lists all twelve with line references. + +The plan in `ultraplan.md` therefore **adds** layers. It does not touch the +scientific code, the numeric contracts, or the parts of the request pipeline +that this review found sound. + +### 1.4 The five findings that matter + +1. **No identity, therefore no accountability, therefore no need-to-know.** + Every route in `src/server/routes/` is unauthenticated and unauthorised. + (SEC-01) +2. **No audit trail and no access log.** `access_log=nothing` + (`src/server/server.jl:498`); destructive operations are recursive `rm` + (`src/server/routes/studies.jl:233`, `runs.jl:465`) with no tombstone and no + record of who asked. (SEC-02) +3. **No encryption at rest, anywhere** — zero occurrences of `encrypt`, + `cipher` or key-management vocabulary in `src/`. (SEC-03) +4. **No classification model**: the words *classification*, *TOP SECRET*, + *caveat*, *need-to-know* and *dissemination* appear nowhere in `src/`, + `config/` or `frontend/src/`, while the app's whole purpose is to export + artefacts. (SEC-04) +5. **One unauthenticated request can publish study material to the public + internet** — `POST /api/v1/studies/{study}/doi/publish` with `confirm=true` + (`src/server/routes/doi.jl:58-88`) pushes files to Zenodo with no + classification check. That is simultaneously a GDPR Art. 44 transfer and a + TOP SECRET dissemination breach. (SEC-05) + +--- + +## 2. System model + +### 2.1 Assets, in the order the classification tier cares about them + +| # | Asset | Where it lives | Sensitivity driver | +| --- | --- | --- | --- | +| A1 | Raw sequence data (`*.fastq.gz`) | `data/{study}/{run}/` | May be human-derived; may carry subject linkage through sample names/metadata | +| A2 | Sample metadata and category sets | `config/composition.yml`, DuckDB columns, CSV exports | Direct identifiers appear here if anywhere | +| A3 | Results and derived analyses | `projects/{study}/**`, `*.duckdb`, `derived/` records | Scientific value; integrity-critical | +| A4 | Configuration and primer/reference libraries | `config/` | Integrity-critical (changes outputs silently) | +| A5 | Provenance and attestation | `attestation.yml`, `pipeline.log`, config hashes | The only tamper evidence that exists | +| A6 | Publication artefacts | `projects/{study}/report/`, figure rasters, exported ZIPs | The artefacts that leave the boundary | +| A7 | Externals: reference databases, Zenodo | downloads cache, Zenodo API | Supply chain and egress | +| A8 | Credentials: `ZENODO_TOKEN` | environment only | Secret | + +### 2.2 Trust boundary as deployed + +``` +browser (operator) ──HTTP, no TLS──► Julia/Oxygen ──bash -lc──► tools (cutadapt, vsearch, …) + │ ──RCall───► R (DADA2) + │ ──ssh─────► remote compute host + │ ──HTTPS───► GitHub release assets, Zenodo + └─ no auth, no session, no audit, no rate limit, no at-rest crypto +``` + +The boundary is the **host**, not the process. Everything inside the host — +any other local user, any other process, any page the operator's browser has +open — is inside the boundary as the code currently stands. `SECURITY.md` +acknowledges this and correctly forbids direct internet exposure; what it does +not do is state the equivalent requirement for a *shared* host, which is where +the practical risk sits in a lab. + +### 2.3 Actors + +| Actor | Capability today | Capability it must have at TOP SECRET | +| --- | --- | --- | +| Operator (analyst) | full read/write/delete/execute | read/write *their* studies only | +| Second analyst on the same host | identical to the first, including delete | no access without an explicit grant | +| Local unprivileged user | reads `data/` and `projects/` via the filesystem if modes permit (no `chmod`/`umask` call exists in `src/`) | no access | +| Anyone who can reach the port | full API | nothing | +| A web page in the operator's browser | can frame the UI (no `frame-ancestors` on the SPA route) | cannot frame; cannot read | +| Remote compute host | receives complete inputs over ssh | inside the accredited boundary | +| Zenodo | receives whatever the operator selects | never, without a per-document determination | + +--- + +## 3. Controls that already exist and are sound + +Each row cites the code that implements it. These are the things the plan must +*not* disturb, and several are reusable primitives for the upgrade. + +| # | Control | Evidence | +| --- | --- | --- | +| G1 | Entity-name validation (`[A-Za-z0-9._-]`, no leading dot, anchored with `\z`) applied before any name reaches a path, plus a URL-decoded path-segment guard that rejects `.`, `..`, `/`, `\`, `"`, NUL | `src/core/validate.jl:21`, `src/server/routes/studies.jl:148-150`, `src/server/server.jl:190-216` | +| G2 | `realpath` containment on every file-serving path: run files, DOI report files, the SPA root | `src/server/server.jl:404-410`, `src/server/routes/doi.jl:31-45`, `src/server/server.jl:429-434` | +| G3 | Origin allowlist + `Host` check on loopback binds + a wildcard-bind rule that admits only an `http://` origin whose IP literal equals the request `Host` (so a DNS-rebinding domain is refused). Unit-tested, including the refusal cases | `src/server/server.jl:299-380`, `test/unit/test_server_security.jl:44-130` | +| G4 | Oxygen's `/docs`, `/docs/schema` and `/metrics` disabled (they sit outside the middleware chain); `show_errors=false`; no stack trace reaches a client | `src/server/server.jl:498`, `src/server/server.jl:73-91` | +| G5 | Error bodies built through the JCS/I-JSON writer with a fallback, so a hostile path segment cannot turn a 400 into a 500 | `src/server/server.jl:73-91` | +| G6 | SQL: every request value is a bind parameter; identifiers are quoted and gated by an explicit `is_safe_name` check; category CASE expressions escape string literals | `src/server/routes/duckdb_helpers.jl:145-215`, `src/core/categories.jl:355-364`, `MetaManifold.Categories._sql_str` | +| G7 | Shell: single-quote helper, metacharacter allowlist for `optional_args`, and remote target validation (no leading `-`, no spaces, absolute staging dir) before anything becomes an ssh word | `src/pipeline/tools.jl:19,86-94`, `src/core/validate.jl:50-68`, `src/pipeline/remote_exec.jl:19-48` | +| G8 | R: ordination formulas parsed as a strict identifier/interaction AST and refused if they are call expressions — this closes the `as.formula` arbitrary-evaluation hole explicitly | `src/analysis/ordination.jl:216-260` | +| G9 | Uploads: per-route byte caps, PNG magic-byte check, UUID/slug file names, and uploaded bytes served with `Content-Security-Policy: default-src 'none'; …; sandbox` + `nosniff` | `src/server/routes/report.jl:11,57-86,140-141`, `src/server/routes/figures.jl:13-14,80-86`, `src/server/server.jl:412-419` | +| G10 | Integrity: SHA-256 and size verification of every reference database before and after download, with a partial-download pattern and a cleanup `finally`; content-addressed derived analyses that refuse a record whose id does not match its content; tree files served with their sha256 | `src/core/databases.jl:114-160,292-345`, `src/analysis/derived_analyses.jl:69`, `src/server/routes/trees.jl:95` | +| G11 | Secrets: the Zenodo token is server-side only, never in the frontend, and is redacted from any text written to a receipt or log | `src/core/doi.jl:93-95,112-120`, `src/server/routes/doi.jl:108-111` | +| G12 | Supply chain and CI: every action SHA-pinned with the version in a comment; top-level `permissions: contents: read`; no `pull_request_target` anywhere; full-history gitleaks with an estate baseline that fails closed if the baseline cannot be fetched; Scorecard; a sha256-verified, `--proto '=https'` installer; `persist-credentials: false`; signed-commit requirement documented | `.github/workflows/*.yml`, `.github/workflows/secret-scanner-reusable.yml:237-310`, `install.sh:167-190`, `notes/commit-signing.md` | + +Two latent weaknesses inside otherwise sound controls are recorded as SEC-12 +and SEC-13 below rather than as strengths. + +--- + +## 4. Findings + +Severity is judged against the *target tier*, not against a hobby deployment: +a finding that is Low for a laptop is High for a TOP SECRET candidate. + +Each finding carries a **disposition** in the vocabulary this brief asks for: + +- **E — Eliminate**: remove the exposure (the capability, the data, or the reach). +- **S — Substitute**: replace the risky mechanism with a safer one that costs the + user nothing. +- **N — Notify**: detect, record and disclose — default-safe, never a prompt. + +and a **friction cost**: `0` = invisible, `1` = one-off setup, `2` = recurs per +task. Nothing at `2` is proposed anywhere in this dossier. + +--- + +### SEC-01 — No authentication, no authorisation, no session (Critical) + +**Evidence.** +The middleware chain is five functions, none of which establishes an identity: +`src/server/server.jl:494-495` — +`[_bad_request_middleware, _r_busy_middleware, _cors_middleware, _analysis_source_middleware, _file_middleware]`. +The only `Authorization` strings in `src/` are Zenodo *client* code +(`src/core/doi.jl:112,119,182`). There is no session store, no token check, no +user table, no peer-credential check. `src/server/jobs.jl:7` states the design +assumption. `SECURITY.md` places the whole burden on the operator: "Treat the +Julia server as a trusted local analysis service… Remote deployments need an +authenticated, TLS-terminating reverse proxy." + +**Impact.** All 155 routes are open to anything that can reach the port. That +includes destructive routes (`DELETE /api/v1/studies/{study}`, +`…/runs/{run}`, `…/results/tables/{table}`, `/api/v1/jobs/{id}`), execution +routes (`POST …/pipeline`, `POST …/stages/{stage}`), configuration routes +(`PATCH`/`DELETE` on global, study, group and run config), and the irreversible +publication route. There is no distinction between read and write, no +need-to-know, and no attributable actor for any of them. + +**Standards.** GDPR Art. 32(1)(b),(d), Art. 5(1)(f), Art. 25; HIPAA +§164.312(a)(1) access control, §164.312(a)(2)(i) **unique user identification +(Required)**, §164.312(d) **person or entity authentication (Required)**, +§164.502(b) minimum necessary; TOP SECRET §5 and §7 (strict need-to-know, +recipients briefed and traceable). + +**Disposition.** E for reach (bind loopback only — already the default at +`src/server/server.jl:507` — and refuse to start on a non-loopback bind without +an explicit acknowledgement *and* a terminating proxy), then S for identity +(accept a verified identity from a mutually-authenticated local proxy or the OS +account, and record it; do not build a password database). Friction: 0 for the +incumbent single user, 1 for a team. + +--- + +### SEC-02 — No audit trail, no access log, no attributable deletion (Critical) + +**Evidence.** `const _SERVE_OPTIONS = (access_log=nothing, …)` +(`src/server/server.jl:498`) — no request log at all. Deletion is +`rm(path; recursive=true, force=true)` with no record and no tombstone: +`src/server/routes/studies.jl:233-241`, `src/server/routes/runs.jl:465`, +`src/server/routes/results.jl:540`, `src/server/routes/trees.jl:140-148`, +`src/server/routes/figures.jl:98-103`, `src/server/routes/config.jl:631,896,981,1030`. +`log_command` and `log_written` record *what the pipeline did to what file* +(`src/core/log.jl:27-33`, `src/pipeline/tools.jl:45-52`) but nothing records +*who asked for it* or *who read it*. + +**Impact.** No accountability and no forensics. After deletion there is no +evidence of what was deleted, by whom, when, or whether it succeeded — which is +simultaneously a GDPR erasure-evidence problem, a HIPAA audit-control failure, +and a direct contradiction of the TOP SECRET rule that assets may not be +destroyed without written approval and a DV-cleared witness. + +**Standards.** GDPR Art. 5(2), Art. 30, Art. 32, Art. 33 (you cannot assess a +breach you cannot reconstruct); HIPAA §164.312(b) **audit controls (Required)**, +§164.308(a)(6) security incident procedures, §164.316(b)(2) six-year retention; +TOP SECRET §5 (destruction with a witness, recorded in the Protected Document +Registry book, destruction and witness both recorded). + +**Disposition.** S — an append-only, hash-chained ledger written by the server +for a fixed vocabulary of events (access, export, delete, config write, publish, +run start/stop, auth decision). The primitives already exist: canonical JSON +(`src/canonical.jl`), `sha256` (`src/core/log.jl:27-33`), atomic publication +(`src/core/derived_analyses.jl`). Friction: 0 — the ledger is written by +`_bad_request_middleware`'s sibling, not by the user. + +--- + +### SEC-03 — No encryption at rest, and no key management (Critical for the target) + +**Evidence.** Grep for `encrypt|cipher` across `src/` returns zero. No `chmod`, +`chown` or `umask` call exists either, so directories created through `mkpath` +(`src/server/routes/studies.jl:201`, `src/server/routes/report.jl:81`, +`src/server/routes/figures.jl`, `src/core/config.jl`) take the process umask — +typically `0755`, i.e. world-readable on a shared host. DuckDB stores, FASTQ, +report uploads, `attestation.yml` and the logs are all plaintext files. + +**Impact.** Any local user, backup system, snapshot, sync client or stolen disk +yields the whole dataset. There is no equivalent control and no documented risk +decision, which is what HIPAA's *addressable* standard requires. + +**Standards.** GDPR Art. 32(1)(a) (encryption as an appropriate technical +measure), Art. 32(1)(b); HIPAA §164.312(a)(2)(iv) **encryption and decryption +(Addressable — implement or document the equivalent)**, §164.312(e)(2)(ii) +transmission encryption, §164.310(d)(2)(i) disposal; TOP SECRET storage and +transport rules throughout §5. + +**Disposition.** S — two substitutions that cost the user nothing: +(a) **whole-tree encryption at the filesystem layer** (LUKS / FileVault / +BitLocker or an equivalent accredited FDE) as the baseline, documented as an +inherited control, plus `0700`/`0600` modes set at creation so the data is not +world-readable even inside the encrypted volume; (b) **envelope encryption for +anything that leaves the tree** — exports, backups and the attestation bundle — +using an age/X25519 recipient list or the platform keychain. Do **not** invent a +custom at-rest scheme. Friction: 0 (FDE is one-off), 1 for the export key. + +--- + +### SEC-04 — No classification model: no marking, no caveats, no registry, no copy control (Critical for the target) + +**Evidence.** `classification`, `top secret`, `caveat`, `need-to-know`, +`dissemination` and `handling` do not occur in `src/`, `config/` or +`frontend/src/` (the only hits repo-wide are in unrelated prose). Export paths +that produce material a human could leak: `GET …/report/export` (ZIP), +`POST …/results/tables/{table}/export` (XLSX), `POST …/analysis/publication-tables`, +figure rasters, tree files, `attestation.yml`, `combined_pipeline.log`. + +**Impact.** Every artefact this tool emits is **unmarked**. That is the single +sharpest mismatch with the target tier: the TOP SECRET baseline is, at bottom, a +labelling-and-registry discipline (§5 — header/footer marking, copy numbers, +Protected Document Registry, handling instructions in the subject line, UK +prefix for international partners). A file that leaves this tool carries no +indication of what it is, so the control collapses to the user's memory. + +It also means an export cannot be *scoped*: nothing prevents a report containing +one study's material from being combined with another's. + +**Standards.** TOP SECRET §5 (all marking, registry, copy-number and +dissemination behaviours); GDPR Art. 5(1)(b) purpose limitation and Art. 44 +transfers (an unmarked export has no record of what it may be sent to); HIPAA +§164.502(b) minimum necessary and §164.528 accounting of disclosures. + +**Disposition.** S — a first-class `classification` attribute on study, group and +run, inherited downward, defaulting to `OFFICIAL`, enforced at **one** choke +point on every export path, rendered as a banner in the UI and in generated +HTML/CSV/XLSX/PDF, with a copy number and a dissemination list carried in the +export manifest. Friction: 0 — set once when a study is created; users never see +a prompt, they see a banner. + +--- + +### SEC-05 — Public internet publication of study material with one request (High) + +**Evidence.** `POST /api/v1/studies/{study}/doi/publish` +(`src/server/routes/doi.jl:58-88`) requires only `confirm: true` in the body, +then resolves report files and calls `publish_study`, which POSTs to +`DOI_ORIGIN` (default: the Zenodo *sandbox*) with the bearer token +(`src/core/doi.jl:153-190`). There is no check that the study's classification +permits external dissemination, no allowlist of files, and no approval record. + +**Impact.** Irreversible publication to a third country platform. This is the +one route in the system whose effect cannot be undone by a restore, and it is +reachable by anything that can reach the port (SEC-01) or by a clickjack +(SEC-11). + +**Standards.** GDPR Art. 44-46 (transfer outside the UK/EU without a mechanism), +Art. 5(1)(b) (a research purpose does not authorise public release), Art. 32; +TOP SECRET — dissemination outside the Secure Isolated Network is prohibited +outright; HIPAA §164.528 accounting of disclosures. + +**Disposition.** E for the default (the route already refuses unless +`DOI_ENABLED=true` — keep and strengthen that), then S: require the study's +classification to be `OFFICIAL` and require an explicit, recorded approval entity +before publication, and write the publication to the audit ledger with the file +hashes. Friction: 0 in normal use (publication is already an exceptional act). + +--- + +### SEC-06 — Unauthenticated rewrite of the reference-database configuration (High) + +**Evidence.** `PUT /api/v1/databases` (`src/server/routes/databases.jl:129-140`) +replaces the whole `databases.yml` document, validated only for shape; +mismatches are *advisory* warnings (`_database_warnings`). The `uri:` field has +no scheme check and the `sha256:` field is optional; `local:` accepts any +absolute path. `POST /api/v1/databases/{key}/download` (`databases.jl:176-187`) +then fetches that URI into the cache, and `_resolve_entry` +(`src/core/databases.jl:292-345`) accepts a file that verifies only when a hash +is configured — the "hand-placed file gets the same scrutiny" comment is true +only if `sha256` is present. + +**Impact.** An attacker (or an accidental edit) can point classification at an +arbitrary remote or local file. Downstream, taxonomy results are wrong in a way +that looks completely normal — the worst failure mode in a scientific tool; and +the download is an outbound GET to an attacker-chosen URL from inside the +accredited network. + +**Standards.** Integrity of processing — GDPR Art. 5(1)(d),(f), Art. 32(1)(b); +HIPAA §164.312(c)(1) integrity; TOP SECRET — data-import controls and no +unvetted egress from the Secure Isolated Network. + +**Disposition.** S — require `https` scheme, require `sha256`, refuse a `uri` +change unless it carries a hash, and make "no hash" a refusal rather than a +warning for pipeline use. Friction: 0. + +--- + +### SEC-07 — No global request-size limit; unbounded job submission (High) + +**Evidence.** Per-route caps exist (report 50 MB, figures 2 MB/200 MB, FASTA +100 MB, trees 50 MB, statistics 16 MB) but the shared body readers +(`_read_json_object` at `src/server/server.jl:96-118`, `_request_json` at +`src/server/server.jl:171-178`) have no cap, and the HTTP layer is not +configured with a maximum. There is no rate limiting anywhere in `src/` (grep: +zero). `JobQueue` prunes finished jobs (`src/server/jobs.jl:35`) but does not cap +*running* jobs, and a pipeline job holds the embedded R interpreter +(`src/core/r_runtime.jl`), so a handful of requests serialise into hours of +exclusive lock. The code already knows: `src/analysis/numeric_policy.jl:27-38` +— "Configuration limits are deliberately finite. They can be changed in source +after review, not raised per request until an unauthenticated request can OOM +us." + +**Impact.** Memory exhaustion and denial of service by anything that reaches the +port. For a TOP SECRET appliance, availability is a security property. + +**Standards.** GDPR Art. 32(1)(b) (availability); HIPAA §164.308(a)(7) +contingency plan, §164.312(c); TOP SECRET — availability of the processing +environment. + +**Disposition.** E for the unbounded submission (cap queued+running jobs, reject +with 429), S for the body cap (one middleware, one number, applied before any +parser runs). Friction: 0 — no legitimate request is anywhere near the limits. + +--- + +### SEC-08 — Destructive operations are unconditional and irreversible (High) + +**Evidence.** Study, run, table, tree, figure, preset, category-set and job +deletion routes (inventory in §8.1) act immediately, with no confirmation token, +no soft delete, no undo and no notification. `POST /api/v1/studies/{study}/rename` +and the group rename move directories (`_safe_move`, `studies.jl:159-163`). +The job cancel route accepts any id (`src/server/routes/jobs.jl:29`). + +**Impact.** One mistyped request or one clickjack deletes a study's raw data and +results permanently, with no record. This is the failure that a TOP SECRET +environment treats as a reportable incident rather than a mistake. + +**Standards.** GDPR Art. 5(1)(e),(f), Art. 17 (erasure must be effective *and* +evidenced); HIPAA §164.312(c)(1) integrity, §164.308(a)(6); +TOP SECRET §5 — no destruction without written approval from the information +creator, and destruction recorded and witnessed. + +**Disposition.** S — two-phase deletion: a `DELETE` marks and moves to a +quarantine area inside the tree and writes the ledger entry; a separate, +privileged, recorded `purge` removes it after a retention window. Friction for +the honest user: 0 (the item disappears from the UI immediately, exactly as +now). For the attacker: they now need two requests and they are both recorded. + +--- + +### SEC-09 — No transport security in the server (High whenever it is not loopback) + +**Evidence.** `start(; host="127.0.0.1", port=8080)` (`src/server/server.jl:500`) +calls `serve(; host, port, …)` with no certificate option; `grep -E "TLS|SSL|certfile"` +over `src/` returns nothing. `README.md:912` instructs `http://localhost:8080`. +`JULIA_METAMANIFOLD_ALLOWED_ORIGINS` accepts `https://` origins for a +split deployment while the API itself remains plaintext. + +**Impact.** On loopback this is fine and honest. Any non-loopback bind — the +documented split deployment, or a shared lab box — sends study names, filters, +results and any metadata in clear text, and lets a network attacker inject into +the pipeline. + +**Standards.** GDPR Art. 32(1)(a) (encryption in transit); HIPAA +§164.312(e)(1) **transmission security (Required)**, §164.312(e)(2)(ii); +TOP SECRET — Secure Isolated Network, no unaccredited bearer. + +**Disposition.** E for reach (already loopback by default; make a non-loopback +bind without a TLS terminator a **startup refusal**, not a warning), S for the +encrypted path (support `:ssl` on the same `serve` call, or document the proxy as +a hard prerequisite and verify the `X-Forwarded-Proto`). Friction: 0. + +--- + +### SEC-10 — Security headers are present on one route and absent on the rest (Medium) + +**Evidence.** `src/server/routes/statistics_ui.jl:187-194` sets +`nosniff`, `Referrer-Policy: no-referrer` and a CSP **including +`frame-ancestors 'none'`** — exemplary. The SPA and static-asset responses set +only `Content-Type` and `Cache-Control` (`src/server/server.jl:440-447`), and +`/files` adds `nosniff` alone (`server.jl:416`). There is no +`X-Frame-Options`, no CSP, no `Referrer-Policy`, no `Permissions-Policy` and no +`Cross-Origin-*` isolation anywhere outside that one route. + +**Impact.** The UI can be framed by any site (see SEC-11), referrer URLs leak +study and run names to third parties, and the frontend has no +defence-in-depth against an injected script (there is currently no +`dangerouslySetInnerHTML` in `frontend/src` — verified — so this is preventive, +not an active XSS). + +**Standards.** GDPR Art. 32(1)(b) confidentiality; TOP SECRET — protection +against the "extremely high capability" threat actor includes not leaking +metadata in a `Referer` header. + +**Disposition.** S — reuse the exact header set from `statistics_ui.jl` in one +response-side helper and call it from `_file_middleware` for every response. +Friction: 0. This is the highest value-per-line change in the whole dossier. + +--- + +### SEC-11 — Clickjacking of an unauthenticated destructive UI (Medium; High in combination) + +**Evidence.** No `frame-ancestors` / `X-Frame-Options` on the SPA +(SEC-10), deletion has no confirmation and no undo (SEC-08), and study deletion +is a single `DELETE` (`studies.jl:233`). Because the framed document *is* the +app, a clickjacked click arrives as a same-origin request that carries +`Origin: http://localhost:8080` — identical to a legitimate one — so the +Origin allowlist (G3), which correctly stops cross-origin scripting, is +structurally unable to stop UI redressing. + +**Impact.** A page the operator visits in the same browser can position an +invisible frame over a plausible button and delete a study or start a pipeline +run. No credentials are needed, so `SameSite` cookies would not help even if a +session existed. + +**Standards.** Same as SEC-10 plus TOP SECRET §5 (unauthorised removal or +destruction of assets). + +**Disposition.** E — `frame-ancestors 'none'` (SEC-10) removes the attack +outright, which is why SEC-10 and SEC-11 ship together and first. + +--- + +### SEC-12 — Identifier validation of SQL table/column names is implicit, not explicit (Medium, latent) + +**Evidence.** `{table}` and `{column}` path params are interpolated into quoted +SQL (`src/server/routes/duckdb_helpers.jl:26-27,51,273-281,318-322`, +`src/server/routes/results.jl:404`). Today the injection is blocked *by +consequence*, not by validation: `_duckdb_columns` is parameterised, so a bogus +table yields `[]`, and callers return 404 before the interpolated statement runs +(`duckdb_helpers.jl:261-270,313-315`; `results.jl:449-451`). A future refactor +that reorders a check re-opens it. Identifier quoting is not escaping: a `"` +inside `table` would break out. + +**Impact.** No current exploit path found — this is a defence-in-depth gap and a +regression trap. + +**Standards.** GDPR Art. 32; HIPAA §164.312(c)(1); TOP SECRET — integrity of +stored data. + +**Disposition.** S — validate `table`/`column` with `Validation.is_safe_name` +(or a dedicated identifier predicate, since real column names contain spaces and +slashes) at the route boundary, and add a unit test that a `"`-bearing table +name returns 400. Friction: 0. + +--- + +### SEC-13 — Provenance artefacts record paths, hostnames and command lines verbatim (Medium, GDPR data minimisation) + +**Evidence.** `src/core/provenance.jl:799-801` states it directly: "Paths and +command lines are recorded verbatim, and may carry a username or a study name. +The Attestation is a publishable artefact, so check it before publishing." +`probe_host()` records `gethostname()` (`provenance.jl:526-533`); `pipeline.log` +embeds whole tool logs (`src/core/log.jl`'s `_TOOL_LOG_FILES` registry and +`finalise_log`), which contain input paths and sample identifiers. + +**Impact.** The artefacts built to make the science reproducible are also the +most likely vehicle for incidental personal data (usernames, hostnames, sample +identifiers, directory names containing a patient code) to travel to a journal, +a reviewer or a preprint server. + +**Standards.** GDPR Art. 5(1)(c) minimisation, Art. 25, Art. 32; HIPAA +§164.502(b) minimum necessary; TOP SECRET — marking and dissemination control of +derived artefacts. + +**Disposition.** N then S — (a) at export time, run a **path/identifier +scrubber** that rewrites absolute paths to ``-relative and applies the +study's redaction list, and (b) *tell the user nothing*: the export simply no +longer contains `C:\Users\jsmith\…`. If a value cannot be safely rewritten the +export is refused with a clear reason, which is a `role="alert"` on an existing +error surface, not a new step. Friction: 0. + +--- + +### SEC-14 — Personal data has no first-class state: no flags, no retention, no rights machinery (High for GDPR) + +**Evidence.** Nothing in `src/` records whether a study contains personal data, +a lawful basis, a retention period, a purpose, or a subject list. Sample +identifiers and metadata are free text (`config/composition.yml`, category sets, +CSV column names). Erasure is `rm` (SEC-08). There is a study export (the report +ZIP) but it is a *manuscript* export, not a subject access export, and it carries +no index of what was disclosed. + +**Impact.** For any dataset containing identifiable human data (a host-association +study with a site/participant code is enough), the controller cannot answer a +DSAR, cannot demonstrate minimisation, cannot evidence erasure, cannot bound +retention, and cannot produce a record of processing. + +**Standards.** GDPR Art. 5(1)(c),(e), Art. 6, Art. 9 (if health data), Art. 12-22 +rights, Art. 24, Art. 25, Art. 30, Art. 35 (DPIA is almost certainly required for +large-scale health-adjacent processing); HIPAA §164.502(b), §164.528. + +**Disposition.** E first (do not accept direct identifiers: detect +identifier-shaped columns and header patterns at ingest and refuse or flag them — +the cheapest possible minimisation), then S (retention policy in config applied +by a maintenance job, and an audited erasure procedure), then N (a +`personal_data: true` flag that turns on the stricter export scrubber and the +retention clock). Friction: 0 for a study without personal data; 1 for a study +that declares it has some. + +--- + +### SEC-15 — No rate limiting, and the SSE stream is unbounded (Medium) + +**Evidence.** `@stream "/api/v1/events"` (`src/server/routes/events.jl:10`) adds +each client to a `Vector{Channel{String}}` under a lock +(`src/server/jobs.jl:38-39`); nothing caps the number of subscribers or +disconnects an idle one. No throttling exists on any route. + +**Impact.** Trivial resource exhaustion; a monitoring gap in its own right. + +**Disposition.** S — cap subscribers, drop the oldest idle, and add a single +token-bucket for mutating routes. Friction: 0. + +--- + +### SEC-16 — Frontend persists study-derived UI state in browser storage (Low) + +**Evidence.** `frontend/src/components/DataTable.tsx:85-92` writes column +visibility, sort and filters to `sessionStorage` under `dt:{key}`. There is no +logout, no session expiry and nothing clears it (`grep -rn logout frontend/src` +→ zero). + +**Impact.** On a shared workstation, column names and filter values (which are +data values) persist for the tab's lifetime and may be recoverable afterwards. + +**Disposition.** S — clear on unload and on any identity change (which arrives +with SEC-01). Friction: 0. + +--- + +### SEC-17 — Documentation and policy drift versus the target tier (Medium) + +**Evidence.** `SECURITY.md` is good and honest but describes a local tool; +`0-AI-MANIFEST.a2ml` lists an `invariants` set that includes "Do not put secrets +or private analysis data in Git" and a governance workflow set, but there is no +threat model, no data-flow map, no DPIA, no incident-response procedure, no +vulnerability disclosure SLA, no retention statement, and no classification +policy. `docs/` has no `security/` directory before this change. + +**Impact.** Accreditation cannot begin from an undocumented system; a security +reviewer will ask for exactly these artefacts and there is nothing to read. + +**Disposition.** N (publish) — this dossier is the start; the ultraplan adds the +threat model, DPIA skeleton, incident procedure and classification policy. + +--- + +## 5. Standards assessment + +### 5.1 GDPR (Reg. (EU) 2016/679) — article by article + +| Clause | Requirement | Current state | Finding | +| --- | --- | --- | --- | +| Art. 5(1)(a)-(b) | Lawfulness, purpose limitation | No purpose or basis recorded anywhere | SEC-14 | +| Art. 5(1)(c) | Minimisation | Free-text metadata; path/username leakage on export | SEC-13, SEC-14 | +| Art. 5(1)(d) | Accuracy | Content-addressed results, config hashes — **good** (G10) | — | +| Art. 5(1)(e) | Storage limitation | No retention concept | SEC-14 | +| Art. 5(1)(f) | Integrity and confidentiality | No at-rest or in-transit crypto, no access control, world-readable dirs | SEC-01, 03, 09 | +| Art. 5(2) | Accountability | No audit trail at all | SEC-02 | +| Art. 24/25 | Controller responsibility, data protection by design | Not documented; defaults are permissive | SEC-17 | +| Art. 28 | Processor contract | Zenodo is a joint/third-party recipient with no recorded basis | SEC-05 | +| Art. 30 | Records of processing | Impossible to produce | SEC-02, SEC-17 | +| Art. 32(1)(a) | Encryption / pseudonymisation | None implemented | SEC-03, SEC-09 | +| Art. 32(1)(b) | Confidentiality, integrity, availability, resilience | Availability unbounded (SEC-07, 15); integrity good for results (G10), absent for access | SEC-07, SEC-15 | +| Art. 32(1)(c) | Restore after incident | No backup/restore path documented | ultraplan W8 | +| Art. 32(1)(d) | Regular testing | CI runs unit tests and gitleaks; no security testing of the HTTP surface beyond `test_server_security.jl` | ultraplan W7 | +| Art. 33/34 | Breach notification, 72 h | No incident procedure, no logging to investigate with | SEC-02, SEC-17 | +| Art. 35 | DPIA | Absent; likely required for health-adjacent data | SEC-14, SEC-17 | +| Art. 15-22 | Subject rights | No DSAR path, no evidenced erasure | SEC-08, SEC-14 | +| Art. 44-46 | Transfers | Zenodo publish is an uncontrolled transfer | SEC-05 | + +### 5.2 HIPAA Security Rule — implementation specification by specification + +| 45 CFR | Spec | Type | Current state | Finding | +| --- | --- | --- | --- | --- | +| §164.308(a)(1)(ii)(A) | Risk analysis | Required | This document is the first one | SEC-17 | +| §164.308(a)(1)(ii)(B) | Risk management | Required | `ultraplan.md` | — | +| §164.308(a)(3) | Workforce security / sanction | Required | No workforce model exists | SEC-01 | +| §164.308(a)(4) | Information access management | Required | No authorisation | SEC-01 | +| §164.308(a)(5) | Security awareness / login monitoring | Required | No login exists | SEC-01 | +| §164.308(a)(6) | Security incident procedures | Required | Absent | SEC-02, SEC-17 | +| §164.308(a)(7) | Contingency plan | Required | No backup/restore/DR documentation | ultraplan W8 | +| §164.310(c) | Workstation security | Required | Inherited from the OS; undocumented | §5.3 | +| §164.310(d)(2)(i) | Disposal | Required | `rm` with no record | SEC-08 | +| §164.312(a)(1) | Access control | Required | **Fail** | SEC-01 | +| §164.312(a)(2)(i) | Unique user identification | **Required** | **Fail** — no identity exists | SEC-01 | +| §164.312(a)(2)(ii) | Emergency access | Required | N/A today (no access control) | — | +| §164.312(a)(2)(iii) | Automatic logoff | Addressable | No session to expire | SEC-01, SEC-16 | +| §164.312(a)(2)(iv) | Encryption at rest | Addressable | **Not implemented, not documented** | SEC-03 | +| §164.312(b) | Audit controls | **Required** | **Fail** — no access log | SEC-02 | +| §164.312(c)(1) | Integrity | Required | Good for results (G10); no baseline for config/data trees | SEC-06, SEC-12 | +| §164.312(c)(2) | Authentication of ePHI | Addressable | SHA-256 exists at artefact level (G10) — partial, undocumented | — | +| §164.312(d) | Person or entity authentication | **Required** | **Fail** | SEC-01 | +| §164.312(e)(1) | Transmission security | Required | **Fail** off loopback | SEC-09 | +| §164.312(e)(2)(ii) | Encryption in transit | Addressable | Not implemented, not documented | SEC-09 | +| §164.316(b)(2) | Six-year documentation retention | Required | Undocumented | SEC-17 | + +Note on "addressable": HIPAA does not permit skipping an addressable +specification; it requires either the implementation or a documented equivalent +with the reasoning. Today there is neither for the two encryption specs. + +### 5.3 TOP SECRET (Tier 3) baseline behaviours + +Twenty-eight behaviours from the brief. The middle column classifies each as +something the **application** must do, something the **environment** must +provide and the app merely must not defeat (**inherited**), or something that has +**no software expression**. Only the first class is scored here. + +**Verbal information** + +| # | Behaviour | Class | Current state | Finding | +| --- | --- | --- | --- | --- | +| TS-01 | Chair declares the classification before a meeting; all attendees cleared | No software expression | — | — | +| TS-02 | Discuss only on IT/telephony approved for TOP SECRET | Inherited | App has no concept of an approved platform; it will happily run on an unaccredited one, unbounded, over plain HTTP | SEC-09 | +| TS-03 | Accredited meeting rooms, NACE guidance, TSCM sweeps | Inherited | — | — | +| TS-04 | Approved headphones where possible | Inherited | — | — | +| TS-05 | Personal/corporate devices, wearables and smart speakers prohibited in TOP SECRET areas | Inherited | Browser app is used on whatever device; nothing detects or records it | SEC-16 | +| TS-06 | Brief-back only to cleared staff, in a suitably secure area, per need-to-know | Application (enforcement) + inherited (area) | No need-to-know model exists; every route is open | SEC-01 | +| TS-07 | No discussion in public | Inherited / human | Nothing in the app encourages or prevents it | — | + +**Hard copy** + +| # | Behaviour | Class | Current state | Finding | +| --- | --- | --- | --- | --- | +| TS-08 | No unattended material; NPSA-approved physical storage | Inherited | — | — | +| TS-09 | Print only on approved devices; consider yellow paper | Inherited | The app generates the printable artefact but marks it with nothing | SEC-04 | +| TS-10 | Printed documents tethered, with a copy number on page 1 | Application (generation) | Generated HTML/PDF/XLSX/CSV carry no copy number and no marking | SEC-04 | +| TS-11 | Register in the Protected Document Registry with a reference number | Application (generation) + inherited (the book) | No registry, no reference number, no document identity | SEC-02, SEC-04 | +| TS-12 | No remote working from home at TOP SECRET | Inherited (network) | — | — | +| TS-13 | Mark "TOP SECRET" in header and footer; "UK" prefix for international partners | Application | No marking anywhere; no partner concept | SEC-04 | +| TS-14 | Mark a pack with its highest classification | Application | No classification attribute to aggregate | SEC-04 | +| TS-15 | Avoid handwritten notes; treat them as documents | Human | — | — | +| TS-16 | Never take material home | Inherited | — | — | +| TS-17 | Prefer secure printing at the destination | Inherited | — | — | +| TS-18 | Written rationale + TVRA + SCS1/SSA approval before moving documents; limited knowledge; registry checkout | Application (record) + inherited (approval) | No movement record, no approval record, no checkout | SEC-02 | +| TS-19 | Hand carriage: DV minimum, two DV escorts, double opaque tamper-evident packaging, marking inside only, return address, delivery receipt, locked discreet container | Inherited (physical) | — | — | +| TS-20 | No commercial courier domestically; approved secure mail with two DV escorts | Inherited | — | — | +| TS-21 | Overseas movement: creator + SSA/SA + SCS1 approval; diplomatic bag or military courier | Inherited + application (record) | No transfer record; and the app offers a *worse* channel — one-click public publication (SEC-05) | SEC-05 | +| TS-22 | Destruction: no destruction without written approval from the creator; return to creator first | Application | Deletion is unconditional, unapproved and unrecorded | SEC-08 | +| TS-23 | Destroy per NPSA Secure Destruction Standard with CSE products, approved shredder | Inherited (paper) / application (digital) | Digital destruction is `rm`, which does not sanitise; no NCSC secure-sanitisation path | SEC-08 | +| TS-24 | Destruction witnessed by a DV-cleared witness and recorded in the registry | Application (record) + inherited (witness) | No record, no witness field, no ledger | SEC-02 | + +**Electronic information** + +| # | Behaviour | Class | Current state | Finding | +| --- | --- | --- | --- | --- | +| TS-25 | Work only in organisation-authorised areas on approved equipment | Inherited | — | — | +| TS-26 | Never read material in public or in the presence of unauthorised people | Human | The UI has no "shoulder-surfing" affordance, and none is needed | — | +| TS-27 | Mark header/footer, number every page; "UK" prefix for partners | Application | Not implemented | SEC-04 | +| TS-28 | Draft, store and share only on systems approved for TOP SECRET; never on the OFFICIAL/SECRET corporate system or personal devices | Application (enforcement) + inherited | The app cannot tell what system it is on, does not encrypt at rest, and offers no way to state its own accreditation | SEC-03, SEC-09 | +| TS-29 | Lock devices when leaving the workspace | Inherited | No automatic logoff (nothing to log off) | SEC-01 | +| TS-30 | Do not send information outside the Secure Isolated Network | Application (egress) | Two egress paths exist and are controlled only by config: reference-database downloads (SEC-06) and Zenodo publication (SEC-05) | SEC-05, SEC-06 | +| TS-31 | Do not share outside the organisation without the creator's authorisation and need-to-know | Application | No creator concept, no authorisation gate on export | SEC-04, SEC-05 | +| TS-32 | Clear handling instructions in the subject line and body | Application (generated artefacts) | No artefact carries handling instructions | SEC-04 | +| TS-33 | Digital destruction per NPSA standard / NCSC secure sanitisation of storage media | Application + inherited | `rm` only; no sanitisation, no certificate of destruction | SEC-08 | + +**Reading of the matrix.** Sixteen of the thirty-three behaviours have no +software expression and belong to the accredited environment; the application +must not defeat them, and today it mostly does not. Nine are *records* the +application should produce and does not (TS-10, TS-11, TS-18, TS-22, TS-24, +TS-27, TS-31, TS-32, TS-33) — those are the ones the ultraplan targets. Two +(TS-28, TS-30) are enforcement points where the application is actively +non-compliant today. + +**The unavoidable conclusion.** TOP SECRET is an *accreditation* of a system, +not a property of an application. A Julia + R + DuckDB + browser stack with +dozens of third-party packages, built by an unvetted toolchain, running on a +general-purpose OS, cannot be *asserted* to the TOP SECRET standard by its own +code. What the code can do is stop being the reason an accreditation fails. The +realistic ladder is in `ultraplan.md` §3. + +--- + +## 6. Colour-team simulation + +The full exercise — six teams, CyBOK knowledge-area mapping, scenarios, results +and the detection matrix — is in [`colour-teams.md`](colour-teams.md). Summary: + +| Team | CyBOK KAs | Verdict | +| --- | --- | --- | +| **White** (governance, law, risk) | RMG, LR, POR | The policy skeleton an assessor expects (threat model, DPIA, retention, incident, classification) is absent. This dossier starts it. | +| **Red** (adversary) | AB, WAM, SS, NS, MAT | The application survives a competent web attacker *on the code surface* (path traversal, SQLi, XSS, RCE all defended — §3) and loses completely on *identity and framing*: 2 requests delete a study, 1 clickjack or 1 script on the host. | +| **Blue** (defence, SOC) | SOIM, F | Detection is essentially absent: no access log, no auth events, no integrity baseline, no alerting. Forensic readiness is, unusually, better than detection because provenance already hashes artefacts. | +| **Purple** (validation) | AB + SOIM | Of ten red scenarios, two would be visible today (both after the fact, via provenance), eight would not. Each proposed control in the ultraplan carries the detection it enables. | +| **Green** (build, crypto, platform) | SSL, SS, C, AC, OSV, DSS | Build hygiene is strong (SHA-pinned actions, hashed locks, verified installer, signed-commit policy). Crypto is entirely absent — not weak, absent. No SBOM, no reproducible build, no signed release. | +| **Gold** (human factors, insider, trust) | HF, AAA | The single-user assumption is the biggest human risk, and it is invisible to the user. Every control in the ultraplan is scored for friction; the total added user actions across the whole plan is **zero**. | + +--- + +## 7. Attack chains (reasoned from source; not yet executed as tests) + +### C1 — "Delete the study" in two requests, unauthenticated +`DELETE /api/v1/studies/{study}` → `rm(recursive=true)` on `data/` and +`projects/` (`studies.jl:233-241`). No identity, no confirmation, no record. If +the study has no active job (the only precondition, `studies.jl:236`), raw +sequence data is gone. Chains with C2 for attribution-free destruction. + +### C2 — Clickjack the operator +Any web page embeds `http://localhost:8080` (no `frame-ancestors`, +`server.jl:440-447`), overlays a button, and the click arrives as a same-origin +`DELETE`. The Origin allowlist cannot help (SEC-11). Two clicks, one study. + +### C3 — Poison the reference database +`PUT /api/v1/databases` with a new `uri` and no `sha256` +(`databases.jl:129`), then `POST /api/v1/databases/{key}/download` +(`databases.jl:176`), then run the pipeline. Every downstream taxonomic +conclusion is now derived from attacker-supplied data, with normal-looking +provenance, and an outbound fetch of the attacker's choosing has occurred from +inside the network (SEC-06). + +### C4 — Publish to the world +`POST …/doi/publish` with `confirm: true` (`doi.jl:58-88`). Irreversible, +uncounted, unclassified. If the deployment has `DOI_ENABLED=true` and a token — +the documented configuration — one request is a reportable breach (SEC-05). + +### C5 — Local peer reads everything +A second account on the same host reads `data/` and `projects/` directly, +because nothing sets restrictive modes and nothing encrypts (SEC-03). Requires +only a login on a shared box — the commonest lab deployment there is. + +### C6 — Memory exhaustion +A single POST to any route that parses a body without a cap +(`_read_json_object`, `server.jl:96-118`), or a burst of pipeline job submissions +against a single-threaded R runtime, denies service to the operator (SEC-07). + +--- + +## 8. Risk register and route inventory + +### 8.1 Mutating routes (the blast radius of SEC-01) + +Counted from `src/server/routes/*.jl`: **155 routes** — `GET` 64, +`POST` 54, `DELETE` 20, `PUT` 11, `PATCH` 5, `SSE` 1. The ones that matter most: + +- Destructive: `studies.jl:233,288`; `runs.jl:465`; `results.jl:540`; + `trees.jl:140`; `figures.jl:98`; `report.jl:119`; `composition.jl:231,251,367`; + `results.jl:527`; `config.jl:631,896,981,1030`; `jobs.jl:29`; + `phylogeny.jl:257,551`; `cladistics.jl:321`; `evidence.jl:696`. +- Executing: `pipeline.jl:622,666`; `phylogeny.jl:281,611`; + `databases.jl:176`; `doi.jl:58`. +- Configuration: `config.jl:615,879,962,1011`; `databases.jl:129`; + `config.jl:862` (primers). + +### 8.2 Prioritised register + +| Rank | ID | Finding | Sev | Effort | Ship in | +| --- | --- | --- | --- | --- | --- | +| 1 | SEC-10/11 | Security headers + `frame-ancestors 'none'` on every response | High | S | W1 | +| 2 | SEC-07 | Body cap + job concurrency cap + SSE subscriber cap | High | S | W1 | +| 3 | SEC-02 | Append-only hash-chained audit ledger, access log on | Critical | M | W2 | +| 4 | SEC-08 | Two-phase deletion with quarantine + ledger entry | High | M | W2 | +| 5 | SEC-01 | Identity from proxy/OS + per-study authorisation | Critical | L | W3 | +| 6 | SEC-03 | Restrictive modes + FDE attestation + envelope-encrypted exports | Critical | M | W3 | +| 7 | SEC-04 | Classification attribute, marking, copy numbers, export manifest | Critical | L | W4 | +| 8 | SEC-05 | Publication gate on classification + recorded approval | High | S | W4 | +| 9 | SEC-09 | Refuse non-loopback bind without TLS termination | High | S | W1 | +| 10 | SEC-06 | `databases.yml`: require https + sha256, refuse unhashed URIs | High | S | W2 | +| 11 | SEC-14 | Personal-data flag, retention, DSAR/erasure procedure | High | M | W5 | +| 12 | SEC-13 | Export path/identifier scrubber | Medium | S | W5 | +| 13 | SEC-12 | Explicit identifier validation for table/column | Medium | S | W2 | +| 14 | SEC-15 | Token bucket on mutating routes | Medium | S | W1 | +| 15 | SEC-16 | Clear browser-side UI state on unload/identity change | Low | S | W3 | +| 16 | SEC-17 | Threat model, DPIA, incident procedure, classification policy | Medium | M | W0/W6 | + +--- + +## 9. Evidence log (reproducible) + +Run from the repository root. Every claim in this report is one of these. + +```bash +# 1. No authentication or authorisation +grep -rniE 'authorization|bearer|session|login|password|authenticate|jwt' src/ --include='*.jl' +# → only src/core/doi.jl:112,119,182 (outbound Zenodo client + redaction) + +# 2. No access log; docs/metrics off +grep -n 'const _SERVE_OPTIONS' -A 2 src/server/server.jl +# → access_log=nothing, show_errors=false, docs=false, metrics=false + +# 3. Middleware chain contains no identity step +grep -n 'const _MIDDLEWARE' -A 2 src/server/server.jl + +# 4. No encryption at rest, no key management +grep -rniE 'encrypt|cipher|keyring|gpg|aes' src/ --include='*.jl' +# → no matches + +# 5. No filesystem permission hardening +grep -rn 'chmod\|chown\|setperm\|umask' src/ --include='*.jl' +# → no matches + +# 6. No TLS in the server ('lossless' matches a bare /SSL/i, so word-anchor it) +grep -rniE '\bTLS\b|SSL_|certfile|cert_?chain|:ssl\b' src/ --include='*.jl' +# → no matches + +# 7. No rate limiting +grep -rniE 'rate.?limit|throttl|token.?bucket' src/ +# → no matches + +# 8. No classification vocabulary. Both hits are unrelated: the statistical +# 'calibration caveat' in differential results, not a handling caveat. +grep -rniE 'classification:|top secret|need.to.know|caveat:|disseminat' src/ config/ frontend/src/ +# → src/analysis/differential.jl, src/core/evidence.jl, +# frontend/src/components/DifferentialPanel.tsx:256 (all 'calibration caveat') +grep -rniE 'top secret|need.to.know|disseminat|handling caveat' src/ config/ frontend/src/ +# → no matches + +# 9. Single-user assumption, stated +sed -n '7p' src/server/jobs.jl + +# 10. Destructive routes +grep -rn '@delete' src/server/routes/*.jl + +# 11. Unauthenticated config and database rewrite +grep -n '@put "/api/v1/databases"' -A 12 src/server/routes/databases.jl + +# 12. Publication route gate +grep -n 'doi/publish' -A 30 src/server/routes/doi.jl | head -40 + +# 13. Headers: the one good route vs the rest +sed -n '187,194p' src/server/routes/statistics_ui.jl +sed -n '440,447p' src/server/server.jl + +# 14. Provenance data-minimisation warning, in the author's own words +sed -n '795,802p' src/core/provenance.jl + +# 15. Strengths: name validation, shell safety, remote target validation +sed -n '21,22p;50,68p' src/core/validate.jl +sed -n '86,94p' src/pipeline/tools.jl +sed -n '19,48p' src/pipeline/remote_exec.jl + +# 16. Strengths: R formula AST validation, SQL identifier guard +sed -n '216,260p' src/analysis/ordination.jl +sed -n '355,364p' src/core/categories.jl + +# 17. Strengths: integrity of reference databases and derived records +sed -n '114,125p' src/core/databases.jl +grep -n 'does not match its ID' src/analysis/derived_analyses.jl + +# 18. Strengths: CI posture +grep -n 'permissions:' -A 3 .github/workflows/ci.yml +grep -n 'pull_request_target' -r .github/workflows/ ; echo "exit=$?" +grep -n 'uses:' .github/workflows/ci.yml | head + +# 19. Absence of dynamic XSS sinks in the frontend +grep -rn 'dangerouslySetInnerHTML\|innerHTML\|eval(' frontend/src +# → no matches + +# 20. Environment limits of this review +git rev-parse --is-shallow-repository # → true +command -v julia Rscript bun # → none present +``` + +--- + +## 10. Residual limitations + +1. **No runtime evidence.** Nothing in this report was observed executing. Two + findings (SEC-11, SEC-12) are explicitly reasoning-only; SEC-07's DoS is + inferred from the absence of a cap, not measured. +2. **No dependency audit.** I did not consult GHSA/OSV for the Julia, R or npm + trees. Dependabot is configured (`.github/dependabot.yml`) and Dependabot + alerts plus Scorecard are the current evidence; that is not the same as a + clean advisory scan, and I do not claim one. +3. **No cryptographic review** of TLS termination, if any, outside this repo; + none is implemented here. +4. **No physical or personnel assessment**, and none is possible from a + repository. +5. **Threat model is generic.** The actor set in §2.3 is inferred from the + deployment documentation; if the author's real deployment differs (e.g. it is + already behind mTLS and FDE), SEC-01's and SEC-03's severity drops + substantially and the ultraplan's W3 shrinks to documentation. That + distinction should be confirmed before any engineering starts. diff --git a/docs/security/ultraplan.md b/docs/security/ultraplan.md new file mode 100644 index 00000000..c756d545 --- /dev/null +++ b/docs/security/ultraplan.md @@ -0,0 +1,725 @@ + + + +# Ultraplan: from local analysis tool to classification-ready appliance + +Status: plan and evidence map for the branch +`arena/924008e1-metamanifold-webui`, based on `d6e8671` (`main`). +Companion to [`recon-report.md`](recon-report.md) (what is wrong and why it +matters) and [`colour-teams.md`](colour-teams.md) (the adversarial and +governance read). Finding IDs `SEC-nn` refer to the recon report. + +**Nothing in this plan is applied.** No source file was modified by this +session; the branch adds documentation only. No test was run: the review +environment has no Julia, R or bun (see `recon-report.md` §0.3). + +--- + +## 1. The four rules this plan is built on + +### Rule 1 — Eliminate, substitute, notify, in that order + +For every exposure, choose the first of these that works: + +1. **Eliminate** — remove the reach, the capability or the data. Nothing to + defend afterwards. +2. **Substitute** — replace the mechanism with a safer one that produces the + same result for the user. Same request, same output, different internals. +3. **Notify** — record and disclose, default-safe. Reads fail open *only* where + the data is already visible; writes fail closed. + +If a control can only be implemented as "ask the user to be careful", it is not +on this list and does not ship. + +### Rule 2 — Zero recurring friction + +The friction budget is **zero added user actions** in the routine path +(`colour-teams.md` §6.3). Any proposal that adds a per-action prompt, a +remembered credential, or a periodic interruption is rejected in favour of a +substitute. The one-off setup cost (setting a study's classification, declaring +that it holds personal data) is capped at **two decisions per study, ever**. + +### Rule 3 — One logical change per PR + +`CONTRIBUTING.md` requires a branch from current `main`, no merge commits, one +logical change, CI evidence, and fast-forward merges with signed commits. Every +workstream below is therefore split into PR-sized units, each of which is +independently revertible and independently useful. + +### Rule 4 — Do not touch the science + +`0-AI-MANIFEST.a2ml` invariants stand: keep the settled toolchains, keep the +independent Python reference oracles, never replace scientific results with +placeholders, keep JSON as I-JSON/JCS and new config as KYAML. This plan touches +none of the numeric, statistical or pipeline logic. Where a change reaches a +route that returns a scientific result, the acceptance test is that the +**bytes are unchanged** for the same input. + +--- + +## 2. Target architecture (what "done" looks like) + +``` +┌──────────────────────────── accredited boundary ────────────────────────────┐ +│ operator's browser ──HTTPS via local proxy (mTLS or OS SSO)──┐ │ +│ ▼ │ +│ proxy asserts principal ──► Julia server (127.0.0.1 only) │ +│ ├─ identity middleware (SEC-01) │ +│ ├─ authorisation (SEC-01) │ +│ ├─ audit ledger (hash-chained, append-only) │ +│ ├─ classification gate (SEC-04) │ +│ ├─ egress allowlist (SEC-05/06) │ +│ └─ resource caps (SEC-07/15) │ +│ │ +│ data/ + projects/ → restrictive modes, inside an FDE volume (SEC-03) │ +│ exports → marked, numbered, manifest-hashed, envelope-encrypted│ +│ every artefact → carries classification, copy number, provenance │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +Design commitments: + +- **Identity is asserted, never invented.** No user table, no password store, no + MFA prompt. The principal comes from a mutually authenticated proxy header or + the OS account of the connecting process on a loopback socket. The server + records it and authorises with it; it never stores a credential. +- **One audit ledger for three standards.** The same append-only records satisfy + GDPR Art. 30/32, HIPAA §164.312(b) and the TOP SECRET registry/registry-book + behaviours (TS-11, TS-18, TS-24). +- **One classification attribute**, inherited down study → group → run, enforced + at one choke point, rendered everywhere. +- **One choke point per concern.** All export routes write through a single + `_publish_artefact` function; all state-changing routes go through one + authorisation + ledger middleware pair. If a new route is added, it is + covered by construction, not by remembering. + +--- + +## 3. The ladder, and which rung each workstream buys + +| Rung | Meaning | Bought by | +| --- | --- | --- | +| R1 OFFICIAL-ready | safe on a managed workstation | W1, W2 | +| R2 OFFICIAL-SENSITIVE-ready | safe for sensitive personal data | W3, W5 | +| R3 SECRET-candidate | deployable inside an accredited SECRET environment | W4, W6, W7 | +| R4 TOP SECRET | an accredited component inside a Tier 3 SIN | W8 + the human gates in §7 | + +Do not claim a rung above the last one whose exit criteria are signed off. The +ladder exists so that partial delivery is still a truthful statement. + +--- + +## 4. Workstreams + +Each workstream gives: objective, the exact files touched, the acceptance +evidence (a command and its expected result), the standards clauses closed, the +event(s) added to the ledger, and the friction cost. + +--- + +### W0 — Publish the evidence base (documentation only) — **this branch** + +**Objective.** An assessor can start work without interviewing anyone. + +**Changes.** `docs/security/README.md`, `recon-report.md`, `colour-teams.md`, +`ultraplan.md`; a pointer section in `SECURITY.md`; optionally a line in +`0-AI-MANIFEST.a2ml`'s `evidence` block naming the security dossier. + +**Acceptance.** `docs/security/` exists, the evidence log in +`recon-report.md` §9 runs clean on `main`, and every claim in the dossier +resolves to a file and line at `d6e8671`. + +**Closes.** SEC-17 (partially). **Friction.** 0. + +**PR.** One. Pure documentation, no executable change, so it cannot regress the +suite. + +--- + +### W1 — Close the zero-cost exposures (the whole of it is headers, caps, refusals) + +**Objective.** Remove every attack in the recon report that needs no +architectural change. This is the highest value-per-line work in the plan and it +should ship first, in one or two PRs. + +**W1.1 — Security headers on every response (SEC-10, SEC-11).** +Reuse the header set already proven on the statistics page +(`src/server/routes/statistics_ui.jl:187-194`). Add one helper in +`src/server/server.jl` and apply it in `_file_middleware` for API, static, SPA +and `/files` responses: + +```julia +# Sketch only — not applied, not compiled. +const _BASE_HEADERS = ( + "X-Content-Type-Options" => "nosniff", + "Referrer-Policy" => "no-referrer", + "X-Frame-Options" => "DENY", + "Content-Security-Policy" => "default-src 'self'; script-src 'self'; " * + "style-src 'self' 'unsafe-inline'; img-src 'self' data:; " * + "connect-src 'self'; frame-ancestors 'none'; base-uri 'none'", + "Permissions-Policy" => "geolocation=(), camera=(), microphone=(), usb=()", + "Cross-Origin-Opener-Policy" => "same-origin", +) +``` + +Note the two constraints the existing app has that a naive CSP will break: +Plotly injects inline styles (`style-src 'unsafe-inline'` is required), and the +frontend is a bundled SPA (`script-src 'self'` is sufficient, no inline script). +Verify by loading the app and every panel. + +**Acceptance.** `test/unit/test_server_security.jl` gains assertions that +`GET /`, `GET /api/v1/studies`, and `GET /files/...` all carry +`frame-ancestors 'none'` and `nosniff`; and a new test that the CSP header on +`/` contains no `unsafe-eval`. + +**W1.2 — Resource caps (SEC-07, SEC-15).** +One middleware that rejects a request whose `Content-Length` (or accumulated +body) exceeds `MAX_REQUEST_BYTES` from `src/analysis/numeric_policy.jl:38` with +`413`; a cap on queued+running jobs in `src/server/jobs.jl` returning `429`; a +cap on SSE subscribers in `events.jl`/`jobs.jl`. + +**Acceptance.** New tests: a body of `MAX_REQUEST_BYTES + 1` on +`/api/v1/studies/{s}/report` returns 413 *before* anything is written; the +`(n+1)`-th concurrent pipeline submission returns 429; `GET /api/v1/jobs` +reports no leaked channels after a subscriber cap is hit. + +**W1.3 — Refuse an unaccredited bind (SEC-09).** +In `start()`: if `host` is not loopback and neither +`JULIA_METAMANIFOLD_TLS_TERMINATED=1` nor an explicit +`JULIA_METAMANIFOLD_ALLOW_PLAINTEXT=1` is set, log the reason and **refuse to +start**. Prefer refusing over warning: a warning in a startup log is not a +control. + +**Acceptance.** Test that `start(host="0.0.0.0")` throws with a message naming +both escape hatches, and that the loopback path is unchanged. + +**W1.4 — Strengthen the existing egress gates (SEC-06, SEC-05 default).** +`PUT /api/v1/databases`: require `https` for `uri`, require `sha256` (and +`size`) whenever `uri` is present, and require that a change to `uri` carries a +new hash. Keep the advisory warnings, but make the *refusals* hard. + +**Acceptance.** Tests: `http://` URI → 400; `https://` with no hash → 400; +`local:` path outside the configured database directory → 400; a legitimate +document round-trips byte-identically. + +**Closes.** SEC-05 (default path), SEC-06, SEC-07, SEC-09, SEC-10, SEC-11, +SEC-15. **Ledger events.** `limit.exceeded`, `config.write databases`, +`startup.refused`. **Friction.** 0. **Size.** S–M. + +--- + +### W2 — The audit ledger and two-phase deletion (SEC-02, SEC-08) + +**Objective.** Every state change is attributable, and destruction is a +recorded, two-step, recoverable act. + +**W2.1 — `src/core/audit.jl`.** +An append-only ledger under `projects/.audit/`, one canonical-JSON record per +line, each record carrying `prev_sha256`, so the chain is verifiable. Reuse +`src/canonical.jl` (`jcs_bytes`) and `SHA.sha256`. Fixed event vocabulary in one +place (`const AUDIT_EVENTS`) so later workstreams extend it deliberately. + +Record shape: + +```json +{"seq":42,"at":"2026-10-25T12:00:00Z","event":"delete.requested", + "actor":{"kind":"proxy","id":"j.smith","source":"127.0.0.1"}, + "target":{"study":"Gut2026","run":"run_A","items_sha256":"…","count":3}, + "prev_sha256":"…","record_sha256":"…"} +``` + +Two rules that keep it honest: +- The actor is `{"kind":"unauthenticated"}` until W3 lands; do **not** omit the + field, or the ledger will have to be rewritten when identity arrives. +- A ledger write failure **fails the operation** for mutating events. A control + that silently stops recording is worse than no control. + +**W2.2 — Wire it.** +A middleware pair around the mutating routes (a `_ledger_middleware` beside +`_bad_request_middleware`, `src/server/server.jl:494`) plus explicit calls in +the ~12 destructive/executing/configuration routes enumerated in +`recon-report.md` §8.1. Turn on a structured access log at the same time +(replaces `access_log=nothing`, `server.jl:498`). + +**W2.3 — Two-phase delete.** +`DELETE` moves the target into `projects/.quarantine/{uuid}` with a manifest +(hash, size, count, original path) and writes `delete.requested`. A new +privileged `POST /api/v1/quarantine/{id}/purge` removes it and writes +`purge.committed`. A maintenance job purges after a configurable retention +(no default purge at all, so nothing disappears unexpectedly). The UI shows a +"Recently deleted" list — a *recovery* affordance, not a confirmation step. + +**Acceptance.** `julia --project=. -t2 test/unit/test_audit.jl` verifies: chain +integrity after N writes; tampering with record k is detected at k+1; a failed +ledger write aborts the delete; delete→purge sequence is order-enforced; +quarantine content hash matches the manifest; concurrent writers serialise. +Plus a route test that `DELETE /api/v1/studies/{s}` on a study with an active +job still returns 409 (behaviour unchanged). + +**Closes.** SEC-02, SEC-08; TS-11, TS-18, TS-22, TS-24, TS-33 (records). +**Ledger events.** `delete.requested`, `purge.committed`, `config.write`, +`run.start`, `run.cancel`. **Friction.** 0. **Size.** M. + +--- + +### W3 — Asserted identity and per-study authorisation (SEC-01, SEC-03a, SEC-16) + +**Objective.** Every request has a principal; every principal has a scope; +nothing on the host is world-readable. + +**W3.1 — `_identity_middleware`.** +Accept a principal in this order, first match wins: + +1. `X-Auth-Request-User` (or a configurable header name) **only** when the + request arrives on loopback *and* `JULIA_METAMANIFOLD_TRUSTED_PROXY=1`. This + is the oauth2-proxy / Authelia pattern: the proxy does the hard part, the app + records the result. +2. `SO_PEERCRED`/`getpeereid` on a Unix socket, mapping the uid to a name. +3. `ENV["USER"]` **only** for a loopback bind with no proxy — the today case, + and it is documented as "trust the operating system's session". + +The principal goes into task-local storage next to `mm_analysis_source` +(`src/server/server.jl:221`) so every route and every ledger record can read it without +signature changes. + +**W3.2 — Authorisation.** +A `study_acl.yml` under `config/` mapping principal → studies (and roles +`viewer`/`editor`/`deleter`/`admin`). Default policy for a loopback, +single-user install: **permit all**, recorded in the ledger as +`{"kind":"os-session"}` — the incumbent sees no change at all. When a second +principal exists, the default flips to deny-by-default for studies they are not +granted, because by then the operator has explicitly asked for multi-user. + +Every route body that takes a `{study}` gets its check through one helper, +`_authorise!(principal, study, role)`, called from `_validate_run_request` and +`_study_exists`'s callers — i.e. from the two functions every route already +calls, so coverage is by construction. + +**W3.3 — Filesystem modes and at-rest position (SEC-03).** +Set `0700` on created directories and `0600` on created files +(a `_secure_mkpath`/`_secure_write` pair used by `studies.jl`, `report.jl`, +`figures.jl`, `trees.jl`, `config.jl`). Add a startup check that the data root +is on an encrypted volume where the platform can tell (Linux: `/proc/mounts` +dm-crypt/crypt target; macOS: `diskutil apfs listcryptousers`; Windows: skip and +warn), and record the verdict in the attestation and the startup log. Do not +implement application-level file encryption for the working tree: FDE is the +substitute, and a second layer inside the app adds a key to lose. + +**W3.4 — Clear client-side state (SEC-16).** Clear `sessionStorage` on +`pagehide` and on principal change (`DataTable.tsx:85-92`). + +**Acceptance.** Tests: request with no principal under default policy succeeds +and the ledger records `os-session`; with `trusted_proxy=1` and the header set, +the ledger records that principal; a header supplied *without* +`trusted_proxy=1` from a non-loopback peer is ignored (spoofing test); principal +A gets 403 on principal B's study once the ACL exists; a created directory is +`0700` (assert with `stat`). + +**Closes.** SEC-01, SEC-03 (a), SEC-16; GDPR Art. 32(1)(b); HIPAA +§164.312(a)(1), (a)(2)(i), (d), §164.308(a)(3)(4); TS-06, TS-28, TS-29. +**Ledger events.** `auth.grant`, `authz.denied`. **Friction.** 0 with a proxy or +an OS session; 1 (one ACL file) for a team. **Size.** L — this is the largest +workstream and the only one that changes every route's assumptions. Ship it in +four PRs: middleware, ACL, helpers, modes. + +--- + +### W4 — Classification, marking, and dissemination (SEC-04, SEC-05) + +**Objective.** Every artefact the tool emits says what it is, who may have it, +and which copy it is — without ever asking the user again after study creation. + +**W4.1 — The attribute.** +`classification: OFFICIAL | OFFICIAL-SENSITIVE | SECRET | TOP-SECRET` plus +`dissemination: [list]` and `originator: ` on the study's +`pipeline.yml`; inherited by group and run via the existing cascade +(`src/core/config.jl`), so no new storage model is needed. Validation lives in +`src/core/validate.jl` beside `SAFE_NAME_RE`; the value is part of the config +hash, so a classification change invalidates the stages exactly as any other +config change does. + +Note the cascade's existing behaviour helps here: the *most specific* level wins, +which is the opposite of what classification requires. Fix it in one place — the +effective classification is the **maximum** over the cascade, never the leaf — +and test it explicitly, because "an analyst quietly downgraded a study" is the +attack this control exists to stop. + +**W4.2 — One export choke point.** +Introduce `_publish_artefact(study; bytes, kind, filename, format)` in a new +`src/server/routes/publication.jl`, used by the report ZIP, the XLSX export, +publication tables, figure rasters, tree files and the attestation. It: + +1. resolves the effective classification, +2. injects the marking banner and a copy number, +3. stamps a manifest (`items`, `sha256`, `classification`, `originator`, + `generated`, `dissemination`) into the archive and alongside the file, +4. writes `export.generated` to the ledger, +5. refuses the export if the format cannot carry a marking and the + classification is above OFFICIAL (a CSV cannot carry a banner — so a marked + companion `.txt`/manifest travels with it automatically; that is a + substitution, not a refusal of the user's workflow). + +Marking formats to follow exactly, from the Tier 3 behaviour table: header and +footer on every page (HTML/PDF), `TOP SECRET` on the inner packaging only when +transporting (not applicable to files), a `UK` prefix when shared with an +international partner, copy numbers `1 of 3` top-right of page 1 of a printed +pack, and a pack marked at its **highest** classification. + +**W4.3 — Banner in the UI.** +A single persistent banner rendered by `App.tsx` from `GET /api/v1/capabilities` +(extend that route rather than adding one). It shows the current study's +classification and the principal. This is the software analogue of the chair's +opening declaration (TS-01) and the "handling instructions in the subject line" +behaviour (TS-32), and it costs no interaction. + +**W4.4 — Gate publication (SEC-05).** +`POST /api/v1/studies/{study}/doi/publish` refuses unless the effective +classification is `OFFICIAL` and records the approval identity, the file hashes +and the DOI in the ledger on success. `DOI_ENABLED` remains the master switch. + +**Acceptance.** Tests: cascade maximum (study OFFICIAL + run TOP-SECRET → +TOP-SECRET); every export route returns bytes containing the banner and a +manifest whose `classification` equals the effective value; a CSV export above +OFFICIAL produces the manifest companion automatically; publish refuses at +SECRET with `403 classification_blocked` and writes `publish.refused`; the +same-input bytes of a scientific JSON response are unchanged (Rule 4). + +**Closes.** SEC-04, SEC-05; TS-09, TS-10, TS-11, TS-13, TS-14, TS-27, TS-31, +TS-32. **Ledger events.** `classification.changed`, `export.generated`, +`publish.requested|refused|completed`. **Friction.** 1 decision per study. +**Size.** L. Split into: attribute+cascade, choke point, banner, publish gate. + +--- + +### W5 — Privacy: personal-data state, retention, rights, export scrubbing (SEC-13, SEC-14) + +**Objective.** The controller can answer a DSAR, evidence an erasure, bound +retention, and stop incidental identifiers leaving in the provenance artefacts. + +**W5.1 — Declare and detect.** `personal_data: true|false|unknown` on the study +(default `unknown`, which behaves like `false` but shows a one-line hint once). +At ingest, run a cheap identifier scan on column names and on sample-name +patterns (`NHS`, `MRN`, `DOB`, postcodes, `surname`, clinical-looking tokens) +and, if `personal_data` is false but the scan fires, refuse with a clear reason. +That is eliminate-by-default: the cheapest minimisation is not accepting the +identifier in the first place. + +**W5.2 — Retention.** `retention_days` in config; a maintenance job (reuse the +`JobQueue`) that lists — never deletes — studies past their window, writing +`retention.due` to the ledger. Deletion stays a human act, recorded, and +two-phase from W2. + +**W5.3 — Rights.** `GET /api/v1/studies/{study}/dsar` produces a manifest of +everything held for a declared subject (a study-scoped export plus the ledger +entries that touched it) — this is mostly assembly of what already exists. +Erasure is the W2 two-phase delete plus a `erasure.evidenced` ledger record that +names what was destroyed and by whom. Both write to the ledger, which is what +makes them *evidence* rather than *activity*. + +**W5.4 — Export scrubber (SEC-13).** The scrubber in W4.2's choke point: rewrite +absolute paths to ``-relative, drop `hostname` from published +attestations (keep it in the local copy), and apply the study's redaction list to +log text. Nothing is asked of the user; the bytes simply stop carrying +`C:\Users\jsmith\`. + +**Acceptance.** Tests: an identifier-shaped column with `personal_data: false` +is refused; a DSAR manifest lists the study's files and their hashes and the +ledger slice; a published attestation contains no `hostname` and no absolute +path; erasure produces a `erasure.evidenced` record whose item hash matches what +was quarantined. + +**Closes.** SEC-13, SEC-14, SEC-17 (DPIA input); GDPR Art. 5(1)(c),(e), 12-22, +30; HIPAA §164.502(b), §164.528. **Friction.** ≤1 decision per study. +**Size.** M. + +--- + +### W6 — Assurance: SBOM, signing, sandbox, and a security test lane (SEC-12, SEC-17) + +**Objective.** Make the release itself something a reviewer can trust, and make +future regressions fail CI. + +**W6.1 — SBOM per release.** Emit CycloneDX for the Julia manifest, `renv.lock` +and `bun.lock` in CI; publish as a workflow artefact and attach it to the +release. + +**W6.2 — Reproducible frontend build + signed release.** `--frozen-lockfile` is +already used in `start.sh`; pin `SOURCE_DATE_EPOCH`, build twice and diff the +`web/dist` hashes in CI, and sign the release bundle (sigstore/cosign or a +GPG-signed `SHA256SUMS`). Document that the *source* is signed +(`notes/commit-signing.md`) and the *artefact* now is too. + +**W6.3 — Advisory gate.** Add an `osv-scanner` (or `npm audit --production` + +`bundle-audit` equivalent) step over the three lockfiles, failing on a known +vulnerable direct dependency and reporting transitive ones. + +**W6.4 — Runtime hardening guidance.** `start.sh`/`docs`: run under a dedicated +unprivileged account, `systemd` with `ProtectSystem=strict`, +`ProtectHome=read-only`, `PrivateTmp=yes`, `NoNewPrivileges=yes`, +`RestrictAddressFamilies=AF_UNIX AF_INET`, and a read-only bind of the +application tree. Document that `bash -lc` and in-process R are deliberate +capabilities and that isolation therefore belongs to the unit file, not to the +router. + +**W6.5 — Security test lane (SEC-12 and regression traps).** +New `test/unit/test_server_security_hardening.jl`: +- explicit identifier predicates for `{table}`/`{column}`, with a `"`-bearing + name returning 400 (removes the "defended by consequence" pattern); +- headers present on every response class; +- body-cap and job-cap behaviour; +- spoofed proxy header ignored without the trust flag; +- ledger chain integrity; +- classification cascade maximum. + +Add a fuzz lane over the JSON/YAML/NEWICK parsers (Julia `Fuzz`/simple seeded +random corpus) and a nightly `zap`-style baseline against a loopback instance. +Both are additive CI jobs; neither gates a PR until it has been green for a +week, then it does. + +**Acceptance.** The new job proves: SBOM exists and lists the three trees; two +builds of the same tree give identical `web/dist` hashes; the signature +verifies; the fuzz lane runs 10k cases with no 500 and no crash. + +**Closes.** SEC-12, SEC-17 (testing); the green-team gaps in +`colour-teams.md` §5 (G-02..G-07). **Friction.** 0. **Size.** M. + +--- + +### W7 — Verification of the recon itself (turn reasoning into tests) + +**Objective.** Every red-team scenario in `colour-teams.md` §2 becomes either a +test that fails before the fix and passes after, or an explicitly accepted risk. + +| Scenario | Test to write | +| --- | --- | +| R-01 delete | `DELETE /api/v1/studies/{s}` moves to quarantine, does not `rm`; a second request purges (W2) | +| R-02 clickjack | response headers carry `frame-ancestors 'none'` on `/` and `/files` | +| R-03 database swap | `http://` and hash-less URIs refused; a swapped `databases.yml` cannot reach a run | +| R-04 publish | refusal at SECRET, ledger entry written | +| R-05 peer read | created dirs are `0700`; FDE check reports its verdict | +| R-06 DoS | body cap, job cap, SSE cap | +| R-07..R-11 | keep as regression tests so they stay closed | + +**Acceptance.** A new `test/unit/test_threat_scenarios.jl` in which each test +names the scenario ID in its `@testset` label, so the evidence map is +machine-readable. + +**Size.** S–M. + +--- + +### W8 — The accreditation pack and the human gates + +**Objective.** Everything a security assessor or a DPO asks for, produced from +the repository rather than written from memory. + +Deliverables (docs only, but each must be signed): + +1. **System security policy** — scope, classification policy, roles, handling + rules, sanctions. +2. **Threat model** — asset/actor/trust-boundary from `recon-report.md` §2, + extended with the deployment's real answer to §10.5. +3. **DPIA** (or a documented screening decision) — purpose, necessity, + proportionality, risks, mitigations, residual risk, sign-off. +4. **Record of processing** template + a version generated from the ledger. +5. **Incident response procedure** — including the specific case "study + published to Zenodo in error" (revocation, notification, ledger evidence). +6. **Retention and destruction policy** — mapped to W2 and W5 mechanisms. +7. **Configuration baseline** — the exact `config/defaults/*` and unit file an + accredited instance must run. +8. **Residual risk statement** — honest, signed, with the rung from §3 named. +9. **Evidence index** — one row per claim, each with a command and a result + (the `recon-report.md` §9 format, extended to every control). + +**Human gates that cannot be closed by an agent, and must not be claimed:** + +- independent security review of the changes in W1–W7; +- DPO sign-off on the DPIA; +- SSA/SA (or equivalent) acceptance of the residual risk; +- cryptographic approval of the at-rest and envelope schemes; +- platform accreditation (the SIN, the rooms, the TSCM, the personnel); +- for TOP SECRET specifically: the Government Security Group guidance the brief + mentions is held at a higher classification, and the NCSC must be consulted + where there is a business need. **This repository cannot contain that + material, and this plan does not attempt to.** + +**Size.** M (documentation), plus unbounded calendar time for the gates. + +--- + +## 5. Sequencing, dependencies, and PR order + +``` +W0 docs ──────────────────────────────────────────────────────────► main now +W1.1 headers ─┬─► W1.3 bind refusal ─► W1.4 egress refusals +W1.2 caps ────┘ │ + ▼ +W2 ledger + two-phase delete ──────────────► W3 identity + ACL + modes + │ + ┌─────────────┴─────────────┐ + ▼ ▼ + W4 classification W5 privacy + └─────────────┬─────────────┘ + ▼ + W6 assurance + test lane + ▼ + W7 threat-scenario tests + ▼ + W8 accreditation pack +``` + +Why this order: + +- W1 first because it is the only workstream that removes live attack paths + without touching a route's contract, and because `frame-ancestors` is + eliminate-class. +- W2 before W3 because the ledger must exist before there is an actor to record; + the ledger's `actor` field is populated with `unauthenticated` from day one so + no schema migration is needed later. +- W4 after W3 because a classification whose enforcement depends on an + unauthenticated caller is theatre. +- W5 after W4 because the export scrubber lives in W4's choke point. +- W6 and W7 last because they are evidence, and evidence of a design that is + still moving is noise. + +Each bullet below is one PR: + +| PR | Content | Depends on | +| --- | --- | --- | +| 1 | W0 docs (this branch) | — | +| 2 | W1.1 headers + tests | 1 | +| 3 | W1.2 caps + W1.3 bind refusal + tests | 2 | +| 4 | W1.4 database-document refusals + tests | 2 | +| 5 | W2.1 `src/core/audit.jl` + chain tests | — | +| 6 | W2.2 wiring + access log + tests | 5 | +| 7 | W2.3 two-phase delete + UI recovery list | 6 | +| 8 | W3.1/3.2 identity + ACL + tests | 6 | +| 9 | W3.3 modes + FDE check + W3.4 client state | 8 | +| 10 | W4.1 attribute + cascade maximum + tests | 9 | +| 11 | W4.2 export choke point + manifests + tests | 10 | +| 12 | W4.3 banner + W4.4 publish gate | 11 | +| 13 | W5.1/5.2 personal-data + retention | 12 | +| 14 | W5.3/5.4 rights + scrubber | 13 | +| 15 | W6.1/6.2 SBOM + reproducible + signing | 14 | +| 16 | W6.3 advisory gate + W6.4 unit file | 15 | +| 17 | W6.5 + W7 test lanes | 16 | +| 18 | W8 pack | 17 | + +PRs 5 and 2 can start in parallel with anything; everything else follows the +graph. Sixteen of the eighteen PRs add files or middleware; only PR 7, 9, 11, 12 +and 14 touch existing route bodies, which keeps the review surface small. + +--- + +## 6. Definition of done, per rung + +**R1 (after W2).** Headers on every response; caps enforced; unaccredited binds +refused; every mutating route writes an audit record; deletion is two-phase; +ledger verifies; the existing suite is green and no scientific response changed +byte-for-byte. + +**R2 (after W5).** Every request has a recorded principal; per-study +authorisation exists and defaults to the incumbent behaviour; data directories +are `0700` inside an attested FDE volume; exports are marked, numbered and +manifest-hashed; personal data is declared, scanned and retained on a schedule; +DSAR and erasure produce evidence. + +**R3 (after W7).** All of R2, plus: every export carries a copy number and a +dissemination list; publication is gated on classification and recorded; the +build is reproducible, signed, SBOM'd and advisory-scanned; a security test lane +covers the ten red scenarios; the accreditation pack exists and every claim in it +has a command and a result. + +**R4.** R3 plus the human gates in W8 and the platform accreditation. **Not +deliverable from this repository, by design and by definition.** + +--- + +## 7. What not to build (and why) + +| Tempting control | Why not | Substitute instead | +| --- | --- | --- | +| A password database / local accounts | Creates a credential to steal, a reset flow to support, and a second identity model next to the proxy's. AAA research has been clear for a decade: don't own the authenticator if you don't have to. | Asserted identity (W3.1) | +| Per-action confirmation dialogs | Click-through blindness; measurable friction with no security gain once two-phase delete exists. | Quarantine + recovery list (W2.3) | +| A classification dropdown on every export | Guarantees wrong answers within a week. | Inherited attribute set once (W4.1) | +| Application-level file encryption of the working tree | A second key to manage, a performance cost on multi-GB FASTQ, and no better threat model than FDE on a managed device. | FDE + restrictive modes (W3.3) | +| Home-grown crypto anywhere | Non-negotiable. | age/sigstore/platform primitives | +| An in-app "handling instructions" cookie banner | Nobody reads them. | Marking on the artefact itself (W4.2) | +| Rewriting the toolchain in a memory-safe language | The manifest invariants forbid it, and it addresses none of the findings in `recon-report.md` §4. | Isolation via the unit file (W6.4) | +| Deleting the DOI feature | Loses a real capability the author needs. | Gate it (W4.4) | +| Removing the remote-compute feature | Same. | It inherits the boundary; document it in the accreditation pack (W8.7) | + +--- + +## 8. Evidence the author should expect to provide + +Per rung, kept in the repository so it does not rot: + +| Evidence | Command | Where it lands | +| --- | --- | --- | +| Headers on every response class | `julia --project=. -t2 test/unit/test_server_security.jl` | CI | +| Caps enforced | `julia --project=. -t2 test/unit/test_server_security_hardening.jl` | CI | +| Ledger chain verifies | `julia --project=. -t2 test/unit/test_audit.jl` | CI | +| Threat scenarios closed | `julia --project=. -t2 test/unit/test_threat_scenarios.jl` | CI | +| Classification cascade | `julia --project=. -t2 test/unit/test_classification.jl` | CI | +| Export marking and manifests | `julia --project=. -t2 test/unit/test_publication.jl` | CI | +| Reproducible frontend | `bash scripts/ci/check-reproducible-frontend.sh` (new) | CI | +| SBOM | `bash scripts/ci/emit-sbom.sh` (new) | release artefact | +| Advisory scan | `osv-scanner -r .` (new step) | CI | +| Secret scan | existing `.github/workflows/secret-scanner.yml` | CI | +| Scorecard | existing `.github/workflows/scorecard.yml` | GitHub | +| Full suite unchanged | existing `ci.yml` | CI | + +Every new CI step must pin its action by SHA, take `permissions: contents: read`, +and avoid `pull_request_target`, matching the estate rules the repository already +enforces in `scripts/check-governance-contract.sh`. + +--- + +## 9. Risks in executing this plan + +| Risk | Mitigation | +| --- | --- | +| Headers break the app (Plotly inline styles, worker scripts, blob URLs) | Apply CSP in report-only mode for one release, collect violations, then enforce. Ship `frame-ancestors` and `nosniff` immediately — neither can break a legitimate page. | +| The ledger becomes a per-request write on every route and slows the API | Ledger only for mutating events and access to *classified* artefacts; batch-fsync; measure with the existing `bench/` harness and record the number in the PR. | +| Identity changes touch every route and risk a regression in scientific output | The ACL check goes into `_validate_run_request`/`_study_exists`, which routes already call; a "permit all in single-user mode" default means the incumbent path is byte-identical, asserted by a full-suite run plus a byte-comparison test on one analysis response. | +| Classification cascade maximum contradicts the config cascade's leaf-wins rule | Implement as an explicit `effective_classification()` function, not as a change to the merge, and test the contradiction directly. | +| The plan is seen as scope creep by the scientific maintainer | W1 is one PR and removes six attack paths; everything else is opt-in by rung. The `0-AI-MANIFEST.a2ml` invariants are explicitly preserved (§1 Rule 4). | +| Accreditation is asked for before R2 is real | The §3 ladder plus the residual-risk statement in W8.8 is the answer: name the rung, never the aspiration. | +| Overhead of 18 PRs on a fast-moving `main` | PRs 2, 5 and 10 are independent of everything else and can be rebased; the rest touch disjoint files by design. | + +--- + +## 10. Indicative sizing + +T-shirt sizes and the reasoning, not dates: + +| Workstream | Size | Why | +| --- | --- | --- | +| W0 | S | Write-up of what is already true | +| W1 | S–M | One middleware, one helper, four refusals, tests | +| W2 | M | New module + chain verification + quarantine semantics | +| W3 | **L** | Touches every route's assumptions; four PRs; the only high-risk item | +| W4 | L | New attribute, one choke point across six export routes, marking formats | +| W5 | M | Assembling what exists; scrubber; retention job | +| W6 | M | New CI jobs, signing, SBOM, unit file | +| W7 | S–M | Mechanical once W1–W5 are in | +| W8 | M | Documentation + calendar time for the gates | + +Sequence W1 now. It is small, it is eliminate-class, it regresses nothing, and +it changes the answer to "can this box be left on a lab network?" from *no* to +*it depends* — which is the first honest step on the ladder. + +--- + +## 11. What this plan deliberately does not do + +- It does not claim any rung is reached. Nothing here was run, and the + recon was static (`recon-report.md` §0.3). +- It does not touch the analysis, statistics or pipeline code. +- It does not add a second identity system, a crypto implementation, or a + password store. +- It does not attempt to bring TOP SECRET guidance held at a higher + classification into this repository. The brief's own §4 says where that comes + from: the Government Security Group, via the organisation's security team, + with the NCSC consulted where there is a business need. From 1211901589f22260f61b37cd497cd94a0bfdcea5 Mon Sep 17 00:00:00 2001 From: Arena Agent Date: Sun, 25 Oct 2026 20:14:52 +0000 Subject: [PATCH 2/3] docs(proposal): propose the security and integrity programme for institutional and commercial use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds docs/proposals/security-and-integrity-programme.md in the form this project uses for proposals, written for the audience that carries institutional risk rather than the one that runs the pipeline. The proposal argues that the field reviews research software at the level of detail its authors care about - estimators, parity tolerances, dispersion priors - and not at the level an institution needs afterwards: who could have altered a result, who can delete a study, what protects the data at rest, and what a released artefact says about itself. It uses this repository's own evidence for the asymmetry: twelve controls that are better than most commercial products (path containment, an Origin allowlist with DNS-rebinding tests, an R formula AST, verified reference data, content-addressed analyses, pinned and least-privileged CI) alongside four absences (no identity, no audit, no encryption at rest, no classification) from which every serious finding follows. It records the design provenance - Joshua Jewell, a leading Royal Veterinary College ethicist and welfare specialist, formerly of the BIOCEV laboratory in Prague with Charles University, an associate of Vladimír Hampl - and why that habit produced immutable, hashed, checksum-verified outputs before it produced identity and audit: ethics and welfare review is the discipline of asking who bears the risk of a decision, who cannot consent to it, and what evidence will survive scrutiny. It also carries a risk table in the terms each owner uses (research office, DPO, research integrity, IT security, KEC partner), the four-rung ladder so that a partial delivery is still a truthful claim, the friction budget (zero added user actions), out-of-scope items, and six questions for the owner. Renames docs/security/ultraplan.md to ultraplan-secure-private.md and says why: 'secure' is the confidentiality, integrity and availability work, 'private' is the data-protection work, and they are one programme because they share the choke point where an artefact is created, labelled, recorded and released. Updates every cross-reference, publishes docs/proposals/ under the existing documentation allowlist, and links the dossier index both ways. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- .gitignore | 3 +- SECURITY.md | 2 +- .../security-and-integrity-programme.md | 339 ++++++++++++++++++ docs/security/README.md | 7 +- docs/security/colour-teams.md | 6 +- docs/security/recon-report.md | 8 +- ...traplan.md => ultraplan-secure-private.md} | 15 +- 7 files changed, 366 insertions(+), 14 deletions(-) create mode 100644 docs/proposals/security-and-integrity-programme.md rename docs/security/{ultraplan.md => ultraplan-secure-private.md} (97%) diff --git a/.gitignore b/.gitignore index f780b9f6..91dc26a7 100644 --- a/.gitignore +++ b/.gitignore @@ -248,12 +248,13 @@ vignettes/*.pdf .Renviron ## Documentation kept local except published release, cladistics, format, maintenance, -## statistics, schema notes, and the published security dossier +## statistics, schema notes, the published security dossier, and accepted proposals docs/* !docs/release-notes/ !docs/cladistics/ !docs/format-migration.md !docs/maintenance/ +!docs/proposals/ !docs/security/ !docs/statistics/ !docs/schemas/ diff --git a/SECURITY.md b/SECURITY.md index 3b625096..01dbbc71 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -50,7 +50,7 @@ handling, and the deployment assumptions: findings with file-and-line evidence, a mapping against GDPR, the HIPAA Security Rule and the UK Government Security Classifications TOP SECRET (Tier 3) baseline behaviours, a CyBOK-referenced colour-team exercise, and a phased plan -(`docs/security/ultraplan.md`) with a zero-friction rule: each exposure is +(`docs/security/ultraplan-secure-private.md`) with a zero-friction rule: each exposure is eliminated, substituted or notified, never pushed back onto the user as an extra step. diff --git a/docs/proposals/security-and-integrity-programme.md b/docs/proposals/security-and-integrity-programme.md new file mode 100644 index 00000000..ed706b85 --- /dev/null +++ b/docs/proposals/security-and-integrity-programme.md @@ -0,0 +1,339 @@ + + + +# Proposal: a security and integrity programme for institutional and commercial use + +Status: proposal for decision. Written against `d6e8671` on `main`. +The evidence behind every claim below is in +[the recon report](../security/recon-report.md); the delivery plan is +[the secure-and-private ultraplan](../security/ultraplan-secure-private.md); +the adversarial read is [the colour-team assessment](../security/colour-teams.md). + +--- + +## Summary + +MetaManifold is a research tool for amplicon metabarcoding, and it is a good one: +the statistics are exact rather than approximate, the provenance is +content-addressed, and the path-traversal defences are better than most +commercial products. That is precisely why the gap this proposal addresses is +worth writing down. + +**It has no identity, no audit trail, no encryption at rest, and no way to mark +what it produces.** Any process on the host can delete a study. Any request that +reaches the port can rewrite the reference database the science then depends on. +Any export leaves unlabelled. Those are not statistical defects and no amount of +methodological review will surface them — which is exactly why they survive in +research software for years, in this lab and in a hundred others. + +This proposal asks for a decision on a bounded programme that closes them, +without adding a single step to the analyst's working day. It is written for the +people who carry the institutional risk rather than the ones who run the +pipeline: research office, data protection officer, research integrity officer, +IT security, and the industrial or public-sector partner performing due +diligence before a knowledge-exchange agreement. + +--- + +## 1. The blind spot this addresses + +Research software is reviewed at the level of detail that its authors care +about, and that is nearly always the method. This repository shows the pattern +in its own issue history: proposals here argue about tie handling in a rank test, +parity tolerances against an R oracle, and whether dispersion shrinkage is +stable at small *n*. Those arguments are correct to have. They are also +unrepresentative of what an institution actually needs to be able to say about +the tool afterwards. + +The same community that will spend three weeks on a dispersion prior will run +the resulting table through software that cannot tell you who deleted last +month's run, whether the export carried a patient code, or whether the reference +database changed between two analyses of the same samples. + +The asymmetry is structural, and it is not a criticism of any one project: + +| What a methods review examines | What a reputational, regulatory or contractual review examines | +| --- | --- | +| Estimator correctness, parity with a reference implementation | Who could have altered the result, and can that be shown? | +| Tolerances, convergence, edge cases | Where the data lives, who can read it, what protects it at rest | +| Reproducibility of the numbers | Reproducibility of the *decision*: approvals, releases, deletions | +| Software licence and citation | Handling, retention, transfer, and deletion with evidence | +| Test coverage of the algorithm | Test coverage of the authorisation boundary | + +Everything in the left column is done well here. Everything in the right column +is, at `d6e8671`, absent — and absent in the same way in most tools a university +would deploy tomorrow. + +The point of this proposal is not that MetaManifold is unusually unsafe. It is +that MetaManifold is unusually *well placed* to fix the pattern, because the +engineering discipline needed is already demonstrated in the parts of the +codebase nobody outside the project will ever look at. + +--- + +## 2. What is actually there, and what is actually missing + +This is the paragraph to read if you read only one. It is measured, not +aspirational: each row can be reproduced with a command in the recon report's +evidence log. + +**Present and sound** (twelve controls, all cited to a file and line in the +report): entity-name validation before any name reaches a path; `realpath` +containment on every file-serving route; an Origin allowlist that also defeats +DNS rebinding, with unit tests for the refusal cases; Oxygen's docs and metrics +disabled; parameterised SQL with an explicit identifier guard; shell-quoting +plus metacharacter gating plus remote-target validation; a strict AST parser +that refuses arbitrary call expressions in R formulas; upload size caps and a +sandboxing CSP on uploaded bytes; SHA-256 and size verification of every +reference database; content-addressed analyses that refuse a tampered record; +secret hygiene with a redaction path; and CI with SHA-pinned actions, least +privilege, full-history secret scanning and a signed-commit policy. + +**Absent** (four absences, from which every serious finding follows): + +| Absence | The one-line consequence | Grep that proves it | +| --- | --- | --- | +| No identity | any process, page or person that reaches the port has the operator's full authority, including deletion | `grep -rniE 'authorization\|session\|login' src/` finds only the outbound Zenodo client | +| No audit trail | after a deletion there is no record of what, by whom, when, or whether it worked | `access_log=nothing` at `src/server/server.jl:498`; deletion is recursive `rm` | +| No encryption at rest | a shared host, a backup, a sync client or a stolen disk yields the whole dataset | `grep -rniE 'encrypt\|cipher' src/` → no matches | +| No classification model | every artefact the tool emits is unmarked and unscoped | `grep -rniE 'top secret\|need.to.know\|disseminat' src/ config/ frontend/src/` → no matches | + +Two further findings matter to a partner's questionnaire rather than to a +statistician, and both are one-request exploits rather than theoretical: +**`POST …/doi/publish` will publish study files to Zenodo** once the deployment +enables DOIs, with `confirm: true` as the only gate; and +**`PUT /api/v1/databases` accepts an `http://` URI with no checksum**, after +which the pipeline classifies against content chosen by whoever made that +request. + +--- + +## 3. Who this is for, and what each of them asks + +| Audience | The question they actually ask | What answers it here | +| --- | --- | --- | +| Research office / faculty | "If this goes wrong, can we show we took it seriously before it did?" | Audit ledger, two-phase delete, published dossier, DPIA input | +| Data protection officer | "Lawful basis, minimisation, retention, subject rights, breach reconstruction in 72 hours?" | Privacy workstream, retention policy, DSAR and erasure evidence | +| Research integrity officer | "Can a result be attributed and shown unaltered?" | Content-addressed records (present) + actor attribution (absent) | +| IT security | "Where does it run, what protects it, what leaves it?" | Bind refusal, transport position, egress gates, at-rest position | +| Industry / KEC partner | "Encryption, access control, audit, deletion certification, sub-processors?" | The same table, answerable from repository artefacts | +| Ethics committee | "Who bears the risk of this data, and is that documented?" | Classification attribute, marking, DPIA, handling policy | +| Funder / journal | "Is the analysis reproducible and the material handled correctly?" | Provenance (present) + marking and scrubber (absent) | + +That last column is the whole proposal in one sentence: **it should be possible +to answer a partner's security questionnaire from the repository, not from the +memory of whoever set the machine up.** + +--- + +## 4. What is proposed + +Four commitments, each substituting a mechanism rather than adding a task: + +| Absence | Commitment | Mechanism | Added user actions | +| --- | --- | --- | --- | +| No identity | Every request has a recorded principal, asserted by the local proxy or the operating system — never a password database maintained by this project | One middleware plus a per-study access file | **0** | +| No audit | Every state change and every release of material is written to an append-only, hash-chained ledger | New `src/core/audit.jl` reusing the canonical-JSON and SHA-256 primitives already in the tree | **0** | +| No encryption at rest | Data sits inside a whole-disk-encrypted volume with restrictive modes; anything that leaves is envelope-encrypted | Filesystem substitution plus creation modes | **0** (once, at install) | +| No classification | Every artefact carries its classification, a copy number, a dissemination list and a manifest | One export choke point plus an inherited attribute | **≤2 decisions per study, ever** | + +Deletion becomes two-phase: the item disappears from the interface immediately, +exactly as today, and its removal is recorded and recoverable before it is +purged. That single change satisfies three interested parties at once — the +adversary needs two recorded requests, the auditor gets an event, and the +analyst gets no new dialog to dismiss. + +### The ladder, so that partial delivery is still a truthful claim + +| Rung | What an institution may then say | Timeframe | +| --- | --- | --- | +| **R1** | "Safe to hold OFFICIAL-tier research data on a managed workstation; deletions and releases are recorded" | First two workstreams | +| **R2** | "Safe to hold sensitive personal data; identity, retention and subject rights are evidenced" | Adds privacy and identity | +| **R3** | "Candidate for deployment inside an accredited SECRET environment; exports are marked and releases are gated" | Adds classification and assurance | +| **R4** | TOP SECRET | **Not offered here.** It is an accreditation of a system inside an accredited environment, not a property of an application | + +No rung above R1 is claimed by this proposal, and none should be cited in a +partner conversation until its exit criteria are signed off. + +--- + +## 5. Where the design comes from + +MetaManifold was designed by **Joshua Jewell**, a leading Royal Veterinary +College ethicist and welfare specialist, formerly a member of the **BIOCEV** +laboratory in Prague, Czechia, with **Charles University**, and an associate of +**Vladimír Hampl**. + +That provenance is not decoration, and it explains the shape of the tool. Ethics +and animal-welfare review is a discipline built around three questions that map +directly onto data governance: *who bears the risk of this decision, who cannot +consent to it, and what evidence will I be able to show that I weighed it +properly?* The same habit produces a tool whose outputs are content-addressed +and immutable, whose configuration is hashed so that a silent change invalidates +the downstream stages, whose reference databases are checksum-verified before +and after download, and whose publication path is disabled unless deliberately +configured. + +The instinct was right from the outset; what was missing was the part of the +field that has no welfare tradition to borrow from — identity, audit and +encryption — and the vocabulary of institutional risk. This programme supplies +that without asking the author to become a security vendor, and without asking +users to change how they work. + +Practically, that means the tool was built for **proper ethical and commercial +use from the beginning**, not retrofitted with a compliance veneer: the licence +is AGPLv3 for code and CC-BY-SA for documentation, the citation file is +maintained, the provenance record is designed to be published, and the +deployment posture is local-first and offline-capable rather than +cloud-dependent. Institutions should still take their own advice on AGPLv3 +obligations; this proposal does not change the licence. + +--- + +## 6. Integrity (measured, not aspirational) + +Following the house convention for proposals in this project: state what is +verified, state what is *not* there, and do not dress a plan as a result. + +**Verified today.** The twelve controls in §2, each with a file-and-line +reference and a reproducing command. Two of them — the Origin allowlist and the +DNS-rebinding refusal — have regression tests that assert the refusal cases, not +just the happy path. + +**Not present today.** The four absences, the two one-request exploits, and the +supporting evidence artefacts an assessor expects (threat model, DPIA, retention +schedule, incident procedure, classification policy). None of these exists in +the repository at `d6e8671`. + +**Not claimed.** No penetration test has been performed. The review behind this +proposal is *static*: it was produced without a Julia, R or bun runtime, on a +shallow clone, and no request was ever sent to a running server. Two findings +are explicitly reasoning-only until tests exist. A green CI run is not a claim +that a security finding is resolved, and the repository already says so in +`SECURITY.md`. Any statement that the tool is "GDPR compliant", "HIPAA +compliant" or "TOP SECRET ready" today would be false, and would be the first +thing a diligent partner dismantles. + +**What the programme would make verifiable.** For each rung, a named command +whose result is retained in CI and in an artifact, so that the claim is +reproducible by the institution rather than trusted by it. + +--- + +## 7. The risk, in the terms the institution owns it + +| Risk | Who owns it | Answer at `d6e8671` | After W1 | After W2 | After W4 | +| --- | --- | --- | --- | --- | --- | +| A result cannot be attributed to a person or a moment | Principal investigator, research integrity | none | none | ledger records the actor and the artefact hashes | same, plus classification context | +| Data deleted by mistake, or by someone who should not have been able to | Group lead, research office | unrecoverable, unattributable | unchanged | quarantine + recorded purge | unchanged | +| Personal data on a shared or stolen machine | DPO, institution as controller | plaintext, world-readable by default | restrictive modes | FDE position recorded | unchanged | +| A subject access request or an erasure request | DPO | no mechanism, no evidence | unchanged | DSAR manifest + erasure evidence | same | +| Material released to a third country platform in error | Research office, contracts | one request, irreversible | one request, irreversible | recorded, still irreversible | gated on classification and recorded | +| A partner's security questionnaire | KEC / commercial team | unanswerable | answerable in part | mostly answerable | answerable with evidence | +| Reference data silently substituted | Research integrity | possible in one request | refused unless https + checksum | logged | logged | +| Denial of service during a deadline | Group lead | trivially achievable | capped | capped and recorded | same | +| Handling instructions missing from an exported table | Whoever receives it | nothing on the artefact | unchanged | unchanged | marking, copy number, manifest | + +The last row is the one that travels. A spreadsheet leaves the institution with +a name on it; a table exported by this tool currently leaves with nothing. No +policy document survives contact with a shared drive, but a banner in cell A1 +does. + +--- + +## 8. Cost, sequence, and friction + +- **Scope:** eight workstreams, eighteen revertible pull requests. Two of them + (headers, size caps) remove six live attack paths and touch no analysis code. +- **Effort:** four small, three medium, one large. The large one is identity, + because it changes every route's assumptions; it is the only item with real + regression risk, and it is sequenced after the ledger so that its records are + meaningful when it lands. +- **Friction:** zero added user actions across the whole programme, with at most + two one-off decisions per study. The plan rejects confirmation dialogs, + per-session passwords and per-export classification dropdowns on human-factors + grounds, and substitutes mechanisms that cost nothing in the routine path. +- **Invariants preserved:** no change to the numeric or statistical logic; the + same outputs byte-for-byte for the same inputs; no new TypeScript application + logic; JCS/I-JSON at the boundary; new configuration as KYAML. The + repository's own manifest rules stand. +- **First step:** one pull request. Security headers on every response, request + and job caps, refusal to bind a non-loopback address without documented TLS + termination, and hard refusals in the database-document route. It is small, it + is additive, it cannot regress the science, and it changes the answer to "may + this box sit on a lab network?" from *no* to *it depends*. + +--- + +## 9. Out of scope + +- Any claim of certification, accreditation or compliance for any rung. +- A rewrite in another language, or any change to the analytical methods. +- A home-grown identity provider, password store or cryptographic construction. +- Physical, personnel and network-accreditation controls, which belong to the + host institution and cannot be evidenced from a repository. +- TOP SECRET guidance held at a higher classification. The Tier 3 material is + available from the Government Security Group through an organisation's own + security team, with the NCSC consulted where there is a business need; this + project neither holds nor reproduces it. + +--- + +## 10. Questions for the owner + +1. **Which rung is the target for the next release** — R1 (safe on a managed + workstation, deletions and releases recorded) or straight to R2 (sensitive + personal data)? R1 is two workstreams and no route-level risk; R2 is the full + identity and privacy programme. +2. **Should this be published as a "Proposal:" issue** in the project's own form + before any code lands? The repository's convention is that an architectural + decision is discussed in an issue first, and this one is architectural. +3. **Who owns the DPIA** — the deploying institution as controller, or this + project as maintainer? Either way `SECURITY.md` should say which, because a + partner will ask on page one. +4. **Which classification vocabulary should the attribute carry?** UK Government + Security Classifications (OFFICIAL / OFFICIAL-SENSITIVE / SECRET / TOP + SECRET) is the right shape for UK institutions and for the Tier 3 behaviours + in the report, but most users are not UK government. A small institution + profile in configuration may be better than a hard-coded scheme. +5. **Is the name "secure and private" right for outward-facing material**, or + should the programme be described as "research integrity and assurance", + which is the term a research office is more likely to fund? +6. **May the author's affiliations be stated** as in §5 in any published + version, with the wording the author prefers? + +--- + +## 11. Where to read the detail + +| Document | What it contains | +| --- | --- | +| [recon report](../security/recon-report.md) | Seventeen findings with file-and-line evidence, the twelve existing controls, GDPR article-by-article, HIPAA specification-by-specification, and all 33 TOP SECRET behaviours classified as application-level, inherited from the environment, or with no software expression; plus a twenty-command evidence log and an explicit statement of what the review could not run | +| [ultraplan — secure and private](../security/ultraplan-secure-private.md) | The eight workstreams and eighteen pull requests, each with its files, ledger events, acceptance tests, standards clauses closed and friction cost; the four-rung ladder; and the list of controls deliberately *not* built | +| [colour-team assessment](../security/colour-teams.md) | White, red, blue, purple, green and gold teams over CyBOK knowledge areas; twelve red scenarios with preconditions; a detection matrix showing eight of ten red scenarios would be invisible today; and the conflicts between teams with how each resolves | +| [security dossier index](../security/README.md) | The reading order and the three sentences that matter | + +--- + +## 12. How this will be judged + +Three tests, all falsifiable: + +1. **The questionnaire test.** A partner's standard security schedule should be + answerable from repository artefacts, with a command and a result, without + interviewing the operator. +2. **The DPO test.** A data protection officer should be able to read the DPIA + input, the retention policy and a DSAR manifest, and identify in each one the + evidence the regulation actually requires. +3. **The analyst test.** An analyst who works exactly as they do today should + notice no difference in their day, except that a mistake is recoverable and a + release is recorded. + +If the third test fails, the programme has failed regardless of the first two. +That is why the friction budget is a hard constraint and not a preference. + +--- + +*This proposal is an argument about governance, not a claim about compliance. +It is written to be challenged: if a finding here is wrong, the command that +proves it wrong belongs in the same pull request as the correction.* diff --git a/docs/security/README.md b/docs/security/README.md index 96aa2577..6fe40e78 100644 --- a/docs/security/README.md +++ b/docs/security/README.md @@ -11,14 +11,15 @@ sensitive material in the country. | --- | --- | | [`recon-report.md`](recon-report.md) | What the system is, what protects it today, what does not, and how that scores against GDPR, the HIPAA Security Rule, and the UK TOP SECRET (Tier 3) baseline behaviours. Every finding cites a file and line, and every absence claim has the grep that proves it. | | [`colour-teams.md`](colour-teams.md) | The CyBOK-referenced colour-team exercise: white (governance), red (adversary), blue (detection), purple (validation), green (build/crypto), gold (human/insider) — including where the teams disagree and how each conflict resolves. | -| [`ultraplan.md`](ultraplan.md) | The plan to close the gaps, ordered by rung on a capability ladder, under a hard **zero-friction** rule (eliminate, substitute, or notify — never "ask the user to be careful"). Each workstream names its files, its ledger events, its acceptance tests and its PR decomposition. | +| [`../proposals/security-and-integrity-programme.md`](../proposals/security-and-integrity-programme.md) | The case for doing any of this at all, written for the people who carry institutional risk rather than the ones who run the pipeline: research office, DPO, research integrity, IT security, and an industry or KEC partner doing due diligence. | +| [`ultraplan-secure-private.md`](ultraplan-secure-private.md) | The plan to close the gaps, ordered by rung on a capability ladder, under a hard **zero-friction** rule (eliminate, substitute, or notify — never "ask the user to be careful"). Each workstream names its files, its ledger events, its acceptance tests and its PR decomposition. | ## Read in this order 1. `recon-report.md` §1 (executive summary) and §5 (standards matrices). 2. `colour-teams.md` §7 (conflicts) and §8 (heat map) — this is the argument for the plan's ordering. -3. `ultraplan.md` §1 (the four rules) and §5 (PR order). +3. `ultraplan-secure-private.md` §1 (the four rules) and §5 (PR order). 4. `recon-report.md` §9 to re-run the evidence yourself. ## The three sentences that matter @@ -29,7 +30,7 @@ sensitive material in the country. 2. **TOP SECRET is an accreditation of a system, not a property of an application.** The achievable statement today is a *rung* on a ladder, not a classification. -3. The first PR (`ultraplan.md` W1) removes six live attack paths with security +3. The first PR (`ultraplan-secure-private.md` W1) removes six live attack paths with security headers, size caps and refusals, adds no user friction, and does not touch a single line of analysis code. diff --git a/docs/security/colour-teams.md b/docs/security/colour-teams.md index 993d232c..cbee6c19 100644 --- a/docs/security/colour-teams.md +++ b/docs/security/colour-teams.md @@ -4,7 +4,7 @@ # CyBOK-referenced colour-team exercise Companion to [`recon-report.md`](recon-report.md) and -[`ultraplan.md`](ultraplan.md). Finding IDs (`SEC-nn`) refer to the recon +[`ultraplan-secure-private.md`](ultraplan-secure-private.md). Finding IDs (`SEC-nn`) refer to the recon report. ## 0. What this is, and what it is not @@ -37,7 +37,7 @@ Scoring: **C**ritical / **H**igh / **M**edium / **L**ow / **P**ass. Rules of engagement for this simulation: no exploitation, no live system, no data. Every red-team result is a source-level reasoning step that a future test -must confirm (see `ultraplan.md` W7). +must confirm (see `ultraplan-secure-private.md` W7). --- @@ -223,7 +223,7 @@ insider paths. | Y-02 | **Copy-and-send is the real dissemination path.** A publication table exported as XLSX and emailed is the TOP SECRET "verbal/hard-copy" failure in modern dress, and the export carries no marking, no copy number, no handling instruction. Marking is the single highest-leverage *human* control available, and it is free. | C for target | export routes | | Y-03 | **No "who am I" affordance.** With no identity, an operator cannot tell whether an action will be attributed later. Attribution pressure is itself a control (AAA). | M | — | | Y-04 | **Insider with legitimate access is indistinguishable from an attacker** — same routes, same authority, no anomaly baseline. | H | — | -| Y-05 | **Friction risk in the obvious fixes.** The instinctive responses (a confirm dialog on delete, a password per session, a classification dropdown on every export) all add recurring friction and would be ignored within a week — click-through blindness is a documented HF failure mode. | — | vetoed in `ultraplan.md` §2 | +| Y-05 | **Friction risk in the obvious fixes.** The instinctive responses (a confirm dialog on delete, a password per session, a classification dropdown on every export) all add recurring friction and would be ignored within a week — click-through blindness is a documented HF failure mode. | — | vetoed in `ultraplan-secure-private.md` §2 | | Y-06 | **The TOP SECRET "chair declares the classification at the start" behaviour has a software analogue that costs nothing**: a persistent classification banner in the UI and on every artefact. It does the chair's job without a prompt. | — | mapping to TS-01/TS-32 | ### 6.2 Gold's veto list (do not build) diff --git a/docs/security/recon-report.md b/docs/security/recon-report.md index 1787e6fe..0b885a6c 100644 --- a/docs/security/recon-report.md +++ b/docs/security/recon-report.md @@ -10,7 +10,7 @@ claims a control is "met" unless the evidence for it is on disk and cited. Companion documents: -- [`ultraplan.md`](ultraplan.md) — the plan to close what this report finds. +- [`ultraplan-secure-private.md`](ultraplan-secure-private.md) — the plan to close what this report finds. - [`colour-teams.md`](colour-teams.md) — the CyBOK-referenced colour-team exercise (white, red, blue, purple, green, gold). @@ -99,7 +99,7 @@ reference-database download, content-addressed analysis records that refuse tampering, SHA-pinned GitHub Actions with least privilege, and a sha256-verified installer. §3 lists all twelve with line references. -The plan in `ultraplan.md` therefore **adds** layers. It does not touch the +The plan in `ultraplan-secure-private.md` therefore **adds** layers. It does not touch the scientific code, the numeric contracts, or the parts of the request pipeline that this review found sound. @@ -681,7 +681,7 @@ threat model, DPIA skeleton, incident procedure and classification policy. | 45 CFR | Spec | Type | Current state | Finding | | --- | --- | --- | --- | --- | | §164.308(a)(1)(ii)(A) | Risk analysis | Required | This document is the first one | SEC-17 | -| §164.308(a)(1)(ii)(B) | Risk management | Required | `ultraplan.md` | — | +| §164.308(a)(1)(ii)(B) | Risk management | Required | `ultraplan-secure-private.md` | — | | §164.308(a)(3) | Workforce security / sanction | Required | No workforce model exists | SEC-01 | | §164.308(a)(4) | Information access management | Required | No authorisation | SEC-01 | | §164.308(a)(5) | Security awareness / login monitoring | Required | No login exists | SEC-01 | @@ -774,7 +774,7 @@ not a property of an application. A Julia + R + DuckDB + browser stack with dozens of third-party packages, built by an unvetted toolchain, running on a general-purpose OS, cannot be *asserted* to the TOP SECRET standard by its own code. What the code can do is stop being the reason an accreditation fails. The -realistic ladder is in `ultraplan.md` §3. +realistic ladder is in `ultraplan-secure-private.md` §3. --- diff --git a/docs/security/ultraplan.md b/docs/security/ultraplan-secure-private.md similarity index 97% rename from docs/security/ultraplan.md rename to docs/security/ultraplan-secure-private.md index c756d545..59686c28 100644 --- a/docs/security/ultraplan.md +++ b/docs/security/ultraplan-secure-private.md @@ -1,7 +1,7 @@ -# Ultraplan: from local analysis tool to classification-ready appliance +# Ultraplan — secure and private: from local analysis tool to classification-ready appliance Status: plan and evidence map for the branch `arena/924008e1-metamanifold-webui`, based on `d6e8671` (`main`). @@ -9,6 +9,17 @@ Companion to [`recon-report.md`](recon-report.md) (what is wrong and why it matters) and [`colour-teams.md`](colour-teams.md) (the adversarial and governance read). Finding IDs `SEC-nn` refer to the recon report. +**Why the name.** *Secure* is the confidentiality, integrity and availability +work: identity, authorisation, the audit ledger, transport, at-rest protection, +resource limits. *Private* is the data-protection work: minimisation at ingest, +retention, subject rights, the export scrubber, and a deployment posture that is +private by default rather than private by configuration. They are one programme +because they share one choke point — the point at which an artefact is created, +labelled, recorded and released — but they are separable in review, and the +rungs in §3 keep them separable in delivery. Where a commitment exists to hold +data that could identify a person *or* to be used by more than one person, both +halves apply. + **Nothing in this plan is applied.** No source file was modified by this session; the branch adds documentation only. No test was run: the review environment has no Julia, R or bun (see `recon-report.md` §0.3). @@ -122,7 +133,7 @@ event(s) added to the ledger, and the friction cost. **Objective.** An assessor can start work without interviewing anyone. **Changes.** `docs/security/README.md`, `recon-report.md`, `colour-teams.md`, -`ultraplan.md`; a pointer section in `SECURITY.md`; optionally a line in +`ultraplan-secure-private.md`; a pointer section in `SECURITY.md`; optionally a line in `0-AI-MANIFEST.a2ml`'s `evidence` block naming the security dossier. **Acceptance.** `docs/security/` exists, the evidence log in From 6b40da0f4aad051e30407971c6998bb9d0bdd26b Mon Sep 17 00:00:00 2001 From: Arena Agent Date: Sun, 25 Oct 2026 23:02:09 +0000 Subject: [PATCH 3/3] docs(remedy): add the remedy ultraplan and link it from the programme documents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds docs/proposals/security-and-integrity-ultraplan.md: the proposal converted into a remedy schedule. Where the secure-and-private ultraplan answers 'how do we build it', this answers 'what harm is removed, for whom, what will we be able to prove, and who signs for it'. Contents: - A twenty-entry remedy register. R-01..R-17 close the recon report's findings, R-18..R-20 are the institutional obligations no code can discharge (DPIA and record of processing, briefing records, transfer position and sub-processor register) and that would otherwise be quietly dropped. Each row carries the harm in the words an institution would use, the causing finding, the disposition (eliminate, substitute, notify, transfer, or accept-recorded), the remedy, the earliest gate, and the evidence artefact. - Nine remedy packages RP-0..RP-8, independently fundable and abandonable, each with the harms it closes, the engineering workstream it draws on, its owner (PI, RO, DPO, RI, ITS, KEC), its exit test and the decision it needs. - Three signable gates plus an explicit refusal of a fourth. Each gate states what may be claimed and, in the same table, what may not — the pressure to over-claim during a KEC negotiation is the risk this table exists for. Gate withdrawal is defined and must happen in the pull request that weakens the control it depended on. - A twelve-question partner answer pack giving the permitted answer at each rung, because five of the twelve are the ones that stop agreements. - A KEC and commercial annex covering licence (AGPLv3 stated rather than discovered), controller/processor position, transfer mechanism, sub-processors, export control, publication rights, liability and support expectations. - The register as KYAML data with a CI governance check in the style of scripts/check-governance-contract.sh, so a remedy cannot be marked verified without the command that proves it. - Metrics chosen because they can be wrong visibly, including gate withdrawals: a gate that never withdraws was never real. - Stop/go, partial-claim integrity, rollback, the risks of the remedy itself (compliance theatre, over-claim, scope creep into the science, sole-maintainer burnout), nine decisions needed to start — three of which a delivery plan never asks — and five conditions under which this plan is wrong. Links the remedy plan from the proposal, the dossier index and the engineering ultraplan, and splits the dossier's reading order into a decision path and an implementation path. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- .../security-and-integrity-programme.md | 6 +- .../security-and-integrity-ultraplan.md | 644 ++++++++++++++++++ docs/security/README.md | 12 +- docs/security/ultraplan-secure-private.md | 7 +- 4 files changed, 665 insertions(+), 4 deletions(-) create mode 100644 docs/proposals/security-and-integrity-ultraplan.md diff --git a/docs/proposals/security-and-integrity-programme.md b/docs/proposals/security-and-integrity-programme.md index ed706b85..4430338f 100644 --- a/docs/proposals/security-and-integrity-programme.md +++ b/docs/proposals/security-and-integrity-programme.md @@ -7,7 +7,10 @@ Status: proposal for decision. Written against `d6e8671` on `main`. The evidence behind every claim below is in [the recon report](../security/recon-report.md); the delivery plan is [the secure-and-private ultraplan](../security/ultraplan-secure-private.md); -the adversarial read is [the colour-team assessment](../security/colour-teams.md). +the adversarial read is [the colour-team assessment](../security/colour-teams.md); +and the remedy schedule — which harm each control removes, who signs for it, and +what may be claimed at each gate — is +[the remedy ultraplan](security-and-integrity-ultraplan.md). --- @@ -311,6 +314,7 @@ does. | [recon report](../security/recon-report.md) | Seventeen findings with file-and-line evidence, the twelve existing controls, GDPR article-by-article, HIPAA specification-by-specification, and all 33 TOP SECRET behaviours classified as application-level, inherited from the environment, or with no software expression; plus a twenty-command evidence log and an explicit statement of what the review could not run | | [ultraplan — secure and private](../security/ultraplan-secure-private.md) | The eight workstreams and eighteen pull requests, each with its files, ledger events, acceptance tests, standards clauses closed and friction cost; the four-rung ladder; and the list of controls deliberately *not* built | | [colour-team assessment](../security/colour-teams.md) | White, red, blue, purple, green and gold teams over CyBOK knowledge areas; twelve red scenarios with preconditions; a detection matrix showing eight of ten red scenarios would be invisible today; and the conflicts between teams with how each resolves | +| [remedy ultraplan](security-and-integrity-ultraplan.md) | The remedy schedule: twenty remedies keyed to the findings, nine packages, three signable gates with what may and may not be said at each, a twelve-question partner answer pack, the KEC and commercial annex, and the decisions needed to start | | [security dossier index](../security/README.md) | The reading order and the three sentences that matter | --- diff --git a/docs/proposals/security-and-integrity-ultraplan.md b/docs/proposals/security-and-integrity-ultraplan.md new file mode 100644 index 00000000..75a76c7e --- /dev/null +++ b/docs/proposals/security-and-integrity-ultraplan.md @@ -0,0 +1,644 @@ + + + +# Ultraplan — remedy: closing the institutional security and integrity gaps + +Status: remedy plan for decision. Written against `d6e8671` on `main`. +Companion to [the proposal](../proposals/security-and-integrity-programme.md) +(why this matters, in institutional terms), [the recon report](../security/recon-report.md) +(what is wrong, with evidence), [the secure-and-private ultraplan](../security/ultraplan-secure-private.md) +(what gets built, in what order) and [the colour-team assessment](../security/colour-teams.md) +(what an adversary and a defender each conclude). + +Finding IDs `SEC-nn` are the recon report's. Work packages here are `RP-n`; +they are **not** the engineering workstreams `W0`–`W8` — §0.2 explains the split +and why both are needed. + +**Nothing here is implemented.** The branch adds documentation; no source file +is changed. No test was run: the review environment has no Julia, R or bun +(`recon-report.md` §0.3). + +--- + +## 0. What a remedy plan is, and what it is not + +### 0.1 The three plans, one programme + +A gap can be closed three ways and each one needs a different document, because +each one is read by a different person and answers a different tense: + +| Document | Tense | Reader | Produces | +| --- | --- | --- | --- | +| [Proposal](../proposals/security-and-integrity-programme.md) | *Should* we | Research office, DPO, KEC partner | A decision to act, and the language to justify it | +| [Secure-and-private ultraplan](../security/ultraplan-secure-private.md) | *How* we build it | Engineer, reviewer | Eighteen pull requests with tests | +| **This remedy plan** | *What harm is removed, for whom, and what will we be able to prove* | Risk owner, auditor, assessor, partner | A register, gates, evidence, and signature points | + +A delivery plan says "add an audit ledger". A remedy plan says "the harm is an +unattributable deletion; the remedy is a ledger plus recovery; the owner is the +research office; the evidence is a chain that verifies; the claim becomes +sayable at gate R1; and if we do not do it, this is the residual risk someone +must sign." + +The engineering plan without this document builds controls nobody has agreed to +own. This document without the engineering plan is a promise. They ship together +or not at all. + +### 0.2 What "remedy" changes about the analysis + +An engineering plan starts from the defect and works forward: *no identity → +add identity*. A remedy plan starts from the harm and works backward: *a result +cannot be attributed → which defect causes that → what removes it → who carries +it today → what evidence closes it → who signs.* + +That reversal matters because **the same defect maps to different harms for +different readers**, and the harms, not the defects, are what get funded: + +| Defect | Engineering statement | Remedy statement (institutional) | +| --- | --- | --- | +| SEC-01 no identity | "Add a principal middleware" | "A finding cannot be attributed to a person, so the institution cannot defend a misconduct allegation or a challenge to a published result" | +| SEC-02 no audit | "Add a hash-chained ledger" | "A subject access request cannot be answered, a breach cannot be reconstructed inside 72 hours, and a deletion cannot be evidenced" | +| SEC-03 no encryption at rest | "Set 0700 and attest FDE" | "A stolen laptop or a shared lab machine is a reportable personal data breach with no mitigating control to point to" | +| SEC-04 no classification | "Add an inherited attribute and a choke point" | "Material leaves the institution with nothing on it, so no handling instruction survives the first email" | + +### 0.3 The rules that carry over, unchanged + +1. **Dispose of every harm by eliminate → substitute → notify**, in that order. + Two dispositions are added here, because an institution always has them and + pretending otherwise is how plans lose credibility: **transfer** (contract or + insurance) and **accept, recorded and signed**. Acceptance is legitimate. An + *unrecorded* acceptance is the failure mode this document exists to prevent. +2. **Zero recurring friction.** Any remedy that adds a step to the analyst's day + is rejected in favour of a substitute. The budget is stated in the proposal: + zero added actions, at most two one-off decisions per study. +3. **Never claim above the last signed gate.** §4 defines what may be said at + each rung and, explicitly, what may not. +4. **Do not disturb the science.** Same inputs, same bytes out. Every remedy + work package inherits that test. + +--- + +## 1. The remedy register + +Twenty remedies. Each is a harm, not a feature: **R-01 to R-17** close the recon +report's findings, **R-18 to R-20** are institutional obligations that no code +change can discharge and that would otherwise be quietly dropped. + +Columns: *class* is the disposition (E/S/N/T/A); *rung* is the earliest gate at +which the harm is materially reduced; *evidence* is the artefact an assessor +will ask for. + +### 1.1 Technical harms (closed by code) + +| ID | Harm, as an institution would state it | Cause | Class | Remedy | Rung | Evidence | +| --- | --- | --- | --- | --- | --- | --- | +| R-01 | A result cannot be attributed to a person | SEC-01 | S | Principal asserted by the local proxy or the OS account, recorded on every request — never a password store owned by this project | R2 | `auth.grant`/`authz.denied` ledger records; proxy configuration in the accreditation pack | +| R-02 | Anyone who reaches the port can delete a study | SEC-01, SEC-08 | E then S | Bind loopback by default and refuse an unaccredited non-loopback bind; deletion becomes two-phase with a quarantine and a recorded purge | R1 | Bind-refusal test; `delete.requested` → `purge.committed` sequence test | +| R-03 | A deletion cannot be reconstructed: not what, not by whom, not when | SEC-02 | S | Append-only, hash-chained audit ledger with a fixed event vocabulary | R1 | Chain verification test; `test_audit.jl` | +| R-04 | A breach cannot be investigated because nothing is logged | SEC-02 | E | Structured access log replaces `access_log=nothing`; destruction and release events are first-class | R1 | Access-log fixture asserted in `test_server_security.jl` | +| R-05 | A stolen laptop or shared lab machine discloses the whole dataset | SEC-03 | S | Whole-disk encryption attested at start; `0700`/`0600` at creation so the tree is not world-readable inside it | R2 | FDE verdict in the startup log and attestation; mode assertion via `stat` | +| R-06 | A file sent to a partner is unprotected in transit and at rest | SEC-03 | S | Envelope encryption (age/X25519 or the platform keychain) for anything leaving the tree | R3 | Export manifest records the recipient key and the ciphertext hash | +| R-07 | Material leaves the institution with nothing on it | SEC-04 | S | Inherited classification, marking, copy number, dissemination list and manifest at one export choke point | R3 | Export-bytes test per route; manifest equality test | +| R-08 | A study is published to a third-country platform in one request, irreversibly | SEC-05 | E then S | Publication gated on classification and on a recorded approval; `DOI_ENABLED` remains the master switch | R3 | `publish.refused` at SECRET; `publish.completed` with DOI and file hashes | +| R-09 | The science silently depends on reference data someone else chose | SEC-06 | S | Require `https` and a checksum whenever a URI is present; refuse an unhashed source | R1 | Route tests for `http://` and hash-less documents | +| R-10 | The service can be taken down during a deadline | SEC-07, SEC-15 | E | One body cap, one job cap, one subscriber cap, one token bucket for mutating routes | R1 | `413`, `429` tests; `limit.exceeded` ledger events | +| R-11 | Traffic is readable and injectable off loopback | SEC-09 | E | A non-loopback bind without documented TLS termination is a startup refusal, not a warning | R1 | Startup-refusal test naming both escape hatches | +| R-12 | The interface can be framed and clickjacked into destructive actions | SEC-10, SEC-11 | E | `frame-ancestors 'none'` and the full header set on every response class | R1 | Header assertions on `/`, `/api/…`, `/files/…` | +| R-13 | A refactor could re-open SQL identifier injection | SEC-12 | S | Explicit identifier validation at the route boundary, replacing defence-by-consequence | R2 | A `"`-bearing table name returns 400 | +| R-14 | Reproducibility artefacts leak usernames, hostnames and paths | SEC-13 | S | Export scrubber: paths root-relative, hostname dropped from published attestations, redaction list applied | R2 | Published attestation contains no hostname and no absolute path | +| R-15 | Personal data has no state: no flag, no purpose, no retention, no rights path | SEC-14 | E then S | Identifier scan at ingest refuses undeclared personal data; retention clock; DSAR manifest; evidenced erasure | R2 | Refusal test; DSAR manifest test; `erasure.evidenced` record | +| R-16 | Study-derived values persist in the browser on a shared machine | SEC-16 | S | Clear client-side UI state on unload and on principal change | R2 | Frontend test | +| R-17 | No assessment can begin because nothing is documented | SEC-17 | N | The dossier, plus the pack named in RP-7 | R1 | This document set | + +### 1.2 Institutional harms (no code can discharge them) + +| ID | Harm | Class | Remedy | Rung | Evidence | +| --- | --- | --- | --- | --- | --- | +| R-18 | Processing has no lawful basis, purpose or DPIA, so the institution is in breach before any defect is exploited | S (substitute a decision for an assumption) | DPIA or a signed screening decision; record of processing generated from the ledger; named controller and, if hosted for others, processor terms | R2 | Signed DPIA; ROPA export | +| R-19 | Nobody has been briefed, so the controls depend on a document nobody read | N | Role-based briefing at first use of a classified study — the software analogue of the Tier 3 requirement that users are briefed before first access and refreshed routinely | R3 | Briefing record per principal, acknowledged in the ledger | +| R-20 | A disclosure to a partner or platform has no contractual cover | T | Data transfer position documented (UK IDTA / addendum or equivalent), sub-processor list, and a KEC security schedule answered from R-01…R-17 evidence | R3 | Signed schedule; sub-processor register | + +### 1.3 Register arithmetic + +Seventeen technical remedies close all seventeen findings. Three +institutional remedies close what code cannot. **Six remedies reach R1** — they +are the ones that make the difference between "cannot be discussed with a +partner" and "can be discussed with caveats". Two are eliminate-class and cost +nothing to run (R-10, R-12); two are eliminate-class and cost one configuration +decision (R-02, R-11). + +--- + +## 2. Remedy packages + +Nine packages, sequenced so that each is independently fundable, independently +reviewable and independently abandonable. Each names: the harms it closes, the +engineering workstream it draws on, the artefacts it produces, its exit test, its +owner and the decision it needs. + +Owner abbreviations: **PI** principal investigator / maintainer · **RO** research +office · **DPO** data protection officer · **RI** research integrity · **ITS** IT +security · **KEC** knowledge-exchange / commercial team. + +--- + +### RP-0 — Decide (no engineering) + +**Closes.** Nothing. It unblocks everything. + +**Produces.** A signed decision record naming: the target rung for the next +release; the DPIA owner; the classification vocabulary; whether the author's +affiliations are stated publicly and in what wording; and whether this goes +upstream as a `Proposal:` issue before code lands. + +**Exit test.** The record exists, is dated, and names an owner for RP-1 and RP-2. +Without it, no later package starts — the most common failure of programmes like +this is beginning with the code because the decision is uncomfortable. + +**Decision needed.** Proposal §10, questions 1–6. + +--- + +### RP-1 — Contain + +**Closes.** R-02 (part), R-09, R-10, R-11, R-12. +Draws on W1.1–W1.4. + +**Why first.** These are the remedies that remove reach rather than manage it, +they touch no analysis code, and they change the answer to "may this box sit on +a lab network?" from *no* to *it depends*. They are also the cheapest possible +demonstration that the programme will actually ship. + +**Artefacts.** Header helper plus tests; one body-cap middleware; job and SSE +caps; startup refusal for an unaccredited bind; hard refusals in the database +document route. + +**Exit test.** The existing suite is green; four new tests assert the refusals; +one analysis response is byte-identical to its pre-change output. + +**Owner.** PI implements; ITS accepts the bind and transport position. + +**Rung.** Contributes R1. + +--- + +### RP-2 — Record + +**Closes.** R-02 (remainder), R-03, R-04. +Draws on W2 and W2.3. + +**Why here.** A ledger without an actor is still worth building — the `actor` +field carries `unauthenticated` from the first record — but an actor without a +ledger has nowhere to be written. Order matters and this is the order. + +**Artefacts.** `src/core/audit.jl`; ledger written by every mutating route; +structured access log; quarantine and recorded purge; a "recently deleted" +recovery list in the interface. + +**Exit test.** Chain verifies over N writes; tampering at record *k* is detected +at *k+1*; a failed ledger write aborts a delete; delete→purge ordering is +enforced; a deleted study is recoverable before purge. + +**Owner.** PI implements; RO accepts the deletion policy and the retention window. + +**Rung.** Completes R1 once RP-1 is signed. + +--- + +### RP-3 — Attribute + +**Closes.** R-01, R-16. +Draws on W3.1, W3.2, W3.4. + +**Why third.** Identity is the only package with real regression risk: it changes +every route's assumptions. It is sequenced after the ledger so that its first +records are meaningful, and it defaults to the incumbent single-user behaviour so +that a solo installation sees no change at all. + +**Artefacts.** Principal middleware (proxy assertion, peer credential, or OS +session); per-study access file; `_authorise!` called from the two helpers every +route already uses. + +**Exit test.** Default single-user policy permits and records `os-session`; a +spoofed proxy header without the trust flag is ignored; a second principal gets +403 on a study it was not granted. + +**Owner.** PI implements; ITS owns the proxy configuration; RO owns the access +grants. + +**Rung.** R2. + +--- + +### RP-4 — Protect + +**Closes.** R-05, R-06. +Draws on W3.3. + +**Artefacts.** Secure creation modes; encryption-position check at start (Linux +dm-crypt, macOS APFS crypto users, Windows warns and records); envelope +encryption for exports and backups. + +**Exit test.** A created directory is `0700`; the startup log and attestation +carry the FDE verdict; an export decrypts with the recipient key and not without. + +**Owner.** ITS owns the platform; PI implements the check and the modes. + +**Rung.** R2. + +--- + +### RP-5 — Distinguish and disseminate + +**Closes.** R-07, R-08. +Draws on W4. + +**Why this rung and not earlier.** A classification enforced against an +unauthenticated caller is theatre. It lands after RP-3 for that reason alone, and +it is the package that turns "our exports are unmarked" into "our exports carry +their handling". + +**Artefacts.** Inherited attribute with **maximum-over-cascade** semantics; +export choke point; marking, copy number, dissemination list, manifest; banner in +the interface; publication gate. + +**Exit test.** A study at OFFICIAL with a run at a higher tier exports at the +higher tier; every export route emits a banner and a manifest; publication at +SECRET is refused and recorded; a CSV above OFFICIAL carries a manifest companion +automatically rather than refusing the export. + +**Owner.** PI implements; RO sets the policy; RI confirms the marking scheme. + +**Rung.** R3. + +--- + +### RP-6 — Respect rights + +**Closes.** R-14, R-15. +Draws on W5. + +**Artefacts.** Ingest-time identifier scan; `personal_data` flag; retention clock +that lists but never deletes; DSAR manifest; evidenced erasure; export scrubber. + +**Exit test.** An identifier-shaped column with `personal_data: false` is refused; +a DSAR manifest lists the study's files, their hashes and the ledger slice; a +published attestation carries no hostname and no absolute path; erasure produces +a record whose item hash matches what was quarantined. + +**Owner.** DPO owns the policy and signs the DPIA input; PI implements. + +**Rung.** R2, completed at R3 once the scrubber covers every export. + +--- + +### RP-7 — Prove + +**Closes.** R-17 (completion) and the evidence question behind every other row. +Draws on W6, W7 and W8. + +**Artefacts.** The accreditation pack: system security policy, threat model, DPIA +input, ROPA template, incident procedure including "published to Zenodo in +error", retention and destruction policy, configuration baseline, residual risk +statement, and an evidence index in which every claim has a command and a +retained result. Plus the assurance CI lane so the index cannot rot. + +**Exit test.** A reviewer who has never spoken to the author can answer every +question in §3 of this document from repository artefacts alone. + +**Owner.** RO assembles; PI supplies commands; DPO and RI sign their sections. + +**Rung.** Enables R3 and is the precondition for any R4 conversation. + +--- + +### RP-8 — Sustain + +**Closes.** R-18, R-19, R-20. +Draws on nothing in the engineering plan, by design. + +**Artefacts.** DPIA and ROPA in force; briefing record per principal; transfer +position and sub-processor register; annual re-attestation; a named review +cadence (§7). + +**Exit test.** Every principal who has touched a classified study has a briefing +record; the ROPA matches the ledger; the transfer position is signed. + +**Owner.** DPO and RO jointly; ITS for the technical attestations. + +**Rung.** R2 for the DPIA, R3 for briefing and transfer. + +--- + +## 3. Gates: what may be claimed, by whom, when + +A gate is not a milestone; it is a **permission to say something**. The right +column is the part that keeps the programme honest under pressure. + +### R1 — Contained and recorded + +**Signed by:** PI and ITS. **Needs:** RP-1, RP-2 complete and green. + +| May say | May not say | +| --- | --- | +| "Deletions and releases are recorded and recoverable" | "Access is controlled" — it is not, until R2 | +| "The tool refuses to be exposed without TLS termination" | "The tool is secure" — no assessment has been done | +| "Reference data cannot be substituted without a checksum" | "The data is encrypted" — the platform position is unverified until R2 | +| "Denial of service by unauthenticated request is bounded" | "GDPR compliant", "HIPAA compliant", "TOP SECRET ready" | + +### R2 — Attributed and protected + +**Signed by:** PI, ITS, DPO. **Needs:** RP-3, RP-4, RP-6 (policy half), RP-8 (DPIA). + +| May say | May not say | +| --- | --- | +| "Every request is attributable to a principal" | "Need-to-know is enforced institution-wide" — only per study | +| "Data sits inside an attested encrypted volume with restrictive modes" | "Exports are safe to share" — they are unmarked until R3 | +| "A subject access request and an erasure can be evidenced" | "The institution is compliant" — that is the DPO's determination, not the tool's | +| "Personal data is declared, scanned and retained on a schedule" | "TOP SECRET candidate" — that is R3 | + +### R3 — Distinguished and provable + +**Signed by:** PI, RO, DPO, RI, and an independent reviewer. **Needs:** RP-5, RP-6 (scrubber), RP-7, RP-8 (briefing and transfer). + +| May say | May not say | +| --- | --- | +| "Every artefact carries its classification, copy number and handling" | "Accredited" — that is R4 and the environment's to give | +| "Release outside the institution is gated and recorded" | "TOP SECRET" in any form | +| "A partner's security schedule can be answered from repository evidence" | "Certified", "approved for government use", or any rung above R3 | + +### R4 — Not offered + +TOP SECRET is an accreditation of a system **inside** an accredited environment: +the Secure Isolated Network, the accredited rooms, the TSCM sweeps, the DV-cleared +staff, the approved couriers and the destruction products are all environmental, +and sixteen of the thirty-three Tier 3 baseline behaviours have no software +expression (`recon-report.md` §5.3). This plan does not take a system to R4. It +takes the *code* from being a reason an accreditation fails to not being one. + +**Gate discipline.** A gate is signed once, with a date and a named signatory, +and it can be withdrawn. If a later change weakens a control a gate depends on, +the gate is suspended in the same pull request that weakens it — not in the next +review cycle. + +--- + +## 4. The answer pack: what a partner asks, and what we can say + +This is the deliverable that makes the programme visible to a university or a +KEC partner. Each row is a question that appears in a real due-diligence +schedule; the value at each rung is the answer the institution is *permitted* to +give under §3. + +| # | Question in the schedule | At R1 | At R2 | At R3 | Evidence | +| --- | --- | --- | --- | --- | --- | +| 1 | Who can access the data? | Anyone with host access (single-operator tool) | Named principals, per-study grants | Same, plus recorded briefing | `study_acl.yml`; `auth.*` records | +| 2 | Is access logged? | Yes — every state change and release | Same, with actor | Same, plus classification context | Ledger chain | +| 3 | Is data encrypted at rest? | Platform-dependent, now recorded rather than assumed | Attested FDE + restrictive modes | Same, plus encrypted exports | Startup verdict; mode assertions | +| 4 | Is data encrypted in transit? | Not by the server; a non-loopback bind is refused without TLS termination | Same | Same | Bind-refusal test | +| 5 | Can you delete data on request? | Yes, and deletion is recoverable before purge | Same, plus evidenced erasure | Same | `delete.requested`, `erasure.evidenced` | +| 6 | How long is data retained? | Not enforced (policy only) | Enforced clock, listed not auto-deleted | Same | `retention.due` records | +| 7 | Are releases to third parties controlled? | Recorded after the fact | Recorded | Gated on classification before release | `publish.*` records | +| 8 | Are sub-processors identified? | No | No | Yes, with transfer position | Sub-processor register | +| 9 | How is personal data minimised? | Not addressed | Identifier scan at ingest; scrubber on export | Same | Refusal test; manifest | +| 10 | Can a result be attributed and shown unaltered? | Unaltered: yes. Attributed: no | Both | Both, with classification | Content-addressed records + ledger | +| 11 | What is your incident process? | None documented | Drafted (RP-7) | Signed and drilled | Incident procedure | +| 12 | How do you demonstrate all of the above? | By inspection of this repository | Same, plus R2 artefacts | Same, plus a signed evidence index | Evidence index | + +Rows 1, 3, 5, 7 and 12 are the ones that most often stop a KEC agreement. R1 +moves five of them from "no answer" to "answer with a caveat"; R3 moves all +twelve to "answer with evidence". + +--- + +## 5. The KEC and commercial annex + +Things a partner will ask that are not security controls and that therefore get +missed: + +| Item | Position | Action | +| --- | --- | --- | +| **Licence** | AGPLv3 for code, CC-BY-SA for documentation | State it in the schedule before the partner's legal team finds it. AGPLv3 obligations on a hosted deployment are real; the institution should take its own advice | +| **Controller / processor** | Depends on deployment: the deploying institution is controller; the maintainer becomes a processor only if hosting for others | Decide once (RP-0), state in `SECURITY.md` | +| **Data transfer** | A published study may leave the UK; a Zenodo deposition is a transfer | Document the mechanism at R3 or disable the route | +| **Sub-processors** | Reference-database hosts (GitHub releases, PR2), Zenodo, the remote compute host | Register at R3; the remote host is inside the accredited boundary or it is not used for classified work | +| **Export control / dual use** | Bioinformatics methods are not controlled, but the *data* may be | Confirm with the institution's research office before any classified deployment | +| **Publication rights** | The tool's provenance and citation artefacts are designed to be published | A partner agreement must not conflict with the reproducibility claims | +| **Liability and insurance** | No warranty beyond the licence; the remedy programme is the mitigation of record | The residual-risk statement (RP-7) is what an insurer or a contracts team will read | +| **Support expectations** | `SECURITY.md` commits to fixes on `main` and no response time | State it plainly; a partner assuming an SLA is a risk in itself | + +--- + +## 6. Evidence: the register as data, not prose + +The engineering plan already requires new estate configuration to be KYAML +(flow style, double-quoted values, two-space indent, `---` header) per the +YAML-POLICY rule the statistics work follows. The remedy register should be +data for the same reason the numeric contracts are: a claim that a human +re-types into a spreadsheet is a claim that drifts. + +```kyaml +--- +{ schema: "assurance-register/1", generated: "2026-10-25", + remedies: [ + { id: "R-01", harm: "A result cannot be attributed to a person", cause: "SEC-01", + disposition: "substitute", rung: "R2", owner: "PI", + evidence: "test/unit/test_audit.jl", gate: "R2", state: "planned" }, + { id: "R-12", harm: "The interface can be framed and clickjacked", cause: "SEC-10, SEC-11", + disposition: "eliminate", rung: "R1", owner: "PI", + evidence: "test/unit/test_server_security.jl", gate: "R1", state: "planned" } + ], + gates: [ + { id: "R1", signed_by: ["PI", "ITS"], signed_on: null }, + { id: "R2", signed_by: ["PI", "ITS", "DPO"], signed_on: null }, + { id: "R3", signed_by: ["PI", "RO", "DPO", "RI", "independent"], signed_on: null } + ] } +``` + +Two rules for this file if it is adopted: + +1. `state` moves only from `planned` → `implemented` → `verified`, and + `verified` requires the named command to have run in CI on the commit that + claims it. +2. A gate is `signed_on`-dated or it does not exist. A null date in `gates` is + the machine-readable version of "we have not claimed this". + +A governance check in the style of `scripts/check-governance-contract.sh` can +then fail CI when a remedy is marked `verified` without a corresponding test +file, which is the only way this register stays true after everyone who wrote it +has moved on. + +*This sketch is not validated against the KYAML formatter: the tool is not +available in the review environment. It is illustrative of shape, not a +committed artefact.* + +--- + +## 7. Sequencing, critical path and cadence + +### 7.1 Order and dependencies + +``` +RP-0 decide + │ + ├─► RP-1 contain ──► RP-2 record ──► [GATE R1] + │ │ + │ ├─► RP-3 attribute ─┐ + │ └─► RP-4 protect ───┤ + │ ├─► [GATE R2] + │ RP-6a rights (policy)┘ + │ RP-8a DPIA ──────────┘ + │ + └─► RP-5 distinguish ◄── RP-3 ──► RP-6b scrubber ──► RP-7 prove ──► [GATE R3] + │ + └─► RP-8b briefing + transfer +``` + +**Critical path:** RP-2 → RP-3 → RP-5 → RP-7. Everything else can proceed in +parallel or be delayed without moving a gate. That is deliberate: a remedy +programme that stalls should stall in the cheapest place. + +**Parallel-safe:** RP-1 before RP-2; RP-4 any time after RP-3 starts; RP-6a and +RP-8a alongside RP-3; RP-0 questions 3–6 need no engineering at all. + +### 7.2 Milestones without dates + +Calendar dates in a plan written by an agent are fiction. These are +effort-ordered, so they can be rescheduled without invalidating anything: + +| Milestone | Reached when | What it buys | +| --- | --- | --- | +| M0 | RP-0 record signed | The programme has an owner | +| M1 | RP-1 merged and green | Six attack paths gone, no friction added | +| M2 | RP-2 merged, chain verifies | Gate R1 claimable | +| M3 | RP-3 and RP-4 merged, DPIA drafted | Gate R2 claimable | +| M4 | RP-5 and RP-7 merged, independent review complete | Gate R3 claimable | +| M5 | RP-8 sustained through one review cycle | The claims survive staff turnover | + +### 7.3 Cadence once M2 is passed + +| Interval | Activity | Output | +| --- | --- | --- | +| Per pull request | Remedy register updated; evidence command run | `state` transitions only | +| Per release | Re-verify every `verified` remedy's command | Evidence index refreshed | +| Quarterly | Gate review: is each signed gate still supported? | Withdrawal or renewal, recorded | +| Annually | Re-attestation: FDE position, access grants, retention settings, briefing records | Signed annual statement | +| On any incident | Ledger slice pulled, procedure executed, lessons recorded | Incident report that cites ledger evidence, not recollection | + +--- + +## 8. Metrics, honestly chosen + +Metrics in security programmes are usually vanity. These are chosen because they +can be *wrong* in a way that is visible: + +| Metric | Why this one | Failure it reveals | +| --- | --- | --- | +| Schedule questions answerable with evidence | It is the outcome a partner cares about | A programme building controls nobody asked for | +| Time to answer a DSAR from the ledger | It is a legal clock | An audit trail that exists but cannot be queried | +| Deletions recovered from quarantine | Shows the recovery path works in practice | A two-phase delete that is never actually two-phase | +| Remedies marked `verified` whose command has not run this release | Detects drift in the evidence itself | Documentation theatre | +| Added user actions per release | The hard constraint | A control smuggled in as "just a quick confirm" | +| Gate withdrawals | A gate that never withdraws was never real | Compliance theatre | + +The last two are the ones to watch. A programme that only ever adds gates is not +measuring anything. + +--- + +## 9. Stop/go, partial claims, and rollback + +**If a package stalls**, the remedy register records it and the gate it blocks is +simply not signed. That is the whole point of the ladder: R1 is a complete, +useful, signable state. A university can run R1 indefinitely and say true things +about itself. + +**If a gate must be withdrawn**, the withdrawal is dated, the reason recorded, and +every outward-facing statement that relied on it is revised in the same change. +A claim that outlives its evidence is the one failure this document cannot +forgive. + +**If a package is abandoned**, its harms move to R-*accepted* entries with a named +owner and a date, or the gate that depended on it is withdrawn. There is no third +option, because the third option is "nobody noticed". + +**Rollback.** Every package is one or more revertible pull requests (proposal §8). +Reverting a package suspends the gate that depended on it in the same change. +Nothing in this plan requires a database migration or an irreversible state. + +--- + +## 10. Risks of the remedy itself + +| Risk | Why it is real here | Mitigation | +| --- | --- | --- | +| **Compliance theatre** | Documents accrete faster than controls; this plan adds seven artefacts | The evidence index pairs every claim with a command; a claim without one is withdrawn | +| **Over-claim** | The pressure to say "compliant" in a KEC negotiation is enormous | §3 states what may *not* be said at each gate, in the same table as what may | +| **Scope creep into the science** | Security work is easier to argue about than statistics | Rule 4: same inputs, same bytes; one byte-comparison test per package | +| **Burnout of a sole maintainer** | Eight packages on top of a research codebase | RP-1 and RP-2 are small and self-contained; the critical path has four nodes, not eighteen | +| **The plan outlives its author** | Research software changes hands | The register is data (§6), gates are dated, and the CI check fails rather than remembers | +| **False comfort from a green suite** | CI passing is not a security finding being resolved | `SECURITY.md` already says this; RP-7 repeats it in the pack | +| **Institutional risk parked on one person** | The tool's origin is one researcher's judgement | RP-0 names the DPIA owner and the gate signatories; a gate with one name is not a gate | + +--- + +## 11. Decisions required to start + +Carried forward from the proposal, with the remedy-specific consequence of each: + +| # | Decision | Blocks | Consequence if unanswered | +| --- | --- | --- | --- | +| 1 | Target rung for the next release: R1 or R2 | RP-0 | Package selection is guesswork; RP-5 may be built before it is enforceable | +| 2 | Publish as a `Proposal:` issue before code | RP-0 | An architectural change lands without the discussion this repository's convention requires | +| 3 | DPIA owner: institution or maintainer | RP-6a, RP-8a | Gate R2 cannot be signed; R3 is unreachable | +| 4 | Classification vocabulary: UK GSC or an institution profile | RP-5 | The attribute is built twice, or built for the wrong audience | +| 5 | Programme name for outward material | RP-7 | The pack cannot be titled, so it cannot be circulated | +| 6 | Author affiliation wording | RP-7 | §5 of the proposal stays internal, weakening the strongest paragraph it has | +| 7 | *New:* who signs each gate | §3 | The whole table is unenforceable; gates become self-certification | +| 8 | *New:* retention window before purge | RP-2 | Quarantine grows forever, or purges by surprise | +| 9 | *New:* is any deployment expected to hold personal data in the next 12 months | RP-6, RP-8a | Either the DPIA is written unnecessarily, or it is written too late | + +Decisions 7–9 are the ones a delivery plan never asks and a remedy plan cannot +avoid. + +--- + +## 12. What would make this plan wrong + +Stated plainly, so that it can be falsified rather than defended: + +1. **If the deployment is already inside an accredited environment** with mTLS, a + proxy identity, FDE and an audited host, then R-01, R-05 and R-11 are already + satisfied environmentally and RP-3 and RP-4 shrink to documentation plus + attestation. The institution's answer to proposal §10 and + `recon-report.md` §10.5 settles this. +2. **If no deployment will ever hold personal data**, RP-6 and RP-8a are + unnecessary, R-15 becomes a recorded acceptance, and R2 collapses into R1 plus + RP-4. +3. **If no third party will ever receive an artefact**, RP-5's dissemination half + is unnecessary — though marking is still the cheapest possible mitigation for + the copy that gets emailed anyway. +4. **If the review's static findings are wrong**, the register shrinks. Each row + cites the grep that would prove it wrong; a correction belongs in the same + pull request as the finding. +5. **If the programme is judged not worth funding**, the honest output is a + signed acceptance of R-19, R-02 and R-03 with a date and a name — and the + withdrawal of any claim that implied otherwise. That outcome is legitimate. + Silence is not. + +--- + +## 13. Where to read the detail + +| Document | What it contains | +| --- | --- | +| [Proposal](../proposals/security-and-integrity-programme.md) | The institutional case: the blind spot in how research software is reviewed, the provenance of the design, the risk table in each owner's terms, and the ladder | +| [Recon report](../security/recon-report.md) | Seventeen findings with file-and-line evidence; twelve existing controls recorded as sound; GDPR article-by-article; HIPAA specification-by-specification; all 33 TOP SECRET behaviours classified; a twenty-command evidence log | +| [Secure-and-private ultraplan](../security/ultraplan-secure-private.md) | The engineering counterpart: eight workstreams, eighteen pull requests, acceptance tests, ledger events, and the controls deliberately not built | +| [Colour-team assessment](../security/colour-teams.md) | White, red, blue, purple, green and gold over CyBOK knowledge areas; twelve red scenarios; the detection matrix; the conflicts and how each resolves | + +--- + +*This plan is a remedy schedule, not a certificate. Each remedy in it is a harm +somebody owns, a mechanism that removes or reduces it, and a command that shows +whether the claim is still true. Where the answer is "we accepted this", the +document asks for a signature rather than silence.* diff --git a/docs/security/README.md b/docs/security/README.md index 6fe40e78..83429281 100644 --- a/docs/security/README.md +++ b/docs/security/README.md @@ -12,15 +12,25 @@ sensitive material in the country. | [`recon-report.md`](recon-report.md) | What the system is, what protects it today, what does not, and how that scores against GDPR, the HIPAA Security Rule, and the UK TOP SECRET (Tier 3) baseline behaviours. Every finding cites a file and line, and every absence claim has the grep that proves it. | | [`colour-teams.md`](colour-teams.md) | The CyBOK-referenced colour-team exercise: white (governance), red (adversary), blue (detection), purple (validation), green (build/crypto), gold (human/insider) — including where the teams disagree and how each conflict resolves. | | [`../proposals/security-and-integrity-programme.md`](../proposals/security-and-integrity-programme.md) | The case for doing any of this at all, written for the people who carry institutional risk rather than the ones who run the pipeline: research office, DPO, research integrity, IT security, and an industry or KEC partner doing due diligence. | +| [`../proposals/security-and-integrity-ultraplan.md`](../proposals/security-and-integrity-ultraplan.md) | The remedy schedule for the same programme: twenty remedies keyed to the findings, nine independently fundable packages, three gates that state what may and may not be claimed at each, a partner answer pack, and the signature points an assessor will ask for. | | [`ultraplan-secure-private.md`](ultraplan-secure-private.md) | The plan to close the gaps, ordered by rung on a capability ladder, under a hard **zero-friction** rule (eliminate, substitute, or notify — never "ask the user to be careful"). Each workstream names its files, its ledger events, its acceptance tests and its PR decomposition. | ## Read in this order +If you are deciding whether to fund or deploy any of this, start at +[the proposal](../proposals/security-and-integrity-programme.md), then +[the remedy ultraplan](../proposals/security-and-integrity-ultraplan.md) §3 +(what may and may not be claimed) and §4 (the partner answer pack). + +If you are implementing or reviewing it: + 1. `recon-report.md` §1 (executive summary) and §5 (standards matrices). 2. `colour-teams.md` §7 (conflicts) and §8 (heat map) — this is the argument for the plan's ordering. 3. `ultraplan-secure-private.md` §1 (the four rules) and §5 (PR order). -4. `recon-report.md` §9 to re-run the evidence yourself. +4. `../proposals/security-and-integrity-ultraplan.md` §1 (the remedy register) + and §2 (the packages), for who owns each harm. +5. `recon-report.md` §9 to re-run the evidence yourself. ## The three sentences that matter diff --git a/docs/security/ultraplan-secure-private.md b/docs/security/ultraplan-secure-private.md index 59686c28..63ae31f1 100644 --- a/docs/security/ultraplan-secure-private.md +++ b/docs/security/ultraplan-secure-private.md @@ -6,8 +6,11 @@ Status: plan and evidence map for the branch `arena/924008e1-metamanifold-webui`, based on `d6e8671` (`main`). Companion to [`recon-report.md`](recon-report.md) (what is wrong and why it -matters) and [`colour-teams.md`](colour-teams.md) (the adversarial and -governance read). Finding IDs `SEC-nn` refer to the recon report. +matters), [`colour-teams.md`](colour-teams.md) (the adversarial and governance +read) and [`../proposals/security-and-integrity-ultraplan.md`](../proposals/security-and-integrity-ultraplan.md) +(the remedy schedule: the same programme expressed as harms, gates, signatures +and evidence for the people who carry institutional risk). Finding IDs `SEC-nn` +refer to the recon report. **Why the name.** *Secure* is the confidentiality, integrity and availability work: identity, authorisation, the audit ledger, transport, at-rest protection,