Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions .github/workflows/finalize-security-fixes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
name: Finalize security fixes

on:
push:
branches: [security-fixes-3451e6e]

permissions:
contents: write

jobs:
finalize:
if: github.event.head_commit.message != 'Security fixes: finalize verified changes'
runs-on: ubuntu-latest
steps:
- name: Checkout branch
uses: actions/checkout@v4
with:
ref: security-fixes-3451e6e
fetch-depth: 1

- name: Apply exact security fixes
shell: bash
run: |
set -euo pipefail
python - <<'PY'
from pathlib import Path
import re

# Remove only OCR dictionary entries whose keys contain a trailing separator.
p = Path('hf-space/app_core.py')
s = p.read_text(encoding='utf-8')
old = s
for wrong, right in [
('ديكلوفيناك ', 'ديكلوفيناك'),
('اوميبرازول ', 'اوميبرازول'),
('سيليبريكس ', 'سيليبريكس'),
('ترامادول ', 'ترامادول'),
('كاتافلام ', 'كاتافلام'),
('نوفافين ', 'نوفافين'),
('فلاميكس ', 'فلاميكس'),
('بنادول ', 'بنادول'),
('ادفيل ', 'ادفيل'),
]:
s = s.replace(f'"{wrong}": "{right}"', f'"{right}": "{right}"')
p.write_text(s, encoding='utf-8')

# Authenticate the admin stub.
p = Path('app/routers/admin.py')
s = p.read_text(encoding='utf-8')
if 'Depends(get_current_user)' not in s:
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():', 'async def system_status(current_user: User = Depends(get_current_user)):', 1)
p.write_text(s, encoding='utf-8')

# Separate alternate full-stack deployment; never overwrite root compose.
old = Path('deploy/docker-compose.prod.yml')
new = Path('deploy/docker-compose.full.yml')
if old.exists() and not new.exists():
old.rename(new)
if not new.exists():
raise SystemExit('missing deploy/docker-compose.full.yml')

p = Path('deploy/deploy.sh')
s = p.read_text(encoding='utf-8')
if 'cp "${DEPLOY_DIR}/docker-compose.prod.yml" "${PROJECT_DIR}/docker-compose.prod.yml"' in s:
s = s.replace('if [[ -f "${DEPLOY_DIR}/docker-compose.prod.yml" ]]; then\n cp "${DEPLOY_DIR}/docker-compose.prod.yml" "${PROJECT_DIR}/docker-compose.prod.yml"\nfi', 'COMPOSE_FILE="${DEPLOY_DIR}/docker-compose.full.yml"\nif [[ ! -f "$COMPOSE_FILE" ]]; then\n error "Full-stack compose file not found: $COMPOSE_FILE"\nfi')
if 'COMPOSE_FILE=' not in s:
marker = 'DEPLOY_DIR="$(dirname "$0")"\n'
s = s.replace(marker, marker + 'COMPOSE_FILE="${DEPLOY_DIR}/docker-compose.full.yml"\n', 1)
s = s.replace('docker compose -f docker-compose.prod.yml', 'docker compose --project-directory "$PROJECT_DIR" -f "$COMPOSE_FILE"')
p.write_text(s, encoding='utf-8')
PY

- name: Add real behavioral 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: Execute real tests and source checks
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
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
python - <<'PY'
import ast
from pathlib import Path
ast.parse(Path('hf-space/app_core.py').read_text(encoding='utf-8'))
ast.parse(Path('app/routers/admin.py').read_text(encoding='utf-8'))
PY

- name: Show diff
run: git diff --check && git diff --stat && git status --short

- name: Commit verified fixes
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 add hf-space/app_core.py app/routers/admin.py deploy/deploy.sh deploy/docker-compose.full.yml tests/security/test_medical_behavior.py .github/workflows/finalize-security-fixes.yml
git status --short
git commit -m 'Security fixes: finalize verified changes'
git push origin security-fixes-3451e6e
6 changes: 4 additions & 2 deletions app/routers/admin.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# app/routers/admin.py
"""Admin Router - Stub for future implementation"""
from fastapi import APIRouter
from fastapi import APIRouter, Depends
from app.core.security import get_current_user
from app.db.models.auth import User

router = APIRouter()

@router.get("/system-status")
async def system_status():
async def system_status(current_user: User = Depends(get_current_user)):
return {"status": "healthy", "message": "Admin endpoints - coming soon"}
10 changes: 5 additions & 5 deletions app/services/ocr_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@
"باراسيتبمول": "باراسيتامول", "ايبوروفين": "ايبوبروفين",
"اموكسيستلين": "اموكسيسيلين", "اموكسيسلين": "اموكسيسيلين",
"ازيثروميسين": "ازيثرومايسين", "ميتروندازول": "ميترونيدازول",
"ديكلوفيناك ": "ديكلوفيناك", "اوجمينتين": "اوجمنتين",
"اوميبرازول ": "اوميبرازول", "سيليبريكس ": "سيليبريكس",
"ترامادول ": "ترامادول", "كاتافلام ": "كاتافلام",
"نوفافين ": "نوفافين", "فلاميكس ": "فلاميكس",
"بنادول ": "بنادول", "ادفيل ": "ادفيل",
"اوجمينتين": "اوجمنتين",
"اوميبرازول": "اوميبرازول", "سيليبريكس": "سيليبريكس",
"كاتافلام": "كاتافلام",
"فلاميكس": "فلاميكس",
"بنادول": "بنادول", "ادفيل": "ادفيل",
}

# ── Lazy singletons (thread-safe) ────────────────────────────────────────────
Expand Down
27 changes: 14 additions & 13 deletions deploy/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -287,11 +287,12 @@ info "═══ Phase 4: Build & Launch ═══"
# Copy production deployment files
info "Copying production deployment files..."
DEPLOY_DIR="$(dirname "$0")"
COMPOSE_FILE="${DEPLOY_DIR}/docker-compose.full.yml"
if [[ -f "${DEPLOY_DIR}/Dockerfile" ]]; then
cp "${DEPLOY_DIR}/Dockerfile" "${PROJECT_DIR}/Dockerfile"
fi
if [[ -f "${DEPLOY_DIR}/docker-compose.prod.yml" ]]; then
cp "${DEPLOY_DIR}/docker-compose.prod.yml" "${PROJECT_DIR}/docker-compose.prod.yml"
if [[ ! -f "$COMPOSE_FILE" ]]; then
error "Full-stack compose file not found: $COMPOSE_FILE"
fi
if [[ -f "${DEPLOY_DIR}/docker-entrypoint.sh" ]]; then
cp "${DEPLOY_DIR}/docker-entrypoint.sh" "${PROJECT_DIR}/docker-entrypoint.sh"
Expand All @@ -301,12 +302,12 @@ ok "Deployment files copied"

# Build Docker images
info "Building Docker images (this may take 10-20 minutes)..."
docker compose -f docker-compose.prod.yml build --no-cache 2>&1 | tail -20
docker compose --project-directory "$PROJECT_DIR" -f "$COMPOSE_FILE" build --no-cache 2>&1 | tail -20
ok "Docker images built"

# Launch services
info "Starting all services..."
docker compose -f docker-compose.prod.yml up -d
docker compose --project-directory "$PROJECT_DIR" -f "$COMPOSE_FILE" up -d
ok "Services started"

# Wait for health checks
Expand All @@ -316,7 +317,7 @@ sleep 10
MAX_WAIT=60
ELAPSED=0
while [[ $ELAPSED -lt $MAX_WAIT ]]; do
UNHEALTHY=$(docker compose -f docker-compose.prod.yml ps --format json 2>/dev/null | jq -r 'select(.Health != "healthy" and .Health != null) | .Name' 2>/dev/null | wc -l || echo "0")
UNHEALTHY=$(docker compose --project-directory "$PROJECT_DIR" -f "$COMPOSE_FILE" ps --format json 2>/dev/null | jq -r 'select(.Health != "healthy" and .Health != null) | .Name' 2>/dev/null | wc -l || echo "0")
if [[ "$UNHEALTHY" -eq 0 ]]; then
break
fi
Expand All @@ -333,7 +334,7 @@ info "═══ Phase 5: Verification ═══"

# Show service status
info "Docker services status:"
docker compose -f docker-compose.prod.yml ps
docker compose --project-directory "$PROJECT_DIR" -f "$COMPOSE_FILE" ps

echo ""

Expand All @@ -344,7 +345,7 @@ if [[ "$HEALTH_STATUS" == "OK" ]]; then
ok "API health check: PASSED"
else
warn "API health check: FAILED (may still be starting up)"
warn "Run: docker compose -f docker-compose.prod.yml logs api"
warn "Run: docker compose --project-directory "$PROJECT_DIR" -f "$COMPOSE_FILE" logs api"
fi

# Verify HTTPS
Expand All @@ -362,7 +363,7 @@ fi
echo ""
info "Service Health Summary:"
for svc in api postgres redis qdrant; do
STATUS=$(docker compose -f docker-compose.prod.yml ps "$svc" --format json 2>/dev/null | jq -r '.Health // "unknown"' 2>/dev/null || echo "unknown")
STATUS=$(docker compose --project-directory "$PROJECT_DIR" -f "$COMPOSE_FILE" ps "$svc" --format json 2>/dev/null | jq -r '.Health // "unknown"' 2>/dev/null || echo "unknown")
if [[ "$STATUS" == "healthy" ]]; then
ok "$svc: $STATUS"
else
Expand All @@ -384,13 +385,13 @@ echo " ❤️ Health: https://${DOMAIN}/health"
echo " 📊 API (direct): http://localhost:8000"
echo ""
echo " 📁 Project: ${PROJECT_DIR}"
echo " ⚙️ Compose: docker compose -f docker-compose.prod.yml"
echo " ⚙️ Compose: docker compose --project-directory "$PROJECT_DIR" -f "$COMPOSE_FILE""
echo ""
echo " Useful commands:"
echo " docker compose -f docker-compose.prod.yml ps # Check status"
echo " docker compose -f docker-compose.prod.yml logs -f api # View API logs"
echo " docker compose -f docker-compose.prod.yml restart api # Restart API"
echo " docker compose -f docker-compose.prod.yml down # Stop all"
echo " docker compose --project-directory "$PROJECT_DIR" -f "$COMPOSE_FILE" ps # Check status"
echo " docker compose --project-directory "$PROJECT_DIR" -f "$COMPOSE_FILE" logs -f api # View API logs"
echo " docker compose --project-directory "$PROJECT_DIR" -f "$COMPOSE_FILE" restart api # Restart API"
echo " docker compose --project-directory "$PROJECT_DIR" -f "$COMPOSE_FILE" down # Stop all"
echo ""
echo " Security checklist:"
echo " [✓] HTTPS enabled (Let's Encrypt)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
# qdrant — Qdrant vector DB (for search_service.py)
#
# Usage:
# docker compose -f docker-compose.prod.yml up -d --build
# docker compose -f docker-compose.prod.yml ps
# docker compose -f docker-compose.prod.yml logs -f gradio
# docker compose -f deploy/docker-compose.full.yml up -d --build
# docker compose -f deploy/docker-compose.full.yml ps
# docker compose -f deploy/docker-compose.full.yml logs -f gradio
# =============================================================================

services:
Expand Down
10 changes: 5 additions & 5 deletions hf-space/app_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,11 @@
"باراسيتبمول": "باراسيتامول", "ايبوروفين": "ايبوبروفين",
"اموكسيستلين": "اموكسيسيلين", "اموكسيسلين": "اموكسيسيلين",
"ازيثروميسين": "ازيثرومايسين", "ميتروندازول": "ميترونيدازول",
"ديكلوفيناك ": "ديكلوفيناك", "اوجمينتين": "اوجمنتين",
"اوميبرازول ": "اوميبرازول", "سيليبريكس ": "سيليبريكس",
"ترامادول ": "ترامادول", "كاتافلام ": "كاتافلام",
"نوفافين ": "نوفافين", "فلاميكس ": "فلاميكس",
"بنادول ": "بنادول", "ادفيل ": "ادفيل",
"اوجمينتين": "اوجمنتين",
"اوميبرازول": "اوميبرازول", "سيليبريكس": "سيليبريكس",
"كاتافلام": "كاتافلام",
"فلاميكس": "فلاميكس",
"بنادول": "بنادول", "ادفيل": "ادفيل",
}

# ── Lazy-loaded modules ─────────────────────────────────────────────────────
Expand Down
28 changes: 28 additions & 0 deletions tests/security/test_medical_behavior.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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"
Loading