diff --git a/.githooks/pre-push b/.githooks/pre-push index ea15115..2edef04 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,4 +1,4 @@ #!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" -exec python3 "$ROOT/scripts/check_safe_to_push.py" --public +exec python3 "$ROOT/scripts/check_safe_to_push.py" --public --push diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..abd2e58 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + - package-ecosystem: pip + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index 8f6a9f1..1d760f5 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -5,6 +5,9 @@ on: branches: [main] pull_request: +permissions: + contents: read + jobs: paths-and-secrets: runs-on: ubuntu-latest @@ -20,14 +23,43 @@ jobs: echo "Patient corpus files must not be in the public repo" exit 1 fi - for f in bot_config.json .env; do + if git ls-files '**/.env' | grep -v '\.env\.example$'; then + echo "Blocked .env file(s) in repo" + exit 1 + fi + if git ls-files '**/bot_config.json' 'bot_config.json' 2>/dev/null | grep -q .; then + echo "bot_config.json must not be committed" + exit 1 + fi + for f in sources incoming; do if git ls-files "$f" 2>/dev/null | grep -q .; then - echo "Blocked file in repo: $f" + echo "Blocked path in repo: $f" exit 1 fi done + - name: Scan tracked tree for secrets and PHI + run: python3 scripts/check_safe_to_push.py --public --scan-all + - name: Gitleaks uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + config-path: .gitleaks.toml + + dependency-audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Dependency audit + run: | + python -m pip install --upgrade pip + pip install pip-audit + pip install -e . + pip-audit diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..0e9eef9 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,16 @@ +title = "biohackbot gitleaks" + +[allowlist] +description = "Ignore mock patient tokens in public fixtures" +paths = [ + '''tests/fixtures/''', + '''examples/demo-instance/''', +] + +[[allowlist.regexes]] +description = "Mock US insurance id in fixtures" +regex = '''US-MOCK-INS-''' + +[[allowlist.regexes]] +description = "Mock deploy placeholders" +regex = '''TELEGRAM_BOT_TOKEN=\s*$''' diff --git a/README.md b/README.md index 71a2762..d54aedc 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ This creates: Edit `PATIENT_PROFILE.json`: ```json -{"dob": "1985-03-20", "full_name_ru": "Your Name"} +{"dob": "1985-06-15", "full_name_ru": "John Smith", "country": "USA"} ``` ## 3. Add documents diff --git a/README.ru.md b/README.ru.md index 2e31393..1e126fd 100644 --- a/README.ru.md +++ b/README.ru.md @@ -55,7 +55,7 @@ medbots init ~/my-health Отредактируйте `PATIENT_PROFILE.json`: ```json -{"dob": "1985-03-20", "full_name_ru": "Иван Иванов"} +{"dob": "1985-06-15", "full_name_ru": "John Smith", "country": "USA"} ``` ## 3. Добавление документов diff --git a/README.zh-CN.md b/README.zh-CN.md index cc97f15..2257ea0 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -57,7 +57,7 @@ medbots init ~/my-health 编辑 `PATIENT_PROFILE.json`: ```json -{"dob": "1985-03-20", "full_name_ru": "Your Name"} +{"dob": "1985-06-15", "full_name_ru": "John Smith", "country": "USA"} ``` ## 3. 添加文档 diff --git a/SECURITY.md b/SECURITY.md index 423b3fd..f8ce9a9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,12 +1,39 @@ # Security +## Public repository policy + **Never commit to this public repo:** -- API keys, bot tokens, `.env` files -- `bot_config.json` with real paths +- API keys, bot tokens, real `.env` files +- `bot_config.json` with instance-specific paths - Patient data: labs, genetics, clinical notes, `PATIENT_PROFILE.json`, `pdf_text/`, `doc_text/` -- Raw PDFs in `sources/` +- Raw PDFs in `sources/` or `incoming/` Keep health data in a local or private instance directory (see README). +## Automated controls + +| Control | Purpose | +|---------|---------| +| `scripts/check_safe_to_push.py --public --push` | Pre-push hook: scans outgoing commits (not just staged files) | +| `scripts/check_safe_to_push.py --public --scan-all` | CI: scans all tracked files | +| `.github/workflows/secret-scan.yml` | Gitleaks + path blocks + dependency audit | +| `.gitleaks.toml` | Allowlist for mock fixtures only | +| `deploy/lib/validate-deploy-path.sh` | Blocks shell metacharacter injection in deploy vars | + +Install hooks after clone: + +```bash +git config core.hooksPath .githooks +``` + +## Private VPS / Telegram bot + +- Store tokens in `~/.config/medbot-openclaw.env` with `chmod 600` +- Set `TELEGRAM_ALLOWED_USER_IDS` — never run the bot open to all Telegram users +- Corpus on VPS: `/opt/medbot-corpus` — restrict SSH and filesystem permissions +- Do not expose corpus directory via HTTP without authentication + +## Reporting + Report vulnerabilities via [GitHub Security Advisories](https://github.com/apodobe/biohackbot/security/advisories/new) — without attaching PHI or tokens. diff --git a/deploy/02-rsync-corpus.sh b/deploy/02-rsync-corpus.sh index 7358b6b..065fc43 100755 --- a/deploy/02-rsync-corpus.sh +++ b/deploy/02-rsync-corpus.sh @@ -1,15 +1,25 @@ #!/usr/bin/env bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=lib/validate-deploy-path.sh +source "$SCRIPT_DIR/lib/validate-deploy-path.sh" + VPS="${VPS:?Set VPS=root@your-host}" LOCAL="${CORPUS:?Set CORPUS=/path/to/structured_database}" REMOTE_DIR="${REMOTE_DIR:-/opt/medbot-corpus/structured_database}" + +validate_deploy_value "VPS" "$VPS" +validate_deploy_value "LOCAL" "$LOCAL" +validate_deploy_value "REMOTE_DIR" "$REMOTE_DIR" + echo "Local: $LOCAL" echo "Remote: $VPS:$REMOTE_DIR" -ssh "$VPS" "mkdir -p '$REMOTE_DIR'" +ssh -- "$VPS" mkdir -p -- "$REMOTE_DIR" rsync -avz --delete \ --exclude '.pytest_cache' \ --exclude '__pycache__' \ --exclude '*.pdf' \ "$LOCAL/" "$VPS:$REMOTE_DIR/" -ssh "$VPS" "mkdir -p /opt/medbot-corpus && chmod -R a+rX /opt/medbot-corpus" +ssh -- "$VPS" mkdir -p -- /opt/medbot-corpus +ssh -- "$VPS" chmod -R a+rX -- /opt/medbot-corpus echo "Done." diff --git a/deploy/03-install-skill-on-vps.sh b/deploy/03-install-skill-on-vps.sh index 16daf53..5f47cf6 100755 --- a/deploy/03-install-skill-on-vps.sh +++ b/deploy/03-install-skill-on-vps.sh @@ -1,10 +1,18 @@ #!/usr/bin/env bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=lib/validate-deploy-path.sh +source "$SCRIPT_DIR/lib/validate-deploy-path.sh" + VPS="${VPS:-n8n-server}" SKILL_SRC="$(cd "$(dirname "$0")" && pwd)/skills/biohacking-corpus" REMOTE_SKILLS="${REMOTE_SKILLS:-/root/.openclaw/workspace/skills}" + +validate_deploy_value "VPS" "$VPS" +validate_deploy_value "REMOTE_SKILLS" "$REMOTE_SKILLS" + echo "Install skill: $SKILL_SRC -> $VPS:$REMOTE_SKILLS/biohacking-corpus/" -ssh "$VPS" "mkdir -p '$REMOTE_SKILLS/biohacking-corpus'" +ssh -- "$VPS" mkdir -p -- "$REMOTE_SKILLS/biohacking-corpus" rsync -avz "$SKILL_SRC/" "$VPS:$REMOTE_SKILLS/biohacking-corpus/" -ssh "$VPS" "chmod +x '$REMOTE_SKILLS/biohacking-corpus/'*.sh 2>/dev/null || true" +ssh -- "$VPS" chmod +x -- "$REMOTE_SKILLS/biohacking-corpus/"*.sh 2>/dev/null || true echo "Done. Restart OpenClaw gateway if needed." diff --git a/deploy/RUNBOOK.md b/deploy/RUNBOOK.md index 4932544..8fa465b 100644 --- a/deploy/RUNBOOK.md +++ b/deploy/RUNBOOK.md @@ -31,7 +31,8 @@ Copy and fill env template on the server: ```bash cp deploy/openclaw.env.example ~/.config/medbot-openclaw.env -# set TELEGRAM token and allowed user IDs +chmod 600 ~/.config/medbot-openclaw.env +# set TELEGRAM token and allowed user IDs (never leave ALLOWED_USER_IDS empty in production) ``` Skill reads corpus from `/opt/medbot-corpus/structured_database` (adjust in `02-rsync-corpus.sh` if needed). diff --git a/deploy/lib/validate-deploy-path.sh b/deploy/lib/validate-deploy-path.sh new file mode 100644 index 0000000..ac06394 --- /dev/null +++ b/deploy/lib/validate-deploy-path.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Reject shell metacharacters in deploy paths/hosts (SSH injection guard). +validate_deploy_value() { + local label="$1" + local value="$2" + if [[ -z "$value" ]]; then + echo "ERROR: $label is empty" >&2 + return 1 + fi + if [[ "$value" == *$'\n'* ]] || [[ "$value" == *$'\r'* ]]; then + echo "ERROR: $label contains newline" >&2 + return 1 + fi + if [[ ! "$value" =~ ^[a-zA-Z0-9@._:/-]+$ ]]; then + echo "ERROR: $label contains unsafe characters: $value" >&2 + return 1 + fi + if [[ "$value" == *".."* ]]; then + echo "ERROR: $label must not contain .." >&2 + return 1 + fi +} diff --git a/deploy/openclaw.env.example b/deploy/openclaw.env.example index 5390edb..cd7aefa 100644 --- a/deploy/openclaw.env.example +++ b/deploy/openclaw.env.example @@ -1,4 +1,5 @@ # /root/.config/medbot-openclaw.env on VPS — do not commit real tokens +# chmod 600 this file after filling values TELEGRAM_BOT_TOKEN= TELEGRAM_ALLOWED_USER_IDS=123456789 MEDBOTS_CORPUS_PATH=/opt/medbot-corpus/structured_database diff --git a/deploy/skills/biohacking-corpus/biohacking-corpus-path.sh b/deploy/skills/biohacking-corpus/biohacking-corpus-path.sh index d93d9d7..fdf8004 100755 --- a/deploy/skills/biohacking-corpus/biohacking-corpus-path.sh +++ b/deploy/skills/biohacking-corpus/biohacking-corpus-path.sh @@ -1,3 +1,8 @@ #!/usr/bin/env bash # Default corpus path for biohacking-corpus skill -echo "${BIOHACKING_CORPUS_PATH:-/opt/medbot-corpus/structured_database}" +BASE="${BIOHACKING_CORPUS_PATH:-/opt/medbot-corpus/structured_database}" +if [[ "$BASE" == *$'\n'* ]] || [[ ! "$BASE" =~ ^[a-zA-Z0-9/_.-]+$ ]] || [[ "$BASE" == *".."* ]]; then + echo "ERROR: invalid BIOHACKING_CORPUS_PATH" >&2 + exit 1 +fi +echo "$BASE" diff --git a/docs/PARSERS.md b/docs/PARSERS.md index 9f9f4a5..3e33003 100644 --- a/docs/PARSERS.md +++ b/docs/PARSERS.md @@ -42,7 +42,7 @@ medbots validate-apple-health --corpus ~/my-health/structured_database 2. **Set date of birth** in `structured_database/PATIENT_PROFILE.json` — used for age-based reference ranges and Apple Health quality checks: ```json - {"dob": "1985-03-20", "full_name_ru": "Your Name"} + {"dob": "1985-06-15", "full_name_ru": "John Smith", "country": "USA"} ``` 3. **Copy source files** into the right folder (see vendor sections below). diff --git a/docs/PARSERS.ru.md b/docs/PARSERS.ru.md index 0f18337..12ccace 100644 --- a/docs/PARSERS.ru.md +++ b/docs/PARSERS.ru.md @@ -42,7 +42,7 @@ medbots validate-apple-health --corpus ~/my-health/structured_database 2. **Указать дату рождения** в `structured_database/PATIENT_PROFILE.json` — нужна для референсов и проверки Apple Health: ```json - {"dob": "1985-03-20", "full_name_ru": "Иван Иванов"} + {"dob": "1985-06-15", "full_name_ru": "John Smith", "country": "USA"} ``` 3. **Положить файлы** в нужную папку `sources/` (см. разделы по вендорам). diff --git a/examples/demo-instance/README.md b/examples/demo-instance/README.md index 0bdd8ae..3f2f972 100644 --- a/examples/demo-instance/README.md +++ b/examples/demo-instance/README.md @@ -1,7 +1,9 @@ -# Demo instance (synthetic, no PHI) +# Demo instance (synthetic mock patient) Pre-built manifest + `pdf_text/` for three vendors. **No PDF files required.** +Mock patient: **John Smith**, USA (`PATIENT_PROFILE.json`). + ```bash # from repo root medbots structure --bot-root examples/demo-instance @@ -16,4 +18,4 @@ medbots pipeline --bot-root examples/demo-instance Medsi entry includes `user_drop_batch: true` (required by the Medsi parser gate in `local_structure_pdfs`). -Text fixtures are copied from `tests/fixtures/pdf_text/` (redacted synthetic extracts). +Text fixtures are copied from `tests/fixtures/pdf_text/` (synthetic extracts with mock PII). diff --git a/examples/demo-instance/structured_database/PATIENT_PROFILE.json b/examples/demo-instance/structured_database/PATIENT_PROFILE.json index e4c76d9..833c0db 100644 --- a/examples/demo-instance/structured_database/PATIENT_PROFILE.json +++ b/examples/demo-instance/structured_database/PATIENT_PROFILE.json @@ -1 +1 @@ -{"dob":"1985-03-20","full_name_ru":"Demo Patient"} +{"dob": "1985-06-15", "full_name_ru": "John Smith", "country": "USA"} diff --git "a/examples/demo-instance/structured_database/pdf_text/sources__emias__2026-06-17__drop_\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__929313a8.pdf.txt" "b/examples/demo-instance/structured_database/pdf_text/sources__emias__2026-06-17__drop_\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__929313a8.pdf.txt" index 537ddb7..e1c206d 100644 --- "a/examples/demo-instance/structured_database/pdf_text/sources__emias__2026-06-17__drop_\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__929313a8.pdf.txt" +++ "b/examples/demo-instance/structured_database/pdf_text/sources__emias__2026-06-17__drop_\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__929313a8.pdf.txt" @@ -1,20 +1,20 @@ Биохимический анализ крови -Тестов +Smith Фамилия: № направления: -268953196 +MOCK-REF-90001 Дата: 16.06.2026 -Алексей +John Имя: ЛПУ: -Карта: 2364501 -Юрьевич +Карта: MOCK-CARD-10001 +- Отчество: Отделение: 10.01. Терапевтическое отделение -Врач: Михайлова Юлия Игоревна +Врач: Robert Miller, MD Номер заказа: -1011274602 +MOCK-ORDER-80001 Наименование исследования Результат Ед. изм. @@ -169,6 +169,6 @@ Consensus Panel. Eur Heart J.2016;37:1944-1958 -Калачева Ольга Сергеевна +Lab Tech Demo 16.06.2026 12:22:13 Врач КДЛ: \ No newline at end of file diff --git "a/examples/demo-instance/structured_database/pdf_text/sources__gemotest__2021-02-14__gemotest_69371781__\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\217_19_\320\277\320\276\320\272\320\260\320\267\320\260\321\202\320\265\320\273\320\265\320\271__d6c4ee6e.pdf.txt" "b/examples/demo-instance/structured_database/pdf_text/sources__gemotest__2021-02-14__gemotest_69371781__\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\217_19_\320\277\320\276\320\272\320\260\320\267\320\260\321\202\320\265\320\273\320\265\320\271__d6c4ee6e.pdf.txt" index 40f339f..a958146 100644 --- "a/examples/demo-instance/structured_database/pdf_text/sources__gemotest__2021-02-14__gemotest_69371781__\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\217_19_\320\277\320\276\320\272\320\260\320\267\320\260\321\202\320\265\320\273\320\265\320\271__d6c4ee6e.pdf.txt" +++ "b/examples/demo-instance/structured_database/pdf_text/sources__gemotest__2021-02-14__gemotest_69371781__\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\217_19_\320\277\320\276\320\272\320\260\320\267\320\260\321\202\320\265\320\273\320\265\320\271__d6c4ee6e.pdf.txt" @@ -58,17 +58,15 @@ Пол пациента мужской Дата рождения пациента -20.03.1985 +15.06.1985 Номер истории болезни пациента Направляющий врач - -50153. "Валрус Мед" (Марьина Роща,Москва) +Springfield General Hospital, USA Палата Адрес -129594, Москва г, Внутригородская территория -муниципальный округ Марьина роща, Марьиной Рощи 2 --я ул, дом 10/14 +123 Main St, Springfield, IL 62701, USA Отделение Адрес пациента г. москва ул. Дыбенко, д.36, к.4, кв.113 @@ -76,11 +74,11 @@ Дата регистрации заказа 14.02.2021 Имя пациента -Алексей Юрьевич +John Smith № заказа 69371781 Фамилия пациента -Тестов +Smith ЛО-50-01-011414 от 01.11.2019 выдана Министерство здравоохранения Московской области ЛО-50-01-012467 от 24.11.2020 выдана Министерством @@ -177,17 +175,15 @@ Aспартатаминотрансфераза (АСТ) Пол пациента мужской Дата рождения пациента -20.03.1985 +15.06.1985 Номер истории болезни пациента Направляющий врач - -50153. "Валрус Мед" (Марьина Роща,Москва) +Springfield General Hospital, USA Палата Адрес -129594, Москва г, Внутригородская территория -муниципальный округ Марьина роща, Марьиной Рощи 2 --я ул, дом 10/14 +123 Main St, Springfield, IL 62701, USA Отделение Адрес пациента г. москва ул. Дыбенко, д.36, к.4, кв.113 @@ -195,11 +191,11 @@ Aспартатаминотрансфераза (АСТ) Дата регистрации заказа 14.02.2021 Имя пациента -Алексей Юрьевич +John Smith № заказа 69371781 Фамилия пациента -Тестов +Smith ЛО-50-01-011414 от 01.11.2019 выдана Министерство здравоохранения Московской области ЛО-50-01-012467 от 24.11.2020 выдана Министерством @@ -319,17 +315,15 @@ x10*9/л Пол пациента мужской Дата рождения пациента -20.03.1985 +15.06.1985 Номер истории болезни пациента Направляющий врач - -50153. "Валрус Мед" (Марьина Роща,Москва) +Springfield General Hospital, USA Палата Адрес -129594, Москва г, Внутригородская территория -муниципальный округ Марьина роща, Марьиной Рощи 2 --я ул, дом 10/14 +123 Main St, Springfield, IL 62701, USA Отделение Адрес пациента г. москва ул. Дыбенко, д.36, к.4, кв.113 @@ -337,11 +331,11 @@ x10*9/л Дата регистрации заказа 14.02.2021 Имя пациента -Алексей Юрьевич +John Smith № заказа 69371781 Фамилия пациента -Тестов +Smith ЛО-50-01-011414 от 01.11.2019 выдана Министерство здравоохранения Московской области ЛО-50-01-012467 от 24.11.2020 выдана Министерством @@ -418,17 +412,15 @@ x10*9/л Пол пациента мужской Дата рождения пациента -20.03.1985 +15.06.1985 Номер истории болезни пациента Направляющий врач - -50153. "Валрус Мед" (Марьина Роща,Москва) +Springfield General Hospital, USA Палата Адрес -129594, Москва г, Внутригородская территория -муниципальный округ Марьина роща, Марьиной Рощи 2 --я ул, дом 10/14 +123 Main St, Springfield, IL 62701, USA Отделение Адрес пациента г. москва ул. Дыбенко, д.36, к.4, кв.113 @@ -436,11 +428,11 @@ x10*9/л Дата регистрации заказа 14.02.2021 Имя пациента -Алексей Юрьевич +John Smith № заказа 69371781 Фамилия пациента -Тестов +Smith ЛО-50-01-011414 от 01.11.2019 выдана Министерство здравоохранения Московской области ЛО-50-01-012467 от 24.11.2020 выдана Министерством @@ -481,17 +473,15 @@ x10*9/л Пол пациента мужской Дата рождения пациента -20.03.1985 +15.06.1985 Номер истории болезни пациента Направляющий врач - -50153. "Валрус Мед" (Марьина Роща,Москва) +Springfield General Hospital, USA Палата Адрес -129594, Москва г, Внутригородская территория -муниципальный округ Марьина роща, Марьиной Рощи 2 --я ул, дом 10/14 +123 Main St, Springfield, IL 62701, USA Отделение Адрес пациента г. москва ул. Дыбенко, д.36, к.4, кв.113 @@ -499,11 +489,11 @@ x10*9/л Дата регистрации заказа 14.02.2021 Имя пациента -Алексей Юрьевич +John Smith № заказа 69371781 Фамилия пациента -Тестов +Smith ЛО-50-01-011414 от 01.11.2019 выдана Министерство здравоохранения Московской области ЛО-50-01-012467 от 24.11.2020 выдана Министерством diff --git "a/examples/demo-instance/structured_database/pdf_text/sources__medsi__2026-06-16__drop_\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__2e563e81.pdf.txt" "b/examples/demo-instance/structured_database/pdf_text/sources__medsi__2026-06-16__drop_\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__2e563e81.pdf.txt" index 2425ddf..cc341cd 100644 --- "a/examples/demo-instance/structured_database/pdf_text/sources__medsi__2026-06-16__drop_\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__2e563e81.pdf.txt" +++ "b/examples/demo-instance/structured_database/pdf_text/sources__medsi__2026-06-16__drop_\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__2e563e81.pdf.txt" @@ -1,20 +1,20 @@ Биохимический анализ крови -Тестов +Smith Фамилия: № направления: -268953196 +MOCK-REF-90001 Дата: 16.06.2026 -Алексей +John Имя: ЛПУ: -Карта: 2364501 -Юрьевич +Карта: MOCK-CARD-10001 +- Отчество: Отделение: 10.01. Терапевтическое отделение -Врач: Михайлова Юлия Игоревна +Врач: Robert Miller, MD Номер заказа: -1011274602 +MOCK-ORDER-80001 Наименование исследования Результат Ед. изм. @@ -76,6 +76,6 @@ мкмоль/л 59-104 84 -Сайфулина Люсинэ Саргисовна +Lab Tech Demo 16.06.2026 12:22:13 Врач КДЛ: \ No newline at end of file diff --git a/medbots/import_apple_health.py b/medbots/import_apple_health.py index b926151..6ea464c 100644 --- a/medbots/import_apple_health.py +++ b/medbots/import_apple_health.py @@ -15,6 +15,7 @@ from xml.etree.ElementTree import iterparse from medbots.corpus_io import load_patient_dob, resolve_corpus +from medbots.zip_safety import UnsafeZipError, validate_zip_archive VALID_DATE_MIN = "2015-01-01" @@ -220,6 +221,7 @@ def stream_import(zip_path: Path) -> tuple[dict[str, DailyAgg], list[dict], Impo export_dob: str | None = None with zipfile.ZipFile(zip_path) as zf: + validate_zip_archive(zf, zip_size=zip_path.stat().st_size) xml_name = find_main_xml(zf) routes = [ i.filename @@ -393,6 +395,7 @@ def build_body_metrics(daily: dict[str, DailyAgg]) -> list[dict[str, Any]]: def parse_ecg_from_zip(zip_path: Path) -> list[dict[str, Any]]: records: list[dict[str, Any]] = [] with zipfile.ZipFile(zip_path) as zf: + validate_zip_archive(zf, zip_size=zip_path.stat().st_size) for info in zf.infolist(): if "electrocardiograms/" not in info.filename or not info.filename.endswith(".csv"): continue @@ -427,6 +430,7 @@ def parse_ecg_from_zip(zip_path: Path) -> list[dict[str, Any]]: def list_gpx_index(zip_path: Path) -> list[dict[str, Any]]: out: list[dict[str, Any]] = [] with zipfile.ZipFile(zip_path) as zf: + validate_zip_archive(zf, zip_size=zip_path.stat().st_size) for info in zf.infolist(): if "workout-routes/" not in info.filename or not info.filename.endswith(".gpx"): continue diff --git a/medbots/init_instance.py b/medbots/init_instance.py index 0e10667..63bb3d6 100644 --- a/medbots/init_instance.py +++ b/medbots/init_instance.py @@ -36,7 +36,7 @@ def init(path: Path, *, force: bool = False) -> None: if not profile.exists() or force: profile.write_text( json.dumps( - {"dob": "YYYY-MM-DD", "full_name_ru": "Your Name"}, + {"dob": "1985-06-15", "full_name_ru": "John Smith", "country": "USA"}, ensure_ascii=False, indent=2, ) diff --git a/medbots/zip_safety.py b/medbots/zip_safety.py new file mode 100644 index 0000000..a9ae5ce --- /dev/null +++ b/medbots/zip_safety.py @@ -0,0 +1,45 @@ +"""Zip archive safety checks (zip-slip, zip bombs).""" +from __future__ import annotations + +import zipfile +from pathlib import PurePosixPath + +MAX_ZIP_BYTES = 2 * 1024 * 1024 * 1024 +MAX_ZIP_MEMBERS = 50_000 +MAX_UNCOMPRESSED_BYTES = 4 * 1024 * 1024 * 1024 +MAX_COMPRESSION_RATIO = 100 + + +class UnsafeZipError(ValueError): + pass + + +def _member_name_safe(name: str) -> None: + path = PurePosixPath(name) + if name.startswith("/") or ".." in path.parts: + raise UnsafeZipError(f"unsafe zip member path: {name!r}") + + +def validate_zip_archive(zf: zipfile.ZipFile, *, zip_size: int | None = None) -> None: + if zip_size is not None and zip_size > MAX_ZIP_BYTES: + raise UnsafeZipError(f"zip file too large: {zip_size} bytes") + + members = zf.infolist() + if len(members) > MAX_ZIP_MEMBERS: + raise UnsafeZipError(f"too many zip members: {len(members)}") + + total_uncompressed = 0 + for info in members: + _member_name_safe(info.filename) + total_uncompressed += info.file_size + if info.file_size and info.compress_size: + ratio = info.file_size / max(info.compress_size, 1) + if ratio > MAX_COMPRESSION_RATIO: + raise UnsafeZipError( + f"suspicious compression ratio for {info.filename!r}: {ratio:.1f}" + ) + + if total_uncompressed > MAX_UNCOMPRESSED_BYTES: + raise UnsafeZipError( + f"uncompressed zip payload too large: {total_uncompressed} bytes" + ) diff --git a/scripts/check_safe_to_push.py b/scripts/check_safe_to_push.py index ae34e32..6cfc547 100644 --- a/scripts/check_safe_to_push.py +++ b/scripts/check_safe_to_push.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Block git push when secrets or patient corpus would be published.""" +"""Block git push / CI when secrets or patient corpus would be published.""" from __future__ import annotations import argparse @@ -12,60 +12,179 @@ DENY_CONTENT = [ re.compile(r"ghp_[A-Za-z0-9]{20,}"), + re.compile(r"gho_[A-Za-z0-9]{20,}"), re.compile(r"sk-[A-Za-z0-9]{20,}"), + re.compile(r"xai-[A-Za-z0-9]{20,}"), re.compile(r"xox[baprs]-[A-Za-z0-9-]{10,}"), re.compile(r"AKIA[0-9A-Z]{16}"), - re.compile(r"-----BEGIN (?:RSA )?PRIVATE KEY-----"), - re.compile(r"TELEGRAM_[A-Z_]*TOKEN\s*=\s*[^\s#]+", re.IGNORECASE), - re.compile(r"Подобедов"), + re.compile(r"-----BEGIN (?:RSA |OPENSSH |EC )?PRIVATE KEY-----"), + re.compile(r"TELEGRAM_BOT_TOKEN\s*=\s*[0-9]+:[A-Za-z0-9_-]{20,}", re.IGNORECASE), + re.compile(r"y0_[A-Za-z0-9._-]{20,}"), re.compile(r"72\.56\.79\.23"), + re.compile(r"7752900844000725"), + re.compile(r"Алексей\s+Юрьевич"), + re.compile(r"20\.03\.1985"), + re.compile(r"\b775\d{13}\b"), ] PUBLIC_BLOCKED_PATHS = re.compile( - r"^(?:structured_database/(?!README\.md$).+|sources/|incoming/|bot_config\.json|.*\.env$|.*\.pdf$)" + r"^(?:structured_database/(?!README\.md$).+|sources/|incoming/|bot_config\.json|(?:.+/)?\.env$|.*\.pdf$)" ) ALLOWED_ENV_EXAMPLE = re.compile(r"\.env\.example$") +# Files that embed deny-pattern literals for documentation/guards. +SCAN_SKIP_FILES = re.compile( + r"^(?:scripts/check_safe_to_push\.py|\.github/workflows/secret-scan\.yml)$" +) -def _staged_files() -> list[str]: - out = subprocess.check_output( - ["git", "diff", "--cached", "--name-only", "--diff-filter=ACMR"], +_ZERO_SHA = "0" * 40 + + +def _git(*args: str) -> str: + return subprocess.check_output( + ["git", *args], cwd=ROOT, text=True, + errors="replace", ) + + +def _staged_files() -> list[str]: + out = _git("diff", "--cached", "--name-only", "--diff-filter=ACMR") return [line.strip() for line in out.splitlines() if line.strip()] def _staged_diff() -> str: - return subprocess.check_output( - ["git", "diff", "--cached", "-U0"], - cwd=ROOT, - text=True, - errors="replace", + return _git("diff", "--cached", "-U0") + + +def _tracked_files() -> list[str]: + out = _git("ls-files") + return [line.strip() for line in out.splitlines() if line.strip()] + + +def _files_in_range(commit_range: str) -> list[str]: + out = _git( + "diff", + "--name-only", + "--diff-filter=ACMR", + commit_range, ) + return [line.strip() for line in out.splitlines() if line.strip()] + + +def _diff_in_range(commit_range: str) -> str: + return _git("diff", "-U0", commit_range) -def check(*, public: bool) -> list[str]: +def _read_paths(paths: list[str]) -> str: + chunks: list[str] = [] + for rel in paths: + if SCAN_SKIP_FILES.match(rel): + continue + path = ROOT / rel + if path.is_file(): + chunks.append(path.read_text(encoding="utf-8", errors="replace")) + return "\n".join(chunks) + + +def _path_errors(path: str, *, public: bool) -> list[str]: errors: list[str] = [] - files = _staged_files() - if not files: - return errors + if public and PUBLIC_BLOCKED_PATHS.match(path): + errors.append(f"blocked path (public repo): {path}") + if path.endswith(".env") and not ALLOWED_ENV_EXAMPLE.search(path): + errors.append(f"blocked env file: {path}") + return errors + + +def _added_diff_text(diff_text: str) -> str: + current_skip = False + lines_out: list[str] = [] + for line in diff_text.splitlines(): + if line.startswith("+++"): + path = line[4:].strip() + if path.startswith("b/"): + path = path[2:] + current_skip = bool(SCAN_SKIP_FILES.match(path)) + continue + if line.startswith("+") and not line.startswith("+++") and not current_skip: + lines_out.append(line[1:]) + return "\n".join(lines_out) - for path in files: - if public and PUBLIC_BLOCKED_PATHS.match(path): - errors.append(f"blocked path (public repo): {path}") - if path.endswith(".env") and not ALLOWED_ENV_EXAMPLE.search(path): - errors.append(f"blocked env file: {path}") - diff = _staged_diff() +def _content_errors(text: str, *, label: str) -> list[str]: + errors: list[str] = [] for pat in DENY_CONTENT: - if pat.search(diff): - errors.append(f"blocked pattern in staged diff: {pat.pattern}") + if pat.search(text): + errors.append(f"blocked pattern in {label}: {pat.pattern}") + return errors + + +def check( + *, + public: bool, + paths: list[str] | None = None, + diff_text: str | None = None, + include_file_bodies: bool = False, +) -> list[str]: + errors: list[str] = [] + file_list = paths or [] + if not file_list and diff_text is None: + return errors + + for path in file_list: + errors.extend(_path_errors(path, public=public)) + texts: list[str] = [] + if diff_text is not None: + texts.append(_added_diff_text(diff_text)) + if include_file_bodies and file_list: + texts.append(_read_paths(file_list)) + + for text in texts: + errors.extend(_content_errors(text, label="content")) return errors +def check_push_range(commit_range: str, *, public: bool) -> list[str]: + files = _files_in_range(commit_range) + if not files: + return [] + return check( + public=public, + paths=files, + diff_text=_diff_in_range(commit_range), + include_file_bodies=False, + ) + + +def check_tracked_tree(*, public: bool) -> list[str]: + files = _tracked_files() + return check( + public=public, + paths=files, + diff_text=_read_paths(files), + include_file_bodies=False, + ) + + +def _push_ranges_from_stdin() -> list[str]: + ranges: list[str] = [] + for line in sys.stdin: + parts = line.strip().split() + if len(parts) != 4: + continue + _local_ref, local_sha, _remote_ref, remote_sha = parts + if local_sha == _ZERO_SHA: + continue + if remote_sha == _ZERO_SHA: + ranges.append(local_sha) + else: + ranges.append(f"{remote_sha}..{local_sha}") + return ranges + + def main() -> int: parser = argparse.ArgumentParser() parser.add_argument( @@ -73,8 +192,45 @@ def main() -> int: action="store_true", help="Strict mode for apodobe/biohackbot (no corpus, no secrets)", ) + parser.add_argument( + "--scan-all", + action="store_true", + help="Scan all git-tracked files (CI)", + ) + parser.add_argument( + "--range", + metavar="REV_RANGE", + help="Scan files/diff in a git revision range (pre-push)", + ) + parser.add_argument( + "--push", + action="store_true", + help="Read pre-push stdin and scan each outgoing commit range", + ) args = parser.parse_args() - errors = check(public=args.public) + + errors: list[str] = [] + if args.push: + ranges = _push_ranges_from_stdin() + if not ranges: + errors.append("pre-push: no commit ranges on stdin") + for commit_range in ranges: + errors.extend(check_push_range(commit_range, public=args.public)) + elif args.scan_all: + errors.extend(check_tracked_tree(public=args.public)) + elif args.range: + errors.extend(check_push_range(args.range, public=args.public)) + else: + files = _staged_files() + errors.extend( + check( + public=args.public, + paths=files, + diff_text=_staged_diff() if files else None, + include_file_bodies=False, + ) + ) + if errors: print("Push blocked — remove sensitive content before pushing:", file=sys.stderr) for err in errors: diff --git "a/tests/fixtures/pdf_text/sources__emias__2019-10-10__\320\240\320\265\320\267\321\203\320\273\321\214\321\202\320\260\321\202_\321\204\320\273\321\216\320\276\321\200\320\276\320\263\321\200\320\260\321\204\320\270\320\270__1ce3317f.pdf.txt" "b/tests/fixtures/pdf_text/sources__emias__2019-10-10__\320\240\320\265\320\267\321\203\320\273\321\214\321\202\320\260\321\202_\321\204\320\273\321\216\320\276\321\200\320\276\320\263\321\200\320\260\321\204\320\270\320\270__1ce3317f.pdf.txt" index 48489dc..b4da121 100644 --- "a/tests/fixtures/pdf_text/sources__emias__2019-10-10__\320\240\320\265\320\267\321\203\320\273\321\214\321\202\320\260\321\202_\321\204\320\273\321\216\320\276\321\200\320\276\320\263\321\200\320\260\321\204\320\270\320\270__1ce3317f.pdf.txt" +++ "b/tests/fixtures/pdf_text/sources__emias__2019-10-10__\320\240\320\265\320\267\321\203\320\273\321\214\321\202\320\260\321\202_\321\204\320\273\321\216\320\276\321\200\320\276\320\263\321\200\320\260\321\204\320\270\320\270__1ce3317f.pdf.txt" @@ -6,6 +6,6 @@ Заключение На момент исследования свежих очаговых и инфильтративных изменений со стороны легких не выявлено . Дата: 10.10.2019 -Полис ОМС: 7752900844000725 -Медицинское учреждение : ГБУЗ ГП 2 ДЗМ филиал 3 +Полис ОМС: US-MOCK-INS-000123456 +Медицинское учреждение : Springfield General Hospital, USA Протокол: РГ флюорографии \ No newline at end of file diff --git "a/tests/fixtures/pdf_text/sources__emias__2019-12-30__\320\236\321\201\320\274\320\276\321\202\321\200_\321\202\320\265\321\200\320\260\320\277\320\265\320\262\321\202\320\260__746635cf.pdf.txt" "b/tests/fixtures/pdf_text/sources__emias__2019-12-30__\320\236\321\201\320\274\320\276\321\202\321\200_\321\202\320\265\321\200\320\260\320\277\320\265\320\262\321\202\320\260__746635cf.pdf.txt" index 5c1f769..b33e682 100644 --- "a/tests/fixtures/pdf_text/sources__emias__2019-12-30__\320\236\321\201\320\274\320\276\321\202\321\200_\321\202\320\265\321\200\320\260\320\277\320\265\320\262\321\202\320\260__746635cf.pdf.txt" +++ "b/tests/fixtures/pdf_text/sources__emias__2019-12-30__\320\236\321\201\320\274\320\276\321\202\321\200_\321\202\320\265\321\200\320\260\320\277\320\265\320\262\321\202\320\260__746635cf.pdf.txt" @@ -2,10 +2,10 @@ не предъявляет. Анамнез заболевания -На ЛН 369 512 204 297 с 26.12, выдан на дому ГП 12 (Фил 2) +На ЛН MOCK-SL-00000001 с 26.12, выдан на дому ГП 12 (Фил 2) аллергия бисептол, димедрол ФЛГ от 09.2019 норма -Место работы ООО Модерн, руководитель проекта. +Место работы Acme Corp, Project Manager, USA. Общий осмотр Рост: 178 см Вес: 80 кг @@ -23,8 +23,8 @@ J06.8 - Другие острые инфекции верхних дыхател Рекомендации ЛН закрыт, к труду с 31.12. Дата: 30.12.2019 -Полис ОМС: 7752900844000725 -Медицинское учреждение : ГБУЗ ГП 2 ДЗМ филиал 3 +Полис ОМС: US-MOCK-INS-000123456 +Медицинское учреждение : Springfield General Hospital, USA Специализация: Врач общей практики (семейный врач) -ФИО: Трембовецкая Я. В. +ФИО: Jane Doe, MD Осмотр терапевта \ No newline at end of file diff --git "a/tests/fixtures/pdf_text/sources__emias__2021-01-18__\320\236\320\277\321\200\320\265\320\264\320\265\320\273\320\265\320\275\320\270\320\265_\320\260\320\275\321\202\320\270\321\202\320\265\320\273_IgM_\320\270_IgG_\320\272_Coronavirus_SARS-CoV-2__079f862f.pdf.txt" "b/tests/fixtures/pdf_text/sources__emias__2021-01-18__\320\236\320\277\321\200\320\265\320\264\320\265\320\273\320\265\320\275\320\270\320\265_\320\260\320\275\321\202\320\270\321\202\320\265\320\273_IgM_\320\270_IgG_\320\272_Coronavirus_SARS-CoV-2__079f862f.pdf.txt" index 882245f..ad2ff9a 100644 --- "a/tests/fixtures/pdf_text/sources__emias__2021-01-18__\320\236\320\277\321\200\320\265\320\264\320\265\320\273\320\265\320\275\320\270\320\265_\320\260\320\275\321\202\320\270\321\202\320\265\320\273_IgM_\320\270_IgG_\320\272_Coronavirus_SARS-CoV-2__079f862f.pdf.txt" +++ "b/tests/fixtures/pdf_text/sources__emias__2021-01-18__\320\236\320\277\321\200\320\265\320\264\320\265\320\273\320\265\320\275\320\270\320\265_\320\260\320\275\321\202\320\270\321\202\320\265\320\273_IgM_\320\270_IgG_\320\272_Coronavirus_SARS-CoV-2__079f862f.pdf.txt" @@ -1,6 +1,6 @@ Дата: 18.01.2021 -Полис ОМС: 7752900844000725 -МНПЦ ДИК ДЗМ +Полис ОМС: US-MOCK-INS-000123456 +Springfield Diagnostic Lab, USA КЛИНИКО-ДИАГНОСТИЧЕСКАЯ ЛАБОРАТОРИЯ РЕЗУЛЬТАТЫ ЛАБОРАТОРНЫХ ИССЛЕДОВАНИЙ Определение антител IgM и IgG к Coronavirus (SARS-CoV-2) diff --git "a/tests/fixtures/pdf_text/sources__emias__2026-06-17__drop_\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__929313a8.pdf.txt" "b/tests/fixtures/pdf_text/sources__emias__2026-06-17__drop_\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__929313a8.pdf.txt" index 537ddb7..e1c206d 100644 --- "a/tests/fixtures/pdf_text/sources__emias__2026-06-17__drop_\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__929313a8.pdf.txt" +++ "b/tests/fixtures/pdf_text/sources__emias__2026-06-17__drop_\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__929313a8.pdf.txt" @@ -1,20 +1,20 @@ Биохимический анализ крови -Тестов +Smith Фамилия: № направления: -268953196 +MOCK-REF-90001 Дата: 16.06.2026 -Алексей +John Имя: ЛПУ: -Карта: 2364501 -Юрьевич +Карта: MOCK-CARD-10001 +- Отчество: Отделение: 10.01. Терапевтическое отделение -Врач: Михайлова Юлия Игоревна +Врач: Robert Miller, MD Номер заказа: -1011274602 +MOCK-ORDER-80001 Наименование исследования Результат Ед. изм. @@ -169,6 +169,6 @@ Consensus Panel. Eur Heart J.2016;37:1944-1958 -Калачева Ольга Сергеевна +Lab Tech Demo 16.06.2026 12:22:13 Врач КДЛ: \ No newline at end of file diff --git "a/tests/fixtures/pdf_text/sources__gemotest__2020-03-03__gemotest_54397876__\320\236\320\221\320\251\320\225\320\232\320\233\320\230\320\235\320\230\320\247\320\225\320\241\320\232\320\230\320\225_\320\230\320\241\320\241\320\233\320\225\320\224\320\236\320\222\320\220\320\235\320\230\320\257_\320\232\320\220\320\233\320\220__d2d73f74.pdf.txt" "b/tests/fixtures/pdf_text/sources__gemotest__2020-03-03__gemotest_54397876__\320\236\320\221\320\251\320\225\320\232\320\233\320\230\320\235\320\230\320\247\320\225\320\241\320\232\320\230\320\225_\320\230\320\241\320\241\320\233\320\225\320\224\320\236\320\222\320\220\320\235\320\230\320\257_\320\232\320\220\320\233\320\220__d2d73f74.pdf.txt" index 58ba00d..835482f 100644 --- "a/tests/fixtures/pdf_text/sources__gemotest__2020-03-03__gemotest_54397876__\320\236\320\221\320\251\320\225\320\232\320\233\320\230\320\235\320\230\320\247\320\225\320\241\320\232\320\230\320\225_\320\230\320\241\320\241\320\233\320\225\320\224\320\236\320\222\320\220\320\235\320\230\320\257_\320\232\320\220\320\233\320\220__d2d73f74.pdf.txt" +++ "b/tests/fixtures/pdf_text/sources__gemotest__2020-03-03__gemotest_54397876__\320\236\320\221\320\251\320\225\320\232\320\233\320\230\320\235\320\230\320\247\320\225\320\241\320\232\320\230\320\225_\320\230\320\241\320\241\320\233\320\225\320\224\320\236\320\222\320\220\320\235\320\230\320\257_\320\232\320\220\320\233\320\220__d2d73f74.pdf.txt" @@ -92,17 +92,15 @@ Пол пациента мужской Дата рождения пациента -20.03.1985 +15.06.1985 Номер истории болезни пациента Направляющий врач - -50153. "Валрус Мед" (Марьина Роща,Москва) +Springfield General Hospital, USA Палата Адрес -129594, Москва г, Внутригородская территория -муниципальный округ Марьина роща, Марьиной Рощи 2 --я ул, дом 10/14 +123 Main St, Springfield, IL 62701, USA Отделение Адрес пациента @@ -110,11 +108,11 @@ Дата регистрации заказа 03.03.2020 Имя пациента -Алексей Юрьевич +John Smith № заказа 54397876 Фамилия пациента -Тестов +Smith ЛО-50-01-011414 от 01.11.2019 выдана Министерство здравоохранения Московской области ЛО-63-01-005303 от 05.11.2019 выдана Министерство @@ -166,17 +164,15 @@ Jodamoeba butschlii (йодамеба Бючли) Пол пациента мужской Дата рождения пациента -20.03.1985 +15.06.1985 Номер истории болезни пациента Направляющий врач - -50153. "Валрус Мед" (Марьина Роща,Москва) +Springfield General Hospital, USA Палата Адрес -129594, Москва г, Внутригородская территория -муниципальный округ Марьина роща, Марьиной Рощи 2 --я ул, дом 10/14 +123 Main St, Springfield, IL 62701, USA Отделение Адрес пациента @@ -184,11 +180,11 @@ Jodamoeba butschlii (йодамеба Бючли) Дата регистрации заказа 03.03.2020 Имя пациента -Алексей Юрьевич +John Smith № заказа 54397876 Фамилия пациента -Тестов +Smith ЛО-50-01-011414 от 01.11.2019 выдана Министерство здравоохранения Московской области ЛО-63-01-005303 от 05.11.2019 выдана Министерство diff --git "a/tests/fixtures/pdf_text/sources__gemotest__2020-03-06__gemotest_54397876__\320\234\320\270\320\272\321\200\320\276\320\261\320\270\320\276\320\273\320\276\320\263\320\270\321\207\320\265\321\201\320\272\320\276\320\265_\320\270\321\201\321\201\320\273\320\265\320\264\320\276\320\262\320\260\320\275\320\270\320\265__204c6698.pdf.txt" "b/tests/fixtures/pdf_text/sources__gemotest__2020-03-06__gemotest_54397876__\320\234\320\270\320\272\321\200\320\276\320\261\320\270\320\276\320\273\320\276\320\263\320\270\321\207\320\265\321\201\320\272\320\276\320\265_\320\270\321\201\321\201\320\273\320\265\320\264\320\276\320\262\320\260\320\275\320\270\320\265__204c6698.pdf.txt" index afd2395..2d7ef46 100644 --- "a/tests/fixtures/pdf_text/sources__gemotest__2020-03-06__gemotest_54397876__\320\234\320\270\320\272\321\200\320\276\320\261\320\270\320\276\320\273\320\276\320\263\320\270\321\207\320\265\321\201\320\272\320\276\320\265_\320\270\321\201\321\201\320\273\320\265\320\264\320\276\320\262\320\260\320\275\320\270\320\265__204c6698.pdf.txt" +++ "b/tests/fixtures/pdf_text/sources__gemotest__2020-03-06__gemotest_54397876__\320\234\320\270\320\272\321\200\320\276\320\261\320\270\320\276\320\273\320\276\320\263\320\270\321\207\320\265\321\201\320\272\320\276\320\265_\320\270\321\201\321\201\320\273\320\265\320\264\320\276\320\262\320\260\320\275\320\270\320\265__204c6698.pdf.txt" @@ -69,17 +69,15 @@ Staphylococcus epidermidis Пол пациента мужской Дата рождения пациента -20.03.1985 +15.06.1985 Номер истории болезни пациента Направляющий врач - -50153. "Валрус Мед" (Марьина Роща,Москва) +Springfield General Hospital, USA Палата Адрес -129594, Москва г, Внутригородская территория -муниципальный округ Марьина роща, Марьиной Рощи 2 --я ул, дом 10/14 +123 Main St, Springfield, IL 62701, USA Отделение Адрес пациента @@ -87,11 +85,11 @@ Staphylococcus epidermidis Дата регистрации заказа 03.03.2020 Имя пациента -Алексей Юрьевич +John Smith № заказа 54397876 Фамилия пациента -Тестов +Smith ЛО-50-01-011414 от 01.11.2019 выдана Министерство здравоохранения Московской области ЛО-63-01-005303 от 05.11.2019 выдана Министерство @@ -152,17 +150,15 @@ Escherichia coli L- Пол пациента мужской Дата рождения пациента -20.03.1985 +15.06.1985 Номер истории болезни пациента Направляющий врач - -50153. "Валрус Мед" (Марьина Роща,Москва) +Springfield General Hospital, USA Палата Адрес -129594, Москва г, Внутригородская территория -муниципальный округ Марьина роща, Марьиной Рощи 2 --я ул, дом 10/14 +123 Main St, Springfield, IL 62701, USA Отделение Адрес пациента @@ -170,11 +166,11 @@ Escherichia coli L- Дата регистрации заказа 03.03.2020 Имя пациента -Алексей Юрьевич +John Smith № заказа 54397876 Фамилия пациента -Тестов +Smith ЛО-50-01-011414 от 01.11.2019 выдана Министерство здравоохранения Московской области ЛО-63-01-005303 от 05.11.2019 выдана Министерство @@ -208,17 +204,15 @@ S Пол пациента мужской Дата рождения пациента -20.03.1985 +15.06.1985 Номер истории болезни пациента Направляющий врач - -50153. "Валрус Мед" (Марьина Роща,Москва) +Springfield General Hospital, USA Палата Адрес -129594, Москва г, Внутригородская территория -муниципальный округ Марьина роща, Марьиной Рощи 2 --я ул, дом 10/14 +123 Main St, Springfield, IL 62701, USA Отделение Адрес пациента @@ -226,11 +220,11 @@ S Дата регистрации заказа 03.03.2020 Имя пациента -Алексей Юрьевич +John Smith № заказа 54397876 Фамилия пациента -Тестов +Smith ЛО-50-01-011414 от 01.11.2019 выдана Министерство здравоохранения Московской области ЛО-63-01-005303 от 05.11.2019 выдана Министерство diff --git "a/tests/fixtures/pdf_text/sources__gemotest__2021-02-14__gemotest_69371781__\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\217_19_\320\277\320\276\320\272\320\260\320\267\320\260\321\202\320\265\320\273\320\265\320\271__d6c4ee6e.pdf.txt" "b/tests/fixtures/pdf_text/sources__gemotest__2021-02-14__gemotest_69371781__\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\217_19_\320\277\320\276\320\272\320\260\320\267\320\260\321\202\320\265\320\273\320\265\320\271__d6c4ee6e.pdf.txt" index 40f339f..a958146 100644 --- "a/tests/fixtures/pdf_text/sources__gemotest__2021-02-14__gemotest_69371781__\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\217_19_\320\277\320\276\320\272\320\260\320\267\320\260\321\202\320\265\320\273\320\265\320\271__d6c4ee6e.pdf.txt" +++ "b/tests/fixtures/pdf_text/sources__gemotest__2021-02-14__gemotest_69371781__\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\217_19_\320\277\320\276\320\272\320\260\320\267\320\260\321\202\320\265\320\273\320\265\320\271__d6c4ee6e.pdf.txt" @@ -58,17 +58,15 @@ Пол пациента мужской Дата рождения пациента -20.03.1985 +15.06.1985 Номер истории болезни пациента Направляющий врач - -50153. "Валрус Мед" (Марьина Роща,Москва) +Springfield General Hospital, USA Палата Адрес -129594, Москва г, Внутригородская территория -муниципальный округ Марьина роща, Марьиной Рощи 2 --я ул, дом 10/14 +123 Main St, Springfield, IL 62701, USA Отделение Адрес пациента г. москва ул. Дыбенко, д.36, к.4, кв.113 @@ -76,11 +74,11 @@ Дата регистрации заказа 14.02.2021 Имя пациента -Алексей Юрьевич +John Smith № заказа 69371781 Фамилия пациента -Тестов +Smith ЛО-50-01-011414 от 01.11.2019 выдана Министерство здравоохранения Московской области ЛО-50-01-012467 от 24.11.2020 выдана Министерством @@ -177,17 +175,15 @@ Aспартатаминотрансфераза (АСТ) Пол пациента мужской Дата рождения пациента -20.03.1985 +15.06.1985 Номер истории болезни пациента Направляющий врач - -50153. "Валрус Мед" (Марьина Роща,Москва) +Springfield General Hospital, USA Палата Адрес -129594, Москва г, Внутригородская территория -муниципальный округ Марьина роща, Марьиной Рощи 2 --я ул, дом 10/14 +123 Main St, Springfield, IL 62701, USA Отделение Адрес пациента г. москва ул. Дыбенко, д.36, к.4, кв.113 @@ -195,11 +191,11 @@ Aспартатаминотрансфераза (АСТ) Дата регистрации заказа 14.02.2021 Имя пациента -Алексей Юрьевич +John Smith № заказа 69371781 Фамилия пациента -Тестов +Smith ЛО-50-01-011414 от 01.11.2019 выдана Министерство здравоохранения Московской области ЛО-50-01-012467 от 24.11.2020 выдана Министерством @@ -319,17 +315,15 @@ x10*9/л Пол пациента мужской Дата рождения пациента -20.03.1985 +15.06.1985 Номер истории болезни пациента Направляющий врач - -50153. "Валрус Мед" (Марьина Роща,Москва) +Springfield General Hospital, USA Палата Адрес -129594, Москва г, Внутригородская территория -муниципальный округ Марьина роща, Марьиной Рощи 2 --я ул, дом 10/14 +123 Main St, Springfield, IL 62701, USA Отделение Адрес пациента г. москва ул. Дыбенко, д.36, к.4, кв.113 @@ -337,11 +331,11 @@ x10*9/л Дата регистрации заказа 14.02.2021 Имя пациента -Алексей Юрьевич +John Smith № заказа 69371781 Фамилия пациента -Тестов +Smith ЛО-50-01-011414 от 01.11.2019 выдана Министерство здравоохранения Московской области ЛО-50-01-012467 от 24.11.2020 выдана Министерством @@ -418,17 +412,15 @@ x10*9/л Пол пациента мужской Дата рождения пациента -20.03.1985 +15.06.1985 Номер истории болезни пациента Направляющий врач - -50153. "Валрус Мед" (Марьина Роща,Москва) +Springfield General Hospital, USA Палата Адрес -129594, Москва г, Внутригородская территория -муниципальный округ Марьина роща, Марьиной Рощи 2 --я ул, дом 10/14 +123 Main St, Springfield, IL 62701, USA Отделение Адрес пациента г. москва ул. Дыбенко, д.36, к.4, кв.113 @@ -436,11 +428,11 @@ x10*9/л Дата регистрации заказа 14.02.2021 Имя пациента -Алексей Юрьевич +John Smith № заказа 69371781 Фамилия пациента -Тестов +Smith ЛО-50-01-011414 от 01.11.2019 выдана Министерство здравоохранения Московской области ЛО-50-01-012467 от 24.11.2020 выдана Министерством @@ -481,17 +473,15 @@ x10*9/л Пол пациента мужской Дата рождения пациента -20.03.1985 +15.06.1985 Номер истории болезни пациента Направляющий врач - -50153. "Валрус Мед" (Марьина Роща,Москва) +Springfield General Hospital, USA Палата Адрес -129594, Москва г, Внутригородская территория -муниципальный округ Марьина роща, Марьиной Рощи 2 --я ул, дом 10/14 +123 Main St, Springfield, IL 62701, USA Отделение Адрес пациента г. москва ул. Дыбенко, д.36, к.4, кв.113 @@ -499,11 +489,11 @@ x10*9/л Дата регистрации заказа 14.02.2021 Имя пациента -Алексей Юрьевич +John Smith № заказа 69371781 Фамилия пациента -Тестов +Smith ЛО-50-01-011414 от 01.11.2019 выдана Министерство здравоохранения Московской области ЛО-50-01-012467 от 24.11.2020 выдана Министерством diff --git "a/tests/fixtures/pdf_text/sources__medsi__2026-06-16__drop_\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__2e563e81.pdf.txt" "b/tests/fixtures/pdf_text/sources__medsi__2026-06-16__drop_\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__2e563e81.pdf.txt" index 2425ddf..cc341cd 100644 --- "a/tests/fixtures/pdf_text/sources__medsi__2026-06-16__drop_\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__2e563e81.pdf.txt" +++ "b/tests/fixtures/pdf_text/sources__medsi__2026-06-16__drop_\320\221\320\270\320\276\321\205\320\270\320\274\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__2e563e81.pdf.txt" @@ -1,20 +1,20 @@ Биохимический анализ крови -Тестов +Smith Фамилия: № направления: -268953196 +MOCK-REF-90001 Дата: 16.06.2026 -Алексей +John Имя: ЛПУ: -Карта: 2364501 -Юрьевич +Карта: MOCK-CARD-10001 +- Отчество: Отделение: 10.01. Терапевтическое отделение -Врач: Михайлова Юлия Игоревна +Врач: Robert Miller, MD Номер заказа: -1011274602 +MOCK-ORDER-80001 Наименование исследования Результат Ед. изм. @@ -76,6 +76,6 @@ мкмоль/л 59-104 84 -Сайфулина Люсинэ Саргисовна +Lab Tech Demo 16.06.2026 12:22:13 Врач КДЛ: \ No newline at end of file diff --git "a/tests/fixtures/pdf_text/sources__medsi__2026-06-16__drop_\320\232\320\273\320\270\320\275\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__4d39f738.pdf.txt" "b/tests/fixtures/pdf_text/sources__medsi__2026-06-16__drop_\320\232\320\273\320\270\320\275\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__4d39f738.pdf.txt" index 4ce85d1..7e6095b 100644 --- "a/tests/fixtures/pdf_text/sources__medsi__2026-06-16__drop_\320\232\320\273\320\270\320\275\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__4d39f738.pdf.txt" +++ "b/tests/fixtures/pdf_text/sources__medsi__2026-06-16__drop_\320\232\320\273\320\270\320\275\320\270\321\207\320\265\321\201\320\272\320\270\320\271_\320\260\320\275\320\260\320\273\320\270\320\267_\320\272\321\200\320\276\320\262\320\270__4d39f738.pdf.txt" @@ -1,20 +1,20 @@ Клинический анализ крови -Тестов +Smith Фамилия: № направления: -268953197 +MOCK-REF-90002 Дата: 16.06.2026 -Алексей +John Имя: ЛПУ: -Карта: 2364501 -Юрьевич +Карта: MOCK-CARD-10001 +- Отчество: Отделение: 10.01. Терапевтическое отделение -Врач: Белякова Людмила Николаевна +Врач: Susan Clark, MD Номер заказа: -1011274602 +MOCK-ORDER-80001 Наименование исследования Результат Ед. изм. @@ -141,6 +141,6 @@ Выполнено по методу Вестергрена -Сайфулина Люсинэ Саргисовна +Lab Tech Demo 16.06.2026 12:06:48 Врач КДЛ: \ No newline at end of file diff --git a/tests/test_check_safe_to_push.py b/tests/test_check_safe_to_push.py new file mode 100644 index 0000000..fda2f18 --- /dev/null +++ b/tests/test_check_safe_to_push.py @@ -0,0 +1,82 @@ +"""Tests for push/CI secret and PHI guards.""" +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT / "scripts")) + +from check_safe_to_push import ( # noqa: E402 + check, + check_tracked_tree, +) + + +def test_check_blocks_real_telegram_token() -> None: + errors = check( + public=True, + paths=["deploy/openclaw.env.example"], + diff_text="+TELEGRAM_BOT_TOKEN=123456789:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw", + ) + assert any("TELEGRAM_BOT_TOKEN" in e for e in errors) + + +def test_check_allows_empty_telegram_placeholder() -> None: + errors = check( + public=True, + paths=["deploy/openclaw.env.example"], + diff_text="+TELEGRAM_BOT_TOKEN=\n", + ) + assert errors == [] + + +def test_check_blocks_leaked_oms_policy() -> None: + errors = check( + public=True, + paths=["tests/fixtures/pdf_text/foo.txt"], + diff_text="+Полис ОМС: 7752900844000725", + ) + assert errors + + +def test_check_blocks_patient_corpus_path() -> None: + errors = check( + public=True, + paths=["structured_database/LABS_NORMALIZED.json"], + diff_text="{}", + ) + assert any("blocked path" in e for e in errors) + + +def test_check_blocks_env_file() -> None: + errors = check( + public=True, + paths=["deploy/.env"], + diff_text="SECRET=1", + ) + assert any("blocked env file" in e for e in errors) + + +def test_scan_all_passes_on_clean_tree() -> None: + errors = check_tracked_tree(public=True) + assert errors == [] + + +def test_pre_push_script_exits_zero_on_current_branch() -> None: + """Outgoing range vs main should be clean after PHI redaction.""" + out = subprocess.check_output( + ["git", "merge-base", "HEAD", "main"], + cwd=ROOT, + text=True, + ).strip() + proc = subprocess.run( + [sys.executable, str(ROOT / "scripts" / "check_safe_to_push.py"), "--public", "--range", f"{out}..HEAD"], + cwd=ROOT, + capture_output=True, + text=True, + ) + assert proc.returncode == 0, proc.stderr diff --git a/tests/test_corpus_io.py b/tests/test_corpus_io.py index 2237fae..a6b895b 100644 --- a/tests/test_corpus_io.py +++ b/tests/test_corpus_io.py @@ -39,9 +39,9 @@ def test_load_manifest_missing_returns_empty(tmp_path: Path) -> None: def test_load_patient_dob_from_profile(tmp_corpus: Path) -> None: (tmp_corpus / "PATIENT_PROFILE.json").write_text( - json.dumps({"dob": "1985-03-20"}), encoding="utf-8" + json.dumps({"dob": "1985-06-15"}), encoding="utf-8" ) - assert load_patient_dob(tmp_corpus) == "1985-03-20" + assert load_patient_dob(tmp_corpus) == "1985-06-15" def test_load_patient_dob_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: @@ -55,10 +55,10 @@ def test_resolve_owner_dob_accepts_repo_root(tmp_path: Path) -> None: corpus = tmp_path / "structured_database" corpus.mkdir() (corpus / "PATIENT_PROFILE.json").write_text( - json.dumps({"dob": "1985-03-20"}), encoding="utf-8" + json.dumps({"dob": "1985-06-15"}), encoding="utf-8" ) repo = tmp_path - assert resolve_owner_dob(repo) == "1985-03-20" + assert resolve_owner_dob(repo) == "1985-06-15" def test_manifest_vendor_index_uses_pdfs_key(tmp_corpus: Path) -> None: diff --git a/tests/test_lab_source_lib.py b/tests/test_lab_source_lib.py index 1e4d4b7..0673f7c 100644 --- a/tests/test_lab_source_lib.py +++ b/tests/test_lab_source_lib.py @@ -62,21 +62,21 @@ def test_extract_dates_no_match() -> None: def test_is_owner_patient_text_owner() -> None: - text = "Дата рождения пациента\n20.03.1985" - ok, reason = is_owner_patient_text(text, owner_dob="1985-03-20") + text = "Дата рождения пациента\n15.06.1985" + ok, reason = is_owner_patient_text(text, owner_dob="1985-06-15") assert ok is True assert reason == "" def test_is_owner_patient_text_child() -> None: text = "Мальчик, 3 года" - ok, reason = is_owner_patient_text(text, owner_dob="1985-03-20") + ok, reason = is_owner_patient_text(text, owner_dob="1985-06-15") assert ok is False assert reason == "child_marker" def test_is_owner_patient_text_wrong_dob() -> None: text = "Дата рождения: 01.01.1980" - ok, reason = is_owner_patient_text(text, owner_dob="1985-03-20") + ok, reason = is_owner_patient_text(text, owner_dob="1985-06-15") assert ok is False assert reason.startswith("patient_dob=") diff --git a/tests/test_zip_safety.py b/tests/test_zip_safety.py new file mode 100644 index 0000000..f64ecfe --- /dev/null +++ b/tests/test_zip_safety.py @@ -0,0 +1,30 @@ +"""Tests for zip archive safety.""" +from __future__ import annotations + +import io +import zipfile + +import pytest + +from medbots.zip_safety import UnsafeZipError, validate_zip_archive + + +def _zip_bytes(members: dict[str, bytes]) -> bytes: + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w") as zf: + for name, data in members.items(): + zf.writestr(name, data) + return buf.getvalue() + + +def test_validate_zip_accepts_normal_archive() -> None: + data = _zip_bytes({"export.xml": b""}) + with zipfile.ZipFile(io.BytesIO(data)) as zf: + validate_zip_archive(zf, zip_size=len(data)) + + +def test_validate_zip_rejects_path_traversal() -> None: + data = _zip_bytes({"../etc/passwd": b"evil"}) + with zipfile.ZipFile(io.BytesIO(data)) as zf: + with pytest.raises(UnsafeZipError, match="unsafe zip member"): + validate_zip_archive(zf, zip_size=len(data))