From 5ef10d2f6d14086527bf9d838c3004868d9c847e Mon Sep 17 00:00:00 2001 From: plusultra-ops Date: Fri, 15 May 2026 15:49:22 +0200 Subject: [PATCH 01/10] chore: remove operator-internal kill-gate file --- kill-gate.md | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 kill-gate.md diff --git a/kill-gate.md b/kill-gate.md deleted file mode 100644 index 0d46e5f..0000000 --- a/kill-gate.md +++ /dev/null @@ -1,24 +0,0 @@ -# Kill-gate — fhir-validator-cli - -**Decision date:** d+30 from public launch (PyPI publish + r/HL7 post + HL7 Europe Slack drop). - -**Continue if any of:** - -1. **≥40 GitHub stars** on the repo (proxy: someone outside the operator's network found it). -2. **≥3 real-affiliation GitHub issues** opened — issues whose author's profile shows a hospital, regional health authority, HL7 affiliate, EHDS contractor, or named digital-health vendor (not the operator, not generic accounts). -3. **≥10 `pip install` per pypistats** in the trailing 14 days (proxy: someone is actually trying the CLI). -4. **≥1 inbound** — DM, email, or LinkedIn message asking about EHDS, hosted CI, or custom IGs. - -**Kill if none of the above.** Move to `archive/fhir-validator-cli/` with a post-mortem covering: what channel produced zero engagement, whether HAPI is too entrenched, whether the EU IG-bundle angle is too niche, whether EHDS timeline is too slow to drive demand. - -**Half-life check at d+14:** if the trajectory is <25% of the d+30 thresholds (e.g., <10 stars, 0 issues, <2 installs), do not double down on distribution — preserve calories for the rest of the wave. - -**What does NOT count:** - -- Operator-created accounts / sock-puppet stars. -- Stars from the personal network (LinkedIn 1st-degree). -- Bot traffic from PyPI mirrors. - -**What buys time without continuing:** - -- A specific EU national-affiliate (HL7 Spain, HL7 France, etc.) saying "we'd ship this in our CI if it bundled our IG" — that's a fork-point, not a kill, but it changes the roadmap (v0.3 brought forward). From 1b42ac63c3c96b43a0145b74349d68f82af90753 Mon Sep 17 00:00:00 2001 From: plusultra-ops Date: Fri, 15 May 2026 15:49:54 +0200 Subject: [PATCH 02/10] chore: remove operator-internal landing-copy draft --- landing-copy.md | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 landing-copy.md diff --git a/landing-copy.md b/landing-copy.md deleted file mode 100644 index ecf004c..0000000 --- a/landing-copy.md +++ /dev/null @@ -1,39 +0,0 @@ -# Carrd landing-page copy — fhir-validator-cli - -## Hero - -**Validate FHIR against EU IGs in one `pip install`.** - -`fhirv validate patient.json --ig hl7-europe-base` — exit 0 if valid, structured-error JSON if not. HL7 Europe Base, IPS, mCSD, EHDS skeleton: bundled, sha256-verified, no IG-fetching scavenger hunt. - -[Install from PyPI →](https://pypi.org/project/fhir-validator-cli/) [GitHub →](https://github.com/plusultra/fhir-validator-cli) - ---- - -## Sub-hero (one paragraph) - -The HAPI FHIR validator is excellent — and a Java dependency, plus a half-day of fetching IG packages from six registries before the first run. `fhirv` is the CLI-first complement: pure Python, no JVM, EU IGs in the wheel, designed for CI pipelines that need to fail builds on non-conformant resources. MIT-licensed, hosted CI-as-a-service planned (€19–49/mo). - ---- - -## Three-card row - -**Zero config.** `pip install fhir-validator-cli`. No simplifier.net account. No `package.json` for FHIR registries. Bundled IGs ship inside the wheel. - -**EHDS-aware.** HL7 Europe Base, IPS, mCSD ship today. EHDS secondary-use skeleton tracked from draft. The IGs your CI will need next year. - -**CI-native.** Stdout is JSON. Exit code is 0 or 1. Drop it in GitHub Actions, GitLab CI, Jenkins. No daemons, no servers. - ---- - -## Audience CTA - -Built FHIR pipelines for an EU hospital / regional authority / national EHDS contact point? - -[Tell me what IG you wish was bundled →](mailto:plusultra.dev@proton.me?subject=fhir-validator-cli) - ---- - -## Footer - -MIT-licensed. Built for r/HL7 and HL7 Europe Slack. v0.1 ships the manifest + CLI surface; v0.2 bundles real EU IG packs. From d402480430f6d0b7f89bf3a44151faef2ba67bd1 Mon Sep 17 00:00:00 2001 From: plusultra-ops Date: Fri, 15 May 2026 15:50:36 +0200 Subject: [PATCH 03/10] fix(pyproject): correct project URLs to plusultra-tools; drop unused pydantic dep - Homepage/Issues URLs were pointing at github.com/plusultra/... (404, wrong org slug). Replaced with github.com/plusultra-tools/... per red-team blocker A. - pydantic>=2.0 was declared but never imported anywhere in src/. Removed to avoid forcing a ~5 MB transitive dep for a feature that does not exist (red-team major B/A). --- pyproject.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c33e770..89ba2da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,15 +26,14 @@ classifiers = [ ] dependencies = [ "jsonschema>=4.0", - "pydantic>=2.0", ] [project.optional-dependencies] dev = ["pytest>=7", "build", "twine"] [project.urls] -Homepage = "https://github.com/plusultra/fhir-validator-cli" -Issues = "https://github.com/plusultra/fhir-validator-cli/issues" +Homepage = "https://github.com/plusultra-tools/fhir-validator-cli" +Issues = "https://github.com/plusultra-tools/fhir-validator-cli/issues" [project.scripts] fhirv = "fhir_validator_cli.cli:main" From 97525857896418b329dc5c344c5d1e89acda60c9 Mon Sep 17 00:00:00 2001 From: plusultra-ops Date: Fri, 15 May 2026 15:51:25 +0200 Subject: [PATCH 04/10] docs(readme): honest v0.1-scaffold framing; strike overclaims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per red-team blockers (Persona C/E #1, Persona A #2): - Replace 'zero-config validator' pitch with explicit 'scaffold, not validator' status block at the top. Almost any resource returns valid=true in v0.1; this is documented expectation. - Strike 'Hosted CI-as-a-service €19-49/mo' pricing block (no DPA, no Stripe, no legal entity; premature commercial claim). - Strike r/HL7 distribution claim (subreddit does not exist). - Add badges (CI, Python, License, Status). - Add explicit 'Use HAPI/Firely if you need real validation today' callout. - Add HL7/FHIR trademark attribution. - Drop pricing entirely until a design partner exists. --- README.md | 116 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 67 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 0c354d3..276e276 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,100 @@ # fhir-validator-cli -**Zero-config FHIR R4/R5 validator with bundled EU Implementation Guide packs.** One `pip install` and you can validate Patient/Encounter/Observation/Bundle resources against HL7 Europe Base, IPS, mCSD, and the EHDS skeleton (when published) — no fetching IGs from six different registries first. +[![tests](https://github.com/plusultra-tools/fhir-validator-cli/actions/workflows/test.yml/badge.svg)](https://github.com/plusultra-tools/fhir-validator-cli/actions/workflows/test.yml) +[![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue)](https://www.python.org/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +[![Status](https://img.shields.io/badge/status-alpha%20scaffold-orange)](#status) -```bash -pip install fhir-validator-cli -fhirv validate patient.json --ig hl7-europe-base -``` +> v0.1 scaffold for a CLI that will validate FHIR R4/R5 resources against bundled EU Implementation Guide packs. **Today this is NOT a conformance validator** — see [Status](#status) below. --- -## Why this exists +## Status (read this first) -The European Health Data Space (EHDS) regulation forces hospitals, regional health authorities, and digital-health vendors to submit FHIR resources conformant to a specific stack of EU-flavoured IGs: +**v0.1 is a CLI scaffold and manifest format.** It does not perform real IG conformance validation. Specifically: -- **HL7 Europe Base** — the floor profile for any EU-wide FHIR exchange. -- **IPS (International Patient Summary)** — cross-border patient summaries. -- **mCSD (Care Services Discovery)** — provider directory exchange. -- **EHDS skeleton** — the secondary-use submission profile (draft as of 2026; will be mandatory). +- The 4 bundled IGs declared in the manifest (`hl7-europe-base`, `ips-international`, `mcsd`, `ehds-skeleton-pending`) are **placeholders** with `sha256 = 0` and `placeholder: true`. No StructureDefinition content ships. +- The validator runs a minimal structural sanity check: every resource must have a `resourceType` field plus a tiny hand-coded required-field map covering 5 resource types (Patient, Bundle, Observation, Encounter, Condition) with a total of ~6 required fields. No terminology check, no cardinality beyond required/not-required, no slicing, no fixed-value, no must-support, no invariants, no profile walk. +- Almost any resource you throw at it will return `"valid": true`. That is **expected behaviour for v0.1**, not a bug. -The existing toolchain is split: +**Use this today only as:** a smoke test for the CLI surface, an example of the manifest format, a fixture for downstream tooling that needs *some* `fhirv`-compatible binary on `$PATH`. Do **not** rely on `"valid": true` as evidence of IG conformance. -- **HAPI FHIR validator** (Java) — heavy, server-grade, mature, but you have to fetch each IG package yourself from `simplifier.net` / `build.fhir.org` / various national registries. Wires up in 1-2 days the first time. -- **fhir.js / fhirpath.js** (Node) — runtime-validation oriented, no CLI-first DX. -- **Inferno** — testing harness for FHIR servers, not a CLI. +Real IG-level validation is scheduled for v0.2. -We close the gap. `fhirv` ships **the EU IG packs bundled inside the wheel**, with a tamper-evident manifest (sha256 of each pack + provenance URL), so CI pipelines can do `pip install && fhirv validate` and get a meaningful exit code in <2 seconds. +If you need production-grade FHIR validation today, use **HAPI FHIR ValidationCLI** ([docs](https://hapifhir.io/hapi-fhir/docs/validation/instance_validator.html)) or **Firely .NET SDK / firely-terminal**. -## What it does +--- -1. `pip install fhir-validator-cli` — single dependency, pure Python, no JVM. -2. `fhirv validate resource.json --ig hl7-europe-base` — validates a FHIR resource or Bundle against a named bundled IG. -3. Exit code `0` if valid; exit code `1` and structured-error JSON to stdout if not. Designed for CI: redirect stdout, parse with `jq`, fail the build. -4. `fhirv list-igs` — prints the bundled IGs with version + last-updated + sha256. -5. `fhirv manifest` — dumps the full IG manifest (source URL, version, sha256 of each pack) so you can audit provenance in your supply chain. +## Install -## What it does NOT do +```bash +pip install fhir-validator-cli # not yet on PyPI; install from source for now +``` + +From source: + +```bash +git clone https://github.com/plusultra-tools/fhir-validator-cli +cd fhir-validator-cli +pip install -e ".[dev]" +``` -- Not a replacement for **HAPI FHIR validator** when you need server-side, terminology-server-backed, real-time validation. HAPI remains the reference for that path. -- No bundled FHIR server, no REST endpoints, no UI. CLI-first, single binary mindset. -- No terminology expansion against external $expand operations in v0.1. (v0.3 will allow plugging a tx-server URL.) -- No code-system normalisation. Garbage in, garbage out at the terminology layer. +## CLI surface (v0.1) -## Bundled IGs (v0.1) +```bash +fhirv validate patient.json --ig hl7-europe-base # base-shape check only (see Status) +fhirv list-igs # list bundled IGs (all placeholders today) +fhirv manifest # dump the raw IG manifest JSON +fhirv --version +``` -> v0.1 ships the manifest format and **one placeholder IG** (`hl7-europe-base@0.0.1-placeholder`) so the CLI surface is exercised end-to-end. v0.2 will bundle the real packs once the legal review on redistribution clears. The schedule below is the target for v0.2. +Exit code `0` on pass, `1` on fail, `2` on user error (file not found / invalid JSON). -| Name | Version | Source URL | sha256 (target v0.2) | -|---|---|---|---| -| `hl7-europe-base` | 0.1.0 (v0.2 target: 1.0.0) | https://build.fhir.org/ig/hl7-eu/base/ | (pending v0.2) | -| `ips-international` | 0.1.0 (v0.2 target: 2.0.0-ballot) | https://hl7.org/fhir/uv/ips/ | (pending v0.2) | -| `mcsd` | 0.1.0 (v0.2 target: 1.0.0) | https://hl7.org/fhir/uv/mcsd/ | (pending v0.2) | -| `ehds-skeleton-pending` | 0.0.1 | https://health.ec.europa.eu/ehealth-digital-health-and-care/european-health-data-space_en | (skeleton, EHDS profile not yet published) | +## What v0.1 does NOT do -The actual values are kept in `src/fhir_validator_cli/data/ig-manifest.json`, which is the single source of truth. Run `fhirv manifest` to dump it. +- Does **not** walk StructureDefinitions. +- Does **not** ship real EU IG content — all bundled IGs are placeholders. +- Does **not** do terminology expansion (`$expand`, SNOMED CT, LOINC, ICD-10). +- Does **not** cover most of the ~150 FHIR R4 resources — only 5 have any required-field rules. +- Does **not** validate against `meta.profile`. +- Does **not** offer `--strict`, `--profile`, `--severity-filter` flags. +- Does **not** replace HAPI FHIR validator, Firely .NET SDK / firely-terminal, or Inferno. ## Roadmap -- **v0.1 (this release)** — CLI surface, manifest format, placeholder IG, structured validation errors, JSON-Schema-level checks. -- **v0.2** — Real EU IG packs bundled, full StructureDefinition walk. -- **v0.3** — Optional terminology-server plug, SNOMED CT subset validation, custom-profile loading. -- **v1.0** — Stable wire format for the structured-error JSON; semver guarantees. +- **v0.1 (this release)** — CLI surface + manifest format + base structural sanity check. +- **v0.2** — Real EU IG packs bundled (hl7-europe-base first), full StructureDefinition walk, `--strict`, `--profile` flags, generated required-fields map from FHIR R4 metadata covering all resources. +- **v0.3** — Optional terminology-server plug (`--tx-server`), SNOMED CT subset validation, custom-profile loading. +- **v1.0** — Stable wire format for structured-error JSON, semver guarantees. + +## Why this might eventually exist -## Pricing +The European Health Data Space (EHDS) regulation (entered into force 2025; phased in 2026-2031) will require FHIR resources conformant to a stack of EU-flavoured IGs (HL7 Europe Base, IPS, mCSD, an EHDS-specific submission profile). Today the toolchain for that workflow is fragmented: HAPI FHIR validator (Java, heavy, requires fetching each IG pack from `simplifier.net` / national registries) and the Firely .NET stack (Windows-first, .NET runtime). -- **CLI: MIT, free, forever.** -- **Hosted CI-as-a-service (planned)** — €19/mo (solo) to €49/mo (team). Same validation, but webhook-driven, dashboards, slack alerts, audit log. Stripe-billed when the demand signal justifies it. +The wedge — *if* this venture survives to v0.2 — is a pure-Python wheel with EU IG packs bundled, suitable for `pip install && fhirv validate` in a Linux CI runner with no JVM and no first-run network fetch. v0.1 ships only the wrapper. -## Audience +## Audience (target, not validated) -- FHIR engineers at EU hospital groups, regional health authorities, EHDS National Contact Points. -- HL7 Europe affiliate working groups. -- Digital-health vendors integrating to EU FHIR endpoints. -- Distribution channels: **r/HL7**, **HL7 Europe Slack**, **dev.to**, **awesome-fhir** GitHub list, EHDS engineering mailing lists. +FHIR engineers at EU hospital integration teams who already run Python in CI and want a faster smoke test than spinning up HAPI. + +Until at least one such engineer files an issue or starts a discussion, the target audience is **hypothetical**. See `kill-gate.md` (operator-internal) for the 30-day demand-signal threshold. ## Contributing -Open an issue with a real resource that fails validation but should pass (or vice versa). PRs welcome — especially for additional EU national IGs (Spain HL7-ES, France ANS, Germany MIO/KBV). +See [CONTRIBUTING.md](CONTRIBUTING.md). Issues welcome, especially: + +- Bug reports against the v0.1 CLI surface (`--help`, exit codes, JSON output shape). +- Feedback on the manifest format before v0.2 locks it in. +- Pointers to EU national IGs (Spain HL7-ES, France ANS, Germany MIO/KBV) that should be in scope for v0.2. + +## Security + +See [SECURITY.md](SECURITY.md) for the vulnerability disclosure policy. ## License MIT. See [LICENSE](LICENSE). + +## Trademarks + +FHIR® and HL7® are registered trademarks of Health Level Seven International. This project is not affiliated with HL7. References to IPS, mCSD, and EHDS are nominative use of standard names. From d5eeb3872c5ccaacd641bc6d8f86a8fb2a4530cc Mon Sep 17 00:00:00 2001 From: plusultra-ops Date: Fri, 15 May 2026 15:52:17 +0200 Subject: [PATCH 05/10] fix(igs): fail-closed on placeholder packs in verify_pack_integrity Per red-team Persona D blocker: v0.1.0 returned True for any bytes when the manifest entry was marked placeholder=true. In v0.2 (when real packs ship) this would be a supply-chain hole: an attacker re-flagging a tampered pack as placeholder bypasses verification silently. Fix: placeholder entries have NO integrity claim by construction and now always return False. Callers that want to accept a placeholder must inspect get_ig(name).placeholder explicitly; verify_pack_integrity will never assert 'verified' for them. Adds 3 regression tests covering unknown IG, default fail-closed, and opted-in path. --- src/fhir_validator_cli/igs.py | 26 ++++++++++++++++++++++---- tests/test_validator.py | 22 +++++++++++++++++++++- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/fhir_validator_cli/igs.py b/src/fhir_validator_cli/igs.py index fbc59b6..43ceb66 100644 --- a/src/fhir_validator_cli/igs.py +++ b/src/fhir_validator_cli/igs.py @@ -73,15 +73,33 @@ def compute_sha256(data: bytes) -> str: return hashlib.sha256(data).hexdigest() -def verify_pack_integrity(name: str, pack_bytes: bytes) -> bool: +def verify_pack_integrity( + name: str, pack_bytes: bytes, *, allow_placeholder: bool = False +) -> bool: """Check that a pack's bytes match the manifest-declared sha256. - Returns True for placeholder packs (no integrity claim) when the manifest - flag is set, otherwise compares against the declared digest. + Fail-closed by default: when an IG is flagged ``placeholder: true`` in the + manifest, integrity CANNOT be verified (no real digest exists) and this + function returns ``False``. Callers must opt in to the placeholder path + with ``allow_placeholder=True`` and handle the "no verification possible" + case explicitly — this prevents a supply-chain hole where an attacker + re-flags a tampered real pack as placeholder to bypass verification. + + Returns: + ``True`` iff the pack's sha256 matches the manifest entry AND the + manifest entry is not a placeholder. + ``False`` for unknown IGs, placeholder IGs (unless explicitly opted-in, + in which case still ``False`` — placeholders have no integrity + claim by construction), or sha256 mismatches. """ ig = get_ig(name) if ig is None: return False if ig.placeholder: - return True + # Placeholder entries carry sha256 = "0" * 64 (no real digest). + # Even if a caller opts in, we cannot truthfully say "verified", + # so we always return False and let the caller branch on the + # placeholder flag via get_ig(name).placeholder if it wants to + # accept the pack without verification. + return False return compute_sha256(pack_bytes) == ig.sha256 diff --git a/tests/test_validator.py b/tests/test_validator.py index 270bf97..9ad9b85 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -5,7 +5,7 @@ import json import unittest -from fhir_validator_cli.igs import list_igs, get_ig, load_manifest +from fhir_validator_cli.igs import list_igs, get_ig, load_manifest, verify_pack_integrity from fhir_validator_cli.validator import validate_resource @@ -85,5 +85,25 @@ def test_manifest_is_valid_json(self) -> None: self.assertIn("hl7-europe-base", serialised) +class IntegrityTest(unittest.TestCase): + def test_verify_unknown_ig_returns_false(self) -> None: + self.assertFalse(verify_pack_integrity("no-such-ig", b"anything")) + + def test_verify_placeholder_returns_false_default(self) -> None: + # Regression: v0.1.0 returned True for placeholder packs (supply-chain + # hole). Must now fail closed. + ig = get_ig("hl7-europe-base") + self.assertIsNotNone(ig) + self.assertTrue(ig.placeholder) + self.assertFalse(verify_pack_integrity("hl7-europe-base", b"anything")) + + def test_verify_placeholder_returns_false_even_when_opted_in(self) -> None: + # Even with allow_placeholder=True we never assert verified for a + # placeholder; caller must inspect get_ig(...).placeholder explicitly. + self.assertFalse( + verify_pack_integrity("hl7-europe-base", b"anything", allow_placeholder=True) + ) + + if __name__ == "__main__": unittest.main() From 9cefdb3aab6c599df9705203e94268d59205350d Mon Sep 17 00:00:00 2001 From: plusultra-ops Date: Fri, 15 May 2026 15:52:40 +0200 Subject: [PATCH 06/10] ci: SHA-pin GitHub Actions; add Dependabot for actions+pip Per red-team Persona D major: - Pin actions/checkout@v4 to commit 34e114876b0b... (full SHA) - Pin actions/setup-python@v5 to commit a26af69be951... (full SHA) - Tag-based pinning lets a compromise of the action publisher's release tag propagate to all downstream pipelines; SHA-pinning prevents that. - Trailing comment preserves the human-readable version for review. - Add dependabot.yml so action+pip pins are kept fresh with reviewed PRs (weekly schedule, cap 5 open PRs per ecosystem). --- .github/dependabot.yml | 12 ++++++++++++ .github/workflows/test.yml | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..af9c84f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 34a7495..dcd1842 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,10 +15,10 @@ jobs: python-version: ["3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: ${{ matrix.python-version }} From ab09b3bf0cc995e2376847295a5a04204fd95778 Mon Sep 17 00:00:00 2001 From: plusultra-ops Date: Fri, 15 May 2026 15:53:15 +0200 Subject: [PATCH 07/10] docs: add CONTRIBUTING.md with PHI-redaction guidance; align CHANGELOG date Per red-team Persona A major + Persona C blocker: - CONTRIBUTING.md was missing. New file covers dev install, test command, PHI redaction rules for bug reports, PR checklist (no real PHI, manifest sha256 either real or placeholder). - CHANGELOG was dated 2026-05-14 (workspace build date) but the repo pushed 2026-05-15. Align to actual publish date so wheel metadata matches. - Document the fail-closed change + Actions SHA-pin under Security. --- CHANGELOG.md | 15 ++++++++++++--- CONTRIBUTING.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 48a9e4a..e4a0d6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,13 +16,22 @@ adheres to [Semantic Versioning](https://semver.org/). - Custom-profile loading from a local IG directory. - SNOMED CT subset validation hooks. -## [0.1.0] - 2026-05-14 +## [0.1.0] - 2026-05-15 ### Added - Initial release: CLI surface (`validate`, `list-igs`, `manifest` subcommands). - Manifest format for bundled IG packs (name, version, source URL, sha256, last-updated). -- Placeholder IGs for hl7-europe-base, ips-international, mcsd, ehds-skeleton-pending. -- Structural + minimum-cardinality validation for Patient, Bundle, Observation, Encounter, Condition. +- Placeholder IGs for hl7-europe-base, ips-international, mcsd, ehds-skeleton-pending + (no real StructureDefinition content; see README "Status"). +- Minimal structural sanity check for Patient, Bundle, Observation, Encounter, Condition. + NOT a conformance validator — see README. - Structured-error JSON on stdout (CI-friendly). - MIT license. - pytest matrix on py3.10/3.11/3.12. + +### Security +- `verify_pack_integrity()` now fails closed on placeholder packs (previously + returned True for any bytes when `placeholder=true`; would have been a + supply-chain hole in v0.2 once real packs ship). +- GitHub Actions SHA-pinned (`actions/checkout`, `actions/setup-python`). +- Dependabot enabled for github-actions + pip ecosystems. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5bc25d9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# Contributing to fhir-validator-cli + +Thanks for the interest. This is a v0.1 scaffold (see README "Status") — most of the validator does not exist yet, so contributions today are mostly bug reports on the CLI surface, manifest-format feedback, and pointers to EU national IGs for the v0.2 scope. + +## Dev install + +```bash +git clone https://github.com/plusultra-tools/fhir-validator-cli +cd fhir-validator-cli +python -m pip install -e ".[dev]" +``` + +## Run tests + +```bash +python -m unittest discover -v tests +``` + +CI runs the same on py3.10 / 3.11 / 3.12 (Linux). + +## Reporting bugs + +Open a GitHub issue. **Do NOT paste real patient data (PHI).** If a bug only reproduces on a real resource: + +1. Strip the resource to the minimum that still triggers the bug. +2. Replace all `Patient.name`, `Patient.identifier`, `Patient.birthDate`, `Patient.address`, `Patient.telecom`, free-text `note`/`comment`/`text.div` fields, and any extension carrying personal data with synthetic values. +3. If you cannot redact without losing the repro, see `SECURITY.md` for the private disclosure channel. + +We will close issues that contain unredacted real PHI and ask you to re-open with a synthetic example. + +## Code style + +- Python 3.10+ type hints required on every public function. +- `from __future__ import annotations` at the top of each module. +- No new runtime dependencies without discussion (pure-stdlib + `jsonschema` is the budget for v0.1; v0.2 may add IG-pack tooling). +- Tests live in `tests/`, named `test_*.py`, use `unittest`. + +## PR checklist + +- [ ] Tests pass locally (`python -m unittest discover -v tests`). +- [ ] No real PHI in fixtures, examples, or commit messages. +- [ ] CHANGELOG.md updated under `[Unreleased]`. +- [ ] If the PR touches `data/ig-manifest.json`, the sha256 fields are either real digests (not zero) or the `placeholder: true` flag is set. + +## License + +Contributions are accepted under the MIT license (see LICENSE). From 4a2993c6d7179badf3ec42d37a77336809f4c4d4 Mon Sep 17 00:00:00 2001 From: plusultra-ops Date: Fri, 15 May 2026 15:53:47 +0200 Subject: [PATCH 08/10] fix(cli): cap input resource size + reject non-object top-level JSON Per red-team Persona D major: - Without an upper bound on resource size, a 1 GB JSON file will OOM the process. Reject anything over 100 MB by default (FHIR resources in practice are <1 MB; this is a generous Bundle ceiling). Tunable via FHIRV_MAX_RESOURCE_BYTES env var. - Reject top-level JSON arrays / scalars early (must be a resource object). Previously these would crash in validator with unhelpful tracebacks. --- src/fhir_validator_cli/cli.py | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/fhir_validator_cli/cli.py b/src/fhir_validator_cli/cli.py index 757b49a..d0e04d3 100644 --- a/src/fhir_validator_cli/cli.py +++ b/src/fhir_validator_cli/cli.py @@ -13,12 +13,53 @@ from fhir_validator_cli.validator import validate_resource +# Hard cap on input resource file size to bound memory + parse time and prevent +# trivial OOM via a 1 GB JSON. FHIR resources in practice are <1 MB; 100 MB is +# a generous Bundle ceiling. Tunable via FHIRV_MAX_RESOURCE_BYTES. +_DEFAULT_MAX_RESOURCE_BYTES = 100 * 1024 * 1024 # 100 MB + + +def _max_resource_bytes() -> int: + """Read the input-size cap from env, falling back to default.""" + import os + + raw = os.environ.get("FHIRV_MAX_RESOURCE_BYTES") + if raw is None: + return _DEFAULT_MAX_RESOURCE_BYTES + try: + value = int(raw) + return value if value > 0 else _DEFAULT_MAX_RESOURCE_BYTES + except ValueError: + return _DEFAULT_MAX_RESOURCE_BYTES + + def _cmd_validate(args: argparse.Namespace) -> int: """Validate a FHIR resource file against a named IG.""" resource_path = Path(args.resource) if not resource_path.exists(): print(json.dumps({"error": f"file not found: {resource_path}"}), file=sys.stderr) return 2 + + try: + size = resource_path.stat().st_size + except OSError as exc: + print(json.dumps({"error": f"cannot stat file: {exc}"}), file=sys.stderr) + return 2 + cap = _max_resource_bytes() + if size > cap: + print( + json.dumps( + { + "error": ( + f"resource file too large: {size} bytes exceeds cap of " + f"{cap} bytes (FHIRV_MAX_RESOURCE_BYTES)" + ) + } + ), + file=sys.stderr, + ) + return 2 + try: with resource_path.open("r", encoding="utf-8") as fh: resource = json.load(fh) @@ -26,6 +67,15 @@ def _cmd_validate(args: argparse.Namespace) -> int: print(json.dumps({"error": f"invalid JSON: {exc}"}), file=sys.stderr) return 2 + if not isinstance(resource, dict): + print( + json.dumps( + {"error": "top-level JSON value must be a FHIR resource object, not array/scalar"} + ), + file=sys.stderr, + ) + return 2 + result = validate_resource(resource, args.ig) print(json.dumps(result.to_dict(), indent=2, ensure_ascii=False)) return result.exit_code From e55b2682b6522b912c82e3641c091b541f49589d Mon Sep 17 00:00:00 2001 From: plusultra-ops Date: Fri, 15 May 2026 15:54:21 +0200 Subject: [PATCH 09/10] docs(security): CRA-aligned timelines, placeholder-pack reality, PHI handling Per red-team Persona C minor + D major + C blocker: - Note CRA Annex VII alignment from late 2027 (24h notification for actively exploited vulns); v0.1 stays at best-effort 72h pre-CRA. - Reflect the fail-closed change: placeholder packs have no integrity claim by construction. Real verification arrives with real packs in v0.2. - Mention the 100 MB input cap as partial DoS mitigation. - Forbid PHI in issues; point to CONTRIBUTING for redaction guidance. --- SECURITY.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 9b65273..0d4ccb1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,14 +11,15 @@ Email **plusultra.dev@proton.me** with subject `[fhir-validator-cli] security`. Do **not** open a public GitHub issue for security findings. -Expected response: acknowledgement within 72 hours, triage within 7 days, fix or mitigation within 30 days for high-severity issues. +Expected response: best-effort acknowledgement within 72 hours, triage within 7 days, fix or mitigation within 30 days for high-severity issues. Once the EU Cyber Resilience Act (CRA) applies to this product (CRA enforcement from late 2027 for products with digital elements), reporting timelines will align with CRA Annex VII obligations (24 h notification of actively exploited vulnerabilities). ## Threat model `fhir-validator-cli` is a local CLI that parses FHIR JSON resources supplied by the user. Trust boundaries: - **Input resources** are assumed potentially adversarial — a malformed or hostile JSON should produce a structured error, never a crash, never code execution. -- **Bundled IG packs** are signed at the manifest level via sha256. Tampering with a bundled pack will be detected by `verify_pack_integrity()`. Tampering with the manifest itself is outside the CLI's defence — verify the wheel signature from PyPI. +- **Bundled IG packs** are intended to be verified at the manifest level via sha256. v0.1 ships ONLY placeholder packs (`placeholder: true`, `sha256 = "0" * 64`); `verify_pack_integrity()` deliberately fails closed on placeholders — they have no integrity claim by construction. Real digests + real verification arrive in v0.2 alongside the real IG packs. +- **Manifest tampering** is outside the CLI's defence — verify the wheel signature from PyPI / the GitHub-attested provenance once v0.2 publishes via Trusted Publishing. - **No network calls** are made by v0.1. v0.3 will introduce optional terminology-server requests; those will require explicit `--tx-server` opt-in. ## Provenance @@ -27,6 +28,10 @@ Bundled IG packs ship with source URL + sha256 in `src/fhir_validator_cli/data/i ## Out-of-scope -- Denial-of-service via extremely large resources (we recommend a 100 MB input cap in your own pipeline). +- Denial-of-service via extremely large resources is **partially mitigated** in v0.1.1 by the 100 MB input cap (`FHIRV_MAX_RESOURCE_BYTES`); pathological deeply-nested JSON within that cap can still slow validation. - Side-channel attacks on validation timing. - Anything involving a FHIR server (this is a CLI; no server surface). + +## PHI handling + +Issues and pull requests in this repository **must not** contain real patient data. See [CONTRIBUTING.md](CONTRIBUTING.md) for redaction guidance. If a vulnerability report can only be demonstrated against real PHI, email plusultra.dev@proton.me first — do not post the resource in a public issue. From 48032fb61190d401bda55eab51c921c9ae21afec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 13:55:28 +0000 Subject: [PATCH 10/10] chore(deps): bump actions/checkout from 4.3.1 to 6.0.2 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.3.1 to 6.0.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/34e114876b0b11c390a56381ad16ebd13914f8d5...de0fac2e4500dabe0009e67214ff5f5447ce83dd) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.2 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dcd1842..d8d3ef2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: python-version: ["3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5