Repository navigation
Security fixes: medical OCR separator, admin auth, deployment topology #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Apply security fixes (temporary) | |
| on: | |
| pull_request: | |
| branches: [main] | |
| permissions: | |
| contents: write | |
| jobs: | |
| apply: | |
| if: github.event.pull_request.head.ref == 'security-fixes-3451e6e' && github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout PR branch | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.head_ref }} | |
| fetch-depth: 1 | |
| - name: Apply fixes to real files | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python - <<'PY' | |
| from pathlib import Path | |
| # 1) Medical OCR: remove identity mappings whose keys contain a trailing | |
| # separator. The previous mappings consumed the separator via str.replace. | |
| p = Path('hf-space/app_core.py') | |
| s = p.read_text(encoding='utf-8') | |
| replacements = { | |
| ' "ديكلوفيناك ": "ديكلوفيناك", "اوجمينتين": "اوجمنتين",': ' "اوجمينتين": "اوجمنتين",', | |
| ' "اوميبرازول ": "اوميبرازول", "سيليبريكس ": "سيليبريكس",': ' "اوميبرازول": "اوميبرازول", "سيليبريكس": "سيليبريكس",', | |
| ' "ترامادول ": "ترامادول", "كاتافلام ": "كاتافلام",': ' "كاتافلام": "كاتافلام",', | |
| ' "نوفافين ": "نوفافين", "فلاميكس ": "فلاميكس",': ' "فلاميكس": "فلاميكس",', | |
| ' "بنادول ": "بنادول", "ادفيل ": "ادفيل",': ' "بنادول": "بنادول", "ادفيل": "ادفيل",', | |
| } | |
| for old, new in replacements.items(): | |
| if old not in s: | |
| raise SystemExit(f'Missing expected app_core pattern: {old!r}') | |
| s = s.replace(old, new, 1) | |
| p.write_text(s, encoding='utf-8') | |
| # 2) Authenticate the admin system-status stub using the repository's | |
| # existing dependency. | |
| p = Path('app/routers/admin.py') | |
| s = p.read_text(encoding='utf-8') | |
| s = s.replace('from fastapi import APIRouter\n', 'from fastapi import APIRouter, Depends\nfrom app.core.security import get_current_user\nfrom app.db.models.auth import User\n', 1) | |
| s = s.replace('async def system_status():\n', 'async def system_status(current_user: User = Depends(get_current_user)):\n', 1) | |
| p.write_text(s, encoding='utf-8') | |
| # 3) Separate the alternate full-stack deployment from the CI/HF | |
| # canonical compose file without changing its project-root semantics. | |
| old = Path('deploy/docker-compose.prod.yml') | |
| new = Path('deploy/docker-compose.full.yml') | |
| if not old.exists(): | |
| raise SystemExit('deploy/docker-compose.prod.yml is missing') | |
| if new.exists(): | |
| raise SystemExit('deploy/docker-compose.full.yml already exists') | |
| old.rename(new) | |
| p = Path('deploy/deploy.sh') | |
| s = p.read_text(encoding='utf-8') | |
| expected = '''DEPLOY_DIR="$(dirname "$0")"\nif [[ -f "${DEPLOY_DIR}/Dockerfile" ]]; then\n cp "${DEPLOY_DIR}/Dockerfile" "${PROJECT_DIR}/Dockerfile"\nfi\nif [[ -f "${DEPLOY_DIR}/docker-compose.prod.yml" ]]; then\n cp "${DEPLOY_DIR}/docker-compose.prod.yml" "${PROJECT_DIR}/docker-compose.prod.yml"\nfi\n''' | |
| replacement = '''DEPLOY_DIR="$(dirname "$0")"\nCOMPOSE_FILE="${DEPLOY_DIR}/docker-compose.full.yml"\nif [[ -f "${DEPLOY_DIR}/Dockerfile" ]]; then\n cp "${DEPLOY_DIR}/Dockerfile" "${PROJECT_DIR}/Dockerfile"\nfi\nif [[ ! -f "$COMPOSE_FILE" ]]; then\n error "Full-stack compose file not found: $COMPOSE_FILE"\nfi\n''' | |
| if expected not in s: | |
| raise SystemExit('Expected deploy.sh compose-copy block not found') | |
| s = s.replace(expected, replacement, 1) | |
| s = s.replace('docker compose -f docker-compose.prod.yml', 'docker compose --project-directory "$PROJECT_DIR" -f "$COMPOSE_FILE"') | |
| s = s.replace('docker-compose.prod.yml', 'deploy/docker-compose.full.yml') | |
| # The previous replacement changes the COMPOSE_FILE path if applied to | |
| # its literal value; restore the actual variable definition. | |
| s = s.replace('COMPOSE_FILE="${DEPLOY_DIR}/deploy/docker-compose.full.yml"', 'COMPOSE_FILE="${DEPLOY_DIR}/docker-compose.full.yml"') | |
| p.write_text(s, encoding='utf-8') | |
| PY | |
| - name: Add behavioral medical regression tests | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p tests/security | |
| cat > tests/security/test_medical_behavior.py <<'PY' | |
| import sys | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[2] | |
| sys.path.insert(0, str(ROOT / 'hf-space')) | |
| import app_core | |
| def test_tramadol_dose_separator_preserved(): | |
| out, _ = app_core._auto_correct_ocr('ترامادول 0.5 mg') | |
| assert out == 'ترامادول 0.5 mg' | |
| def test_medical_negation_and_dose_preserved(): | |
| text = 'لا يعطى ترامادول 0.5 mg' | |
| out, _ = app_core._auto_correct_ocr(text) | |
| assert out == text | |
| def test_decimals_preserved(): | |
| for text in ['0.5', '1.25', '0.75', '٠٫٥', '١٫٢٥']: | |
| out, _ = app_core._auto_correct_ocr(text) | |
| assert out == text | |
| def test_negations_preserved(): | |
| for text in ['لا', 'ليس', 'لم', 'لن', 'غير', 'بدون', 'لا يوجد سكري']: | |
| out, _ = app_core._auto_correct_ocr(text) | |
| assert out == text | |
| def test_intended_ocr_correction_remains(): | |
| out, _ = app_core._auto_correct_ocr('باراسيتبمول 500 mg') | |
| assert out == 'باراسيتامول 500 mg' | |
| PY | |
| - name: Run targeted behavioral tests | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python -m pip install --disable-pip-version-check pytest numpy opencv-python-headless 'gradio<6' >/dev/null | |
| pytest -q tests/security/test_medical_behavior.py | |
| - name: Verify deployment references and admin source | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| test -f deploy/docker-compose.full.yml | |
| test -f docker-compose.prod.yml | |
| ! grep -q 'cp .*docker-compose.prod.yml.*docker-compose.prod.yml' deploy/deploy.sh | |
| grep -q 'docker-compose.full.yml' deploy/deploy.sh | |
| grep -q 'Depends(get_current_user)' app/routers/admin.py | |
| grep -q '"ترامادول": "ترامادول"' hf-space/app_core.py | |
| ! grep -q '"ترامادول ": "ترامادول"' hf-space/app_core.py | |
| - name: Commit verified fixes back to PR branch | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| git config user.name 'github-actions[bot]' | |
| git config user.email '41898282+github-actions[bot]@users.noreply.github.com' | |
| git status --short | |
| git diff --check | |
| git diff --stat | |
| git add hf-space/app_core.py app/routers/admin.py deploy/deploy.sh deploy/docker-compose.full.yml tests/security/test_medical_behavior.py | |
| git rm --cached deploy/docker-compose.prod.yml 2>/dev/null || true | |
| git add -A deploy/docker-compose.prod.yml deploy/docker-compose.full.yml | |
| if git diff --cached --quiet; then | |
| echo 'No changes to commit.' | |
| exit 0 | |
| fi | |
| git commit -m 'fix(security): harden medical OCR and production topology' | |
| git push origin HEAD:${{ github.head_ref }} |