Skip to content

fix(security): web server hardening, easyRadiology SSRF guard, offline provider stub - #46

Merged
Liohtml merged 4 commits into
mainfrom
claude/repo-issues-features-3ZLdb
May 28, 2026
Merged

fix(security): web server hardening, easyRadiology SSRF guard, offline provider stub#46
Liohtml merged 4 commits into
mainfrom
claude/repo-issues-features-3ZLdb

Conversation

@Liohtml

@Liohtml Liohtml commented May 28, 2026

Copy link
Copy Markdown
Owner

Prioritised batch addressing the three open High-severity issues found by the repo-monitor/repo-health sweeps.

Closes

Changes

#26 — Web server hardening

  • Default bind host changed from 0.0.0.0127.0.0.1 (CLI serve + Settings). Network exposure is now an explicit opt-in.
  • Optional API-key auth: when MEDCHECK_API_KEY is set, /api endpoints require a matching X-API-Key header (constant-time compare). /health and / stay public. With no key configured, behaviour is unchanged (localhost dev).
  • serve prints a warning when binding to a non-loopback host without a key.

#28 — easyRadiology SSRF guard

  • New _validate_download_url(): requires https and a host on the easyradiology.net/.de allowlist before fetching the exam ZIP.
  • Disabled follow_redirects on the download client so a 30x bounce can't escape the allowlist (e.g. to 169.254.169.254 or RFC 1918).

#36 — Offline provider stub

  • New LocalLLMProvider (llm/local.py): check_available() returns False so the router skips it cleanly; analyze_images() raises an actionable NotImplementedError pointing to Add local LLaVA-Med vision model provider #18.
  • Registered in the vision router and surfaced in medcheck models. No more RuntimeError: No LLM provider available when running with zero API keys.
  • README / .env.example mark local/LLaVA-Med as coming soon (Add local LLaVA-Med vision model provider #18).

Validation

  • ruff check ✅ · ruff format --check ✅ · mypy ✅ · bandit -ll
  • Test suite: 89 passed (was 73; +16 covering URL validation, API-key auth, default host, and the local provider)
  • Coverage 66%

Note: development is constrained to the claude/repo-issues-features-3ZLdb branch, so these three independent fixes are bundled into a single PR.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz


Generated by Claude Code

claude added 4 commits May 28, 2026 22:01
Addresses three previously unaddressed issues:

- #43: build_prompt() now loads detailed anatomy templates from
  prompts/anatomy/*.txt (knee/shoulder/spine), with cached lookup and
  fallback to built-in hints. The shipped templates were dead code before.
  Adds an abdomen hint so all README-advertised regions are covered.
- #44: add `medcheck providers` and `medcheck models` discovery commands.
- #42: fix README "Report Bug" link (bug_report.md -> bug_report.yml).

Updates CHANGELOG and README; adds unit tests for the new loader and CLI
commands (72 passing).

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz
…versal

- Add missing blank line so `ruff format --check` passes (lint CI was red).
- Slug-validate the anatomy region before building a filesystem path in
  load_anatomy_instructions(), preventing path traversal (CodeRabbit review).
- Add regression test for the traversal guard.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz
…dd offline provider stub

Addresses three High-severity issues:

- #26: web server now binds 127.0.0.1 by default (was 0.0.0.0); add optional
  X-API-Key auth on /api endpoints via MEDCHECK_API_KEY, and warn when binding
  to a non-loopback host without a key configured.
- #28: validate the easyRadiology linkToERI download URL (HTTPS + host
  allowlist) and disable redirects to prevent SSRF to internal/metadata hosts.
- #36: add LocalLLMProvider stub so the advertised "local" offline fallback
  degrades gracefully (check_available()=False, actionable NotImplementedError)
  instead of crashing with "No LLM provider available"; register it in the
  vision router and `medcheck models`.

Updates README, .env.example, CHANGELOG; adds unit tests for all three
(89 passing, coverage 66%).

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Liohtml, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 1 minute and 27 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 763941fc-8949-4431-af5b-74c5482d74bf

📥 Commits

Reviewing files that changed from the base of the PR and between c44fdf9 and 4c81bbc.

📒 Files selected for processing (13)
  • .env.example
  • CHANGELOG.md
  • README.md
  • src/medcheck/core/config.py
  • src/medcheck/llm/local.py
  • src/medcheck/main.py
  • src/medcheck/pipeline/vision_analysis.py
  • src/medcheck/providers/easyradiology.py
  • src/medcheck/web/app.py
  • tests/unit/test_core/test_config.py
  • tests/unit/test_llm/test_local.py
  • tests/unit/test_providers/test_easyradiology.py
  • tests/unit/test_web.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/repo-issues-features-3ZLdb

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.41667% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/medcheck/main.py 40.00% 3 Missing ⚠️
src/medcheck/pipeline/vision_analysis.py 0.00% 2 Missing ⚠️
src/medcheck/providers/easyradiology.py 81.81% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@Liohtml
Liohtml merged commit 9de26f4 into main May 28, 2026
14 checks passed
Liohtml pushed a commit that referenced this pull request May 28, 2026
…validation

Addresses four Medium/High-severity privacy issues:

- #27: gate external (cloud) LLM transmission behind explicit consent
  (--allow-cloud-llm flag, MEDCHECK_ALLOW_EXTERNAL_LLM env, interactive
  prompt); vision_analysis raises instead of silently sending PHI.
- #29: stop logging the raw patient name to stdout — log a non-reversible
  hash of the patient ID instead.
- #30: stop echoing portal access codes into ValueError messages.
- #33: add a Pydantic AnalyzeRequest schema with validation for
  POST /api/analyze (auth was already added in #46).

Docs: SECURITY.md "Handling of Patient Data" section, README + .env.example.
Tests: +5 (consent gate, credential redaction, request validation); 93 passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz
Liohtml added a commit that referenced this pull request May 29, 2026
…validation (#47)

* feat: anatomy template loader, provider/model discovery CLI, doc fixes

Addresses three previously unaddressed issues:

- #43: build_prompt() now loads detailed anatomy templates from
  prompts/anatomy/*.txt (knee/shoulder/spine), with cached lookup and
  fallback to built-in hints. The shipped templates were dead code before.
  Adds an abdomen hint so all README-advertised regions are covered.
- #44: add `medcheck providers` and `medcheck models` discovery commands.
- #42: fix README "Report Bug" link (bug_report.md -> bug_report.yml).

Updates CHANGELOG and README; adds unit tests for the new loader and CLI
commands (72 passing).

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: satisfy ruff format and harden anatomy template path against traversal

- Add missing blank line so `ruff format --check` passes (lint CI was red).
- Slug-validate the anatomy region before building a filesystem path in
  load_anatomy_instructions(), preventing path traversal (CodeRabbit review).
- Add regression test for the traversal guard.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix(security): harden web server, validate easyRadiology downloads, add offline provider stub

Addresses three High-severity issues:

- #26: web server now binds 127.0.0.1 by default (was 0.0.0.0); add optional
  X-API-Key auth on /api endpoints via MEDCHECK_API_KEY, and warn when binding
  to a non-loopback host without a key configured.
- #28: validate the easyRadiology linkToERI download URL (HTTPS + host
  allowlist) and disable redirects to prevent SSRF to internal/metadata hosts.
- #36: add LocalLLMProvider stub so the advertised "local" offline fallback
  degrades gracefully (check_available()=False, actionable NotImplementedError)
  instead of crashing with "No LLM provider available"; register it in the
  vision router and `medcheck models`.

Updates README, .env.example, CHANGELOG; adds unit tests for all three
(89 passing, coverage 66%).

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix(security): PHI consent gate, log/credential redaction, API input validation

Addresses four Medium/High-severity privacy issues:

- #27: gate external (cloud) LLM transmission behind explicit consent
  (--allow-cloud-llm flag, MEDCHECK_ALLOW_EXTERNAL_LLM env, interactive
  prompt); vision_analysis raises instead of silently sending PHI.
- #29: stop logging the raw patient name to stdout — log a non-reversible
  hash of the patient ID instead.
- #30: stop echoing portal access codes into ValueError messages.
- #33: add a Pydantic AnalyzeRequest schema with validation for
  POST /api/analyze (auth was already added in #46).

Docs: SECURITY.md "Handling of Patient Data" section, README + .env.example.
Tests: +5 (consent gate, credential redaction, request validation); 93 passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: address CodeRabbit review on #47

- ingest: stop logging study description (free-text DICOM field may contain PHI)
- vision_analysis: honour explicit LLM provider preference (wires up --model)
  instead of hardcoding "claude"; default to on-device "local" without consent
  so offline execution stays reachable
- docs: soften compliance/anonymity wording (drop "(HIPAA/GDPR)" assertion and
  "non-reversible" claim; describe pseudonymisation accurately)

94 tests passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: keep 'local' as default vision provider regardless of consent

Consent (--allow-cloud-llm / MEDCHECK_ALLOW_EXTERNAL_LLM) now only permits
falling back to an external provider; it no longer flips the implicit default
from on-device 'local' to 'claude'. Addresses CodeRabbit review on #47.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

---------

Co-authored-by: Claude <noreply@anthropic.com>
Liohtml added a commit that referenced this pull request May 31, 2026
…t clarity (#48)

* feat: anatomy template loader, provider/model discovery CLI, doc fixes

Addresses three previously unaddressed issues:

- #43: build_prompt() now loads detailed anatomy templates from
  prompts/anatomy/*.txt (knee/shoulder/spine), with cached lookup and
  fallback to built-in hints. The shipped templates were dead code before.
  Adds an abdomen hint so all README-advertised regions are covered.
- #44: add `medcheck providers` and `medcheck models` discovery commands.
- #42: fix README "Report Bug" link (bug_report.md -> bug_report.yml).

Updates CHANGELOG and README; adds unit tests for the new loader and CLI
commands (72 passing).

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: satisfy ruff format and harden anatomy template path against traversal

- Add missing blank line so `ruff format --check` passes (lint CI was red).
- Slug-validate the anatomy region before building a filesystem path in
  load_anatomy_instructions(), preventing path traversal (CodeRabbit review).
- Add regression test for the traversal guard.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix(security): harden web server, validate easyRadiology downloads, add offline provider stub

Addresses three High-severity issues:

- #26: web server now binds 127.0.0.1 by default (was 0.0.0.0); add optional
  X-API-Key auth on /api endpoints via MEDCHECK_API_KEY, and warn when binding
  to a non-loopback host without a key configured.
- #28: validate the easyRadiology linkToERI download URL (HTTPS + host
  allowlist) and disable redirects to prevent SSRF to internal/metadata hosts.
- #36: add LocalLLMProvider stub so the advertised "local" offline fallback
  degrades gracefully (check_available()=False, actionable NotImplementedError)
  instead of crashing with "No LLM provider available"; register it in the
  vision router and `medcheck models`.

Updates README, .env.example, CHANGELOG; adds unit tests for all three
(89 passing, coverage 66%).

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix(security): PHI consent gate, log/credential redaction, API input validation

Addresses four Medium/High-severity privacy issues:

- #27: gate external (cloud) LLM transmission behind explicit consent
  (--allow-cloud-llm flag, MEDCHECK_ALLOW_EXTERNAL_LLM env, interactive
  prompt); vision_analysis raises instead of silently sending PHI.
- #29: stop logging the raw patient name to stdout — log a non-reversible
  hash of the patient ID instead.
- #30: stop echoing portal access codes into ValueError messages.
- #33: add a Pydantic AnalyzeRequest schema with validation for
  POST /api/analyze (auth was already added in #46).

Docs: SECURITY.md "Handling of Patient Data" section, README + .env.example.
Tests: +5 (consent gate, credential redaction, request validation); 93 passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: address CodeRabbit review on #47

- ingest: stop logging study description (free-text DICOM field may contain PHI)
- vision_analysis: honour explicit LLM provider preference (wires up --model)
  instead of hardcoding "claude"; default to on-device "local" without consent
  so offline execution stays reachable
- docs: soften compliance/anonymity wording (drop "(HIPAA/GDPR)" assertion and
  "non-reversible" claim; describe pseudonymisation accurately)

94 tests passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: keep 'local' as default vision provider regardless of consent

Consent (--allow-cloud-llm / MEDCHECK_ALLOW_EXTERNAL_LLM) now only permits
falling back to an external provider; it no longer flips the implicit default
from on-device 'local' to 'claude'. Addresses CodeRabbit review on #47.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: LLM resiliency, non-root Docker, vendored htmx, crypto-comment clarity

Addresses four Medium-severity reliability/supply-chain issues:

- #38: add configurable timeout (MEDCHECK_LLM_TIMEOUT) + retry with exponential
  backoff (MEDCHECK_LLM_RETRIES) to all three LLM providers via a shared
  call_with_retries helper; transient failures no longer crash the pipeline and
  surface as a clear LLMProviderError.
- #39: run Docker containers as a non-root 'medcheck' user.
- #37: vendor htmx locally (src/medcheck/web/static/htmx.min.js) with an SRI
  hash instead of loading from the unpkg CDN; air-gapped friendly.
- #34: clarify the misleading bandit nosec on the Crypto import (pycryptodome,
  not pycrypto; B413 can't distinguish the shared namespace) and add an AES-CBC
  round-trip test that verifies the decryption path.

102 tests passing; coverage 67%.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: address CI + CodeRabbit review on #48

- base: only retry transient errors (timeouts/connection/429/5xx), fail fast on
  permanent ones (auth/4xx); add is_transient_error() helper
- claude/openai: pass max_retries=0 so call_with_retries is the sole retry
  controller (avoids SDK retry stacking)
- remove unused `# noqa: BLE001` directive (RUF100 lint failure)
- add fake-SDK provider tests (claude/openai/gemini happy path + missing key),
  lifting LLM provider patch coverage to ~100% (fixes codecov/patch)
- web test now also asserts the htmx SRI integrity + crossorigin attributes

113 tests passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: guard call_with_retries against attempts<=0 (avoid UnboundLocalError)

Coerce the attempt count to >=1 so the loop always runs once and failures wrap
as LLMProviderError instead of raising UnboundLocalError. Addresses CodeRabbit
review on #48.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

---------

Co-authored-by: Claude <noreply@anthropic.com>
Liohtml added a commit that referenced this pull request May 31, 2026
* feat: anatomy template loader, provider/model discovery CLI, doc fixes

Addresses three previously unaddressed issues:

- #43: build_prompt() now loads detailed anatomy templates from
  prompts/anatomy/*.txt (knee/shoulder/spine), with cached lookup and
  fallback to built-in hints. The shipped templates were dead code before.
  Adds an abdomen hint so all README-advertised regions are covered.
- #44: add `medcheck providers` and `medcheck models` discovery commands.
- #42: fix README "Report Bug" link (bug_report.md -> bug_report.yml).

Updates CHANGELOG and README; adds unit tests for the new loader and CLI
commands (72 passing).

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: satisfy ruff format and harden anatomy template path against traversal

- Add missing blank line so `ruff format --check` passes (lint CI was red).
- Slug-validate the anatomy region before building a filesystem path in
  load_anatomy_instructions(), preventing path traversal (CodeRabbit review).
- Add regression test for the traversal guard.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix(security): harden web server, validate easyRadiology downloads, add offline provider stub

Addresses three High-severity issues:

- #26: web server now binds 127.0.0.1 by default (was 0.0.0.0); add optional
  X-API-Key auth on /api endpoints via MEDCHECK_API_KEY, and warn when binding
  to a non-loopback host without a key configured.
- #28: validate the easyRadiology linkToERI download URL (HTTPS + host
  allowlist) and disable redirects to prevent SSRF to internal/metadata hosts.
- #36: add LocalLLMProvider stub so the advertised "local" offline fallback
  degrades gracefully (check_available()=False, actionable NotImplementedError)
  instead of crashing with "No LLM provider available"; register it in the
  vision router and `medcheck models`.

Updates README, .env.example, CHANGELOG; adds unit tests for all three
(89 passing, coverage 66%).

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix(security): PHI consent gate, log/credential redaction, API input validation

Addresses four Medium/High-severity privacy issues:

- #27: gate external (cloud) LLM transmission behind explicit consent
  (--allow-cloud-llm flag, MEDCHECK_ALLOW_EXTERNAL_LLM env, interactive
  prompt); vision_analysis raises instead of silently sending PHI.
- #29: stop logging the raw patient name to stdout — log a non-reversible
  hash of the patient ID instead.
- #30: stop echoing portal access codes into ValueError messages.
- #33: add a Pydantic AnalyzeRequest schema with validation for
  POST /api/analyze (auth was already added in #46).

Docs: SECURITY.md "Handling of Patient Data" section, README + .env.example.
Tests: +5 (consent gate, credential redaction, request validation); 93 passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: address CodeRabbit review on #47

- ingest: stop logging study description (free-text DICOM field may contain PHI)
- vision_analysis: honour explicit LLM provider preference (wires up --model)
  instead of hardcoding "claude"; default to on-device "local" without consent
  so offline execution stays reachable
- docs: soften compliance/anonymity wording (drop "(HIPAA/GDPR)" assertion and
  "non-reversible" claim; describe pseudonymisation accurately)

94 tests passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: keep 'local' as default vision provider regardless of consent

Consent (--allow-cloud-llm / MEDCHECK_ALLOW_EXTERNAL_LLM) now only permits
falling back to an external provider; it no longer flips the implicit default
from on-device 'local' to 'claude'. Addresses CodeRabbit review on #47.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: LLM resiliency, non-root Docker, vendored htmx, crypto-comment clarity

Addresses four Medium-severity reliability/supply-chain issues:

- #38: add configurable timeout (MEDCHECK_LLM_TIMEOUT) + retry with exponential
  backoff (MEDCHECK_LLM_RETRIES) to all three LLM providers via a shared
  call_with_retries helper; transient failures no longer crash the pipeline and
  surface as a clear LLMProviderError.
- #39: run Docker containers as a non-root 'medcheck' user.
- #37: vendor htmx locally (src/medcheck/web/static/htmx.min.js) with an SRI
  hash instead of loading from the unpkg CDN; air-gapped friendly.
- #34: clarify the misleading bandit nosec on the Crypto import (pycryptodome,
  not pycrypto; B413 can't distinguish the shared namespace) and add an AES-CBC
  round-trip test that verifies the decryption path.

102 tests passing; coverage 67%.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: address CI + CodeRabbit review on #48

- base: only retry transient errors (timeouts/connection/429/5xx), fail fast on
  permanent ones (auth/4xx); add is_transient_error() helper
- claude/openai: pass max_retries=0 so call_with_retries is the sole retry
  controller (avoids SDK retry stacking)
- remove unused `# noqa: BLE001` directive (RUF100 lint failure)
- add fake-SDK provider tests (claude/openai/gemini happy path + missing key),
  lifting LLM provider patch coverage to ~100% (fixes codecov/patch)
- web test now also asserts the htmx SRI integrity + crossorigin attributes

113 tests passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: guard call_with_retries against attempts<=0 (avoid UnboundLocalError)

Coerce the attempt count to >=1 so the loop always runs once and failures wrap
as LLMProviderError instead of raising UnboundLocalError. Addresses CodeRabbit
review on #48.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix(#31): do not gate easyRadiology auth on unused date of birth

authenticate() required a non-empty dob, implying DOB verification — but dob is
never sent to the portal or checked by this client. Gating on it created a false
security boundary. Authentication now relies on the access code only; dob remains
accepted but is documented as not verified.

Updates README + .env.example to stop overstating DOB verification.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* docs(#31): align .env.example DOB wording with code (not verified)

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

---------

Co-authored-by: Claude <noreply@anthropic.com>
Liohtml added a commit that referenced this pull request May 31, 2026
…, #12) (#50)

* feat: anatomy template loader, provider/model discovery CLI, doc fixes

Addresses three previously unaddressed issues:

- #43: build_prompt() now loads detailed anatomy templates from
  prompts/anatomy/*.txt (knee/shoulder/spine), with cached lookup and
  fallback to built-in hints. The shipped templates were dead code before.
  Adds an abdomen hint so all README-advertised regions are covered.
- #44: add `medcheck providers` and `medcheck models` discovery commands.
- #42: fix README "Report Bug" link (bug_report.md -> bug_report.yml).

Updates CHANGELOG and README; adds unit tests for the new loader and CLI
commands (72 passing).

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: satisfy ruff format and harden anatomy template path against traversal

- Add missing blank line so `ruff format --check` passes (lint CI was red).
- Slug-validate the anatomy region before building a filesystem path in
  load_anatomy_instructions(), preventing path traversal (CodeRabbit review).
- Add regression test for the traversal guard.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix(security): harden web server, validate easyRadiology downloads, add offline provider stub

Addresses three High-severity issues:

- #26: web server now binds 127.0.0.1 by default (was 0.0.0.0); add optional
  X-API-Key auth on /api endpoints via MEDCHECK_API_KEY, and warn when binding
  to a non-loopback host without a key configured.
- #28: validate the easyRadiology linkToERI download URL (HTTPS + host
  allowlist) and disable redirects to prevent SSRF to internal/metadata hosts.
- #36: add LocalLLMProvider stub so the advertised "local" offline fallback
  degrades gracefully (check_available()=False, actionable NotImplementedError)
  instead of crashing with "No LLM provider available"; register it in the
  vision router and `medcheck models`.

Updates README, .env.example, CHANGELOG; adds unit tests for all three
(89 passing, coverage 66%).

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix(security): PHI consent gate, log/credential redaction, API input validation

Addresses four Medium/High-severity privacy issues:

- #27: gate external (cloud) LLM transmission behind explicit consent
  (--allow-cloud-llm flag, MEDCHECK_ALLOW_EXTERNAL_LLM env, interactive
  prompt); vision_analysis raises instead of silently sending PHI.
- #29: stop logging the raw patient name to stdout — log a non-reversible
  hash of the patient ID instead.
- #30: stop echoing portal access codes into ValueError messages.
- #33: add a Pydantic AnalyzeRequest schema with validation for
  POST /api/analyze (auth was already added in #46).

Docs: SECURITY.md "Handling of Patient Data" section, README + .env.example.
Tests: +5 (consent gate, credential redaction, request validation); 93 passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: address CodeRabbit review on #47

- ingest: stop logging study description (free-text DICOM field may contain PHI)
- vision_analysis: honour explicit LLM provider preference (wires up --model)
  instead of hardcoding "claude"; default to on-device "local" without consent
  so offline execution stays reachable
- docs: soften compliance/anonymity wording (drop "(HIPAA/GDPR)" assertion and
  "non-reversible" claim; describe pseudonymisation accurately)

94 tests passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: keep 'local' as default vision provider regardless of consent

Consent (--allow-cloud-llm / MEDCHECK_ALLOW_EXTERNAL_LLM) now only permits
falling back to an external provider; it no longer flips the implicit default
from on-device 'local' to 'claude'. Addresses CodeRabbit review on #47.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: LLM resiliency, non-root Docker, vendored htmx, crypto-comment clarity

Addresses four Medium-severity reliability/supply-chain issues:

- #38: add configurable timeout (MEDCHECK_LLM_TIMEOUT) + retry with exponential
  backoff (MEDCHECK_LLM_RETRIES) to all three LLM providers via a shared
  call_with_retries helper; transient failures no longer crash the pipeline and
  surface as a clear LLMProviderError.
- #39: run Docker containers as a non-root 'medcheck' user.
- #37: vendor htmx locally (src/medcheck/web/static/htmx.min.js) with an SRI
  hash instead of loading from the unpkg CDN; air-gapped friendly.
- #34: clarify the misleading bandit nosec on the Crypto import (pycryptodome,
  not pycrypto; B413 can't distinguish the shared namespace) and add an AES-CBC
  round-trip test that verifies the decryption path.

102 tests passing; coverage 67%.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: address CI + CodeRabbit review on #48

- base: only retry transient errors (timeouts/connection/429/5xx), fail fast on
  permanent ones (auth/4xx); add is_transient_error() helper
- claude/openai: pass max_retries=0 so call_with_retries is the sole retry
  controller (avoids SDK retry stacking)
- remove unused `# noqa: BLE001` directive (RUF100 lint failure)
- add fake-SDK provider tests (claude/openai/gemini happy path + missing key),
  lifting LLM provider patch coverage to ~100% (fixes codecov/patch)
- web test now also asserts the htmx SRI integrity + crossorigin attributes

113 tests passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: guard call_with_retries against attempts<=0 (avoid UnboundLocalError)

Coerce the attempt count to >=1 so the loop always runs once and failures wrap
as LLMProviderError instead of raising UnboundLocalError. Addresses CodeRabbit
review on #48.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix(#31): do not gate easyRadiology auth on unused date of birth

authenticate() required a non-empty dob, implying DOB verification — but dob is
never sent to the portal or checked by this client. Gating on it created a false
security boundary. Authentication now relies on the access code only; dob remains
accepted but is documented as not verified.

Updates README + .env.example to stop overstating DOB verification.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* docs(#31): align .env.example DOB wording with code (not verified)

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* feat: add hip, ankle/foot, and wrist anatomy prompt templates

Closes #10, #11, #12. Adds detailed MRI analysis templates (modelled on knee.txt)
that the prompt loader picks up automatically, and extends detect_anatomy() with
hip/ankle/wrist keyword patterns (incl. German terms). Removes the over-broad
"gelenk" keyword from the knee pattern so it no longer mis-matches Handgelenk
(wrist) / Sprunggelenk (ankle); "Knie" still matches knee.

115 tests passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

---------

Co-authored-by: Claude <noreply@anthropic.com>
Liohtml added a commit that referenced this pull request Jun 11, 2026
…el IDs (#75)

* feat: anatomy template loader, provider/model discovery CLI, doc fixes

Addresses three previously unaddressed issues:

- #43: build_prompt() now loads detailed anatomy templates from
  prompts/anatomy/*.txt (knee/shoulder/spine), with cached lookup and
  fallback to built-in hints. The shipped templates were dead code before.
  Adds an abdomen hint so all README-advertised regions are covered.
- #44: add `medcheck providers` and `medcheck models` discovery commands.
- #42: fix README "Report Bug" link (bug_report.md -> bug_report.yml).

Updates CHANGELOG and README; adds unit tests for the new loader and CLI
commands (72 passing).

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: satisfy ruff format and harden anatomy template path against traversal

- Add missing blank line so `ruff format --check` passes (lint CI was red).
- Slug-validate the anatomy region before building a filesystem path in
  load_anatomy_instructions(), preventing path traversal (CodeRabbit review).
- Add regression test for the traversal guard.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix(security): harden web server, validate easyRadiology downloads, add offline provider stub

Addresses three High-severity issues:

- #26: web server now binds 127.0.0.1 by default (was 0.0.0.0); add optional
  X-API-Key auth on /api endpoints via MEDCHECK_API_KEY, and warn when binding
  to a non-loopback host without a key configured.
- #28: validate the easyRadiology linkToERI download URL (HTTPS + host
  allowlist) and disable redirects to prevent SSRF to internal/metadata hosts.
- #36: add LocalLLMProvider stub so the advertised "local" offline fallback
  degrades gracefully (check_available()=False, actionable NotImplementedError)
  instead of crashing with "No LLM provider available"; register it in the
  vision router and `medcheck models`.

Updates README, .env.example, CHANGELOG; adds unit tests for all three
(89 passing, coverage 66%).

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix(security): PHI consent gate, log/credential redaction, API input validation

Addresses four Medium/High-severity privacy issues:

- #27: gate external (cloud) LLM transmission behind explicit consent
  (--allow-cloud-llm flag, MEDCHECK_ALLOW_EXTERNAL_LLM env, interactive
  prompt); vision_analysis raises instead of silently sending PHI.
- #29: stop logging the raw patient name to stdout — log a non-reversible
  hash of the patient ID instead.
- #30: stop echoing portal access codes into ValueError messages.
- #33: add a Pydantic AnalyzeRequest schema with validation for
  POST /api/analyze (auth was already added in #46).

Docs: SECURITY.md "Handling of Patient Data" section, README + .env.example.
Tests: +5 (consent gate, credential redaction, request validation); 93 passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: address CodeRabbit review on #47

- ingest: stop logging study description (free-text DICOM field may contain PHI)
- vision_analysis: honour explicit LLM provider preference (wires up --model)
  instead of hardcoding "claude"; default to on-device "local" without consent
  so offline execution stays reachable
- docs: soften compliance/anonymity wording (drop "(HIPAA/GDPR)" assertion and
  "non-reversible" claim; describe pseudonymisation accurately)

94 tests passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: keep 'local' as default vision provider regardless of consent

Consent (--allow-cloud-llm / MEDCHECK_ALLOW_EXTERNAL_LLM) now only permits
falling back to an external provider; it no longer flips the implicit default
from on-device 'local' to 'claude'. Addresses CodeRabbit review on #47.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: LLM resiliency, non-root Docker, vendored htmx, crypto-comment clarity

Addresses four Medium-severity reliability/supply-chain issues:

- #38: add configurable timeout (MEDCHECK_LLM_TIMEOUT) + retry with exponential
  backoff (MEDCHECK_LLM_RETRIES) to all three LLM providers via a shared
  call_with_retries helper; transient failures no longer crash the pipeline and
  surface as a clear LLMProviderError.
- #39: run Docker containers as a non-root 'medcheck' user.
- #37: vendor htmx locally (src/medcheck/web/static/htmx.min.js) with an SRI
  hash instead of loading from the unpkg CDN; air-gapped friendly.
- #34: clarify the misleading bandit nosec on the Crypto import (pycryptodome,
  not pycrypto; B413 can't distinguish the shared namespace) and add an AES-CBC
  round-trip test that verifies the decryption path.

102 tests passing; coverage 67%.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: address CI + CodeRabbit review on #48

- base: only retry transient errors (timeouts/connection/429/5xx), fail fast on
  permanent ones (auth/4xx); add is_transient_error() helper
- claude/openai: pass max_retries=0 so call_with_retries is the sole retry
  controller (avoids SDK retry stacking)
- remove unused `# noqa: BLE001` directive (RUF100 lint failure)
- add fake-SDK provider tests (claude/openai/gemini happy path + missing key),
  lifting LLM provider patch coverage to ~100% (fixes codecov/patch)
- web test now also asserts the htmx SRI integrity + crossorigin attributes

113 tests passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: guard call_with_retries against attempts<=0 (avoid UnboundLocalError)

Coerce the attempt count to >=1 so the loop always runs once and failures wrap
as LLMProviderError instead of raising UnboundLocalError. Addresses CodeRabbit
review on #48.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix(#31): do not gate easyRadiology auth on unused date of birth

authenticate() required a non-empty dob, implying DOB verification — but dob is
never sent to the portal or checked by this client. Gating on it created a false
security boundary. Authentication now relies on the access code only; dob remains
accepted but is documented as not verified.

Updates README + .env.example to stop overstating DOB verification.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* docs(#31): align .env.example DOB wording with code (not verified)

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* feat: add hip, ankle/foot, and wrist anatomy prompt templates

Closes #10, #11, #12. Adds detailed MRI analysis templates (modelled on knee.txt)
that the prompt loader picks up automatically, and extends detect_anatomy() with
hip/ankle/wrist keyword patterns (incl. German terms). Removes the over-broad
"gelenk" keyword from the knee pattern so it no longer mis-matches Handgelenk
(wrist) / Sprunggelenk (ankle); "Knie" still matches knee.

115 tests passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* docs: add open-source options research report

Verified (against primary sources) survey of open-source resources MedCheck can
adopt, mapped to the open feature issues:
- local medical VLMs (#18): recommend Lingshu-7B (MIT, MRI-capable) over the
  research-only LLaVA-Med
- DICOMDIR/CD handling (#25): pydicom FileSet + pylibjpeg/gdcm
- DICOMweb providers (#13 Orthanc, #14 Google Cloud Healthcare)
- #15 re-scope: OHIF is a viewer, not a data provider
- ML frameworks (MONAI/TorchIO), reporting standards (FHIR/DICOM SR), datasets

Includes license compatibility analysis for Apache-2.0 and flags research-only /
non-commercial weights.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* chore: remove session-only research notes from repo

The open-source options survey was working material for planning; it does not
belong in the public repository. Findings are being applied directly as code.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

* fix: honour MEDCHECK_LLM_PROVIDER, validate LLM findings, correct model IDs

Three correctness bugs from the repo-health sweep:

- #73: the `analyze` command now falls back to Settings.default_llm_provider
  (MEDCHECK_LLM_PROVIDER) when --model is omitted, instead of silently using the
  offline `local` provider (which raises NotImplementedError).
- #71: parse_llm_response now defensively coerces each LLM-returned structure
  (known fields only, type coercion, confidence clamped to [0,1], empty entries
  dropped) instead of StructureFinding(**s), which crashed on unexpected keys and
  rendered fabricated high-confidence findings verbatim.
- #68/#69: default Claude model ID corrected claude-opus-4-7 -> claude-opus-4-8
  and made overridable via MEDCHECK_{CLAUDE,OPENAI,GEMINI}_MODEL; updated all
  README/docs/web-UI references.

130 tests passing.

https://claude.ai/code/session_01KYcUxCGAHaTPrRHTzJCWQz

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants