diff --git a/.env.example b/.env.example index 938b66e..175fb4c 100644 --- a/.env.example +++ b/.env.example @@ -11,3 +11,10 @@ BOOKISH_CLEANUP_HOURS=1 # RATE LIMIT upload per IP/ora (0 = disabilitato, default 20) #BOOKISH_RATE_LIMIT_UPLOAD=20 + +# BUILD REPRODUCIBILE (R4b): sha256 di requirements.lock. Il default in +# docker-compose.yml è già il pin corrente; in dev sovrascrivi con: +# REQUIREMENTS_CHECKSUM="" → skippa il check in build locale +# quando aggiorni le dipendenze (pip-compile) aggiorna anche il pin: +# sed -i "s/[a-f0-9]\{64\}/$(sha256sum requirements.lock | awk '{print $1}')/" docker-compose.yml +#REQUIREMENTS_CHECKSUM=d13e4fb14a909e0bd06d20894860152d5334aa341dd9dec2e5e4ca368a0d2a62 diff --git a/.github/workflows/checksum.yml b/.github/workflows/checksum.yml new file mode 100644 index 0000000..cf8aeb6 --- /dev/null +++ b/.github/workflows/checksum.yml @@ -0,0 +1,34 @@ +name: deps-checksum + +# R4b: il lockfile è "pinnato" per costruzioni riproducibili. Questo job +# fallisce se l'hash in docker-compose.yml non coincide con requirements.lock. +# Quando aggiorni le dipendenze: +# pip-compile --output-file=requirements.lock requirements.in +# sed -i "s/d13e4fb14a909e0bd06d20894860152d5334aa341dd9dec2e5e4ca368a0d2a62/$(sha256sum requirements.lock | awk '{print $1}')/" docker-compose.yml + +on: + push: + branches: [main] + pull_request: + +jobs: + checksum: + runs-on: ubuntu-latest + name: checksum lockfile + steps: + - uses: actions/checkout@v4 + + - name: Calcola hash attuale + id: hash + run: | + H=$(sha256sum requirements.lock | awk '{print $1}') + echo "hash=$H" >> "$GITHUB_OUTPUT" + + - name: hash pinned in docker-compose.yml + run: | + PIN=$(grep -oP 'REQUIREMENTS_CHECKSUM: \$\{REQUIREMENTS_CHECKSUM:-\K[a-f0-9]{64}' docker-compose.yml | head -1) + [ -n "$PIN" ] || { echo "hash non pinnato in docker-compose.yml"; exit 1; } + echo "pinned=$PIN" + echo "atteso=${{ steps.hash.outputs.hash }}" + [ "$PIN" = "${{ steps.hash.outputs.hash }}" ] || { echo "checksum non aggiornato — rivedi docker-compose.yml"; exit 1; } + echo "checksum OK" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67cdfdc..520eb40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: docker compose logs bookish exit 1 - - name: Smoke: upload epub -> epub (conversione, SSE) + - name: "Smoke: upload epub -> epub (conversione, SSE)" run: | # finto epub (zip minimo vuoto accettato per estensione; la conversione # può fallire per formato, ma l'upload deve tornare 200 con task_id) @@ -67,11 +67,11 @@ jobs: echo "$RESP" echo "$RESP" | grep -q '"task_id"' || { echo "task_id mancante"; exit 1; } - - name: Smoke: CSP header presente + - name: "Smoke: CSP header presente" run: | curl -sI http://127.0.0.1:5000/ | grep -q "Content-Security-Policy" || { echo "CSP assente"; exit 1; } - - name: Smoke: auth opzionale funziona (override) + - name: "Smoke: auth opzionale funziona (override)" run: | cat > /tmp/override.yml << 'EOF' services: diff --git a/Dockerfile b/Dockerfile index a8a834f..0657ce0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,17 @@ FROM python:3.12-slim-bookworm AS builder +# Build riproducibile (R4b): se REQUIREMENTS_CHECKSUM è valorizzato, la +# build fallisce se requirements.lock non coincide — nessuna dipendenza può +# cambiare senza aggiornare il pin. Genera con: +# sha256sum requirements.lock | awk '{print $1}' +ARG REQUIREMENTS_CHECKSUM="" + WORKDIR /build COPY requirements.txt requirements.lock ./ +RUN test -z "$REQUIREMENTS_CHECKSUM" \ + -o "$(sha256sum requirements.lock | awk '{print $1}')" = "$REQUIREMENTS_CHECKSUM" \ + || { echo "FATAL: requirements.lock non corrisponde a REQUIREMENTS_CHECKSUM" >&2; exit 1; } RUN python -m venv /opt/venv \ && /opt/venv/bin/pip install --no-cache-dir --upgrade pip \ && /opt/venv/bin/pip install --no-cache-dir -r requirements.lock diff --git a/README.md b/README.md index 39ac048..f519d33 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,17 @@ pip install pip-tools pip-compile --strip-extras --no-hash --output-file requirements.lock requirements.txt ``` +La build Docker verifica che `requirements.lock` coincida con l'hash pinnato +in `docker-compose.yml` (`REQUIREMENTS_CHECKSUM`, R4b): se aggiorni le +dipendenze, aggiorna anche il pin: + +```bash +sed -i "s/[a-f0-9]\{64\}/$(sha256sum requirements.lock | awk '{print $1}')/" docker-compose.yml +``` + +(trova la riga `REQUIREMENTS_CHECKSUM:` — primo hash del file). La CI +(`.github/workflows/checksum.yml`) fallisce se il pin non coincide col lock. + ## Note sul container - Gunicorn, 1 worker + thread: i task SSE vivono in memoria del processo. diff --git a/docker-compose.yml b/docker-compose.yml index f295970..bc89d2d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,11 @@ services: bookish: - build: . + build: + context: . + args: + # Riproducibilita (R4b): sha256 di requirements.lock. Default = hash + # corrente; in dev puoi sovrascriverlo con REQUIREMENTS_CHECKSUM="" + REQUIREMENTS_CHECKSUM: ${REQUIREMENTS_CHECKSUM:-d13e4fb14a909e0bd06d20894860152d5334aa341dd9dec2e5e4ca368a0d2a62} image: bookish:local ports: - "${PORT:-5000}:5000" diff --git a/docs/review-docker-2026.md b/docs/review-docker-2026.md index 9f8d652..69d0a85 100644 --- a/docs/review-docker-2026.md +++ b/docs/review-docker-2026.md @@ -21,8 +21,9 @@ vanno chiuse prima di considerare il progetto "pronto" per un uso reale. Esito revisione: **❌ non pronta per produzione — servono correzioni**. > Aggiornamento (sessione corrente): la P0 è chiusa (**R1, R2, R3, R6**) e -> parte della P1 (**R4** lock + **CI** smoke test + **self-host JS offline**). -> Restano P1 R4b e P2 (R5, R7). +> parte della P1 (**R4** lock + **CI** smoke test + **self-host JS offline** + +> **R4b** checksum lock nel Dockerfile/CI). +> Restano P2 (R5, R7). --- @@ -227,10 +228,10 @@ rimosse, trust proxy ristretto via env. | R1 | Reader 404 su file convertiti | Alta | Alta | ✅ risolto | | R2 | Disk/CPU DoS via upload non autenticato | Alta | Media | ✅ mitigato (auth+rate) | | R3 | XSS da file HTML malevoli | Alta | Bassa | ✅ mitigato (CSP+DOMPurify) | -| R4 | Build non riproducibile | Media | Media | ✅ lockfile | +| R4 | Build non riproducibile | Media | Media | ✅ lockfile + checksum pinned | | R5 | Errori timeout conversioni lunghe | Media | Media | ⏳ P2 | | R6 | Trust proxy / opzioni fantasma | Bassa | Media | ✅ risolto | -| R7 | /tmp tmpfs vs conversioni grandi | Bassa | Bassa | ⏳ P1 | +| R7 | /tmp tmpfs vs conversioni grandi | Bassa | Bassa | ⏳ P2 | --- @@ -247,8 +248,9 @@ rimosse, trust proxy ristretto via env. ✅ `requirements.lock` (pip-compile, 112 righe) + Dockerfile lo usa. - [x] Self-hosting dei JS frontend (per offline). ✅ epub.js/pdf.js+worker/JSZip vendored in `static/vendor/`, CSP senza CDN. -- [ ] **R4b** `git` revision pin nel Dockerfile build-arg `--build-arg - requirements_checksum` (advanced). +- [x] **R4b** Hash del lockfile pinnato nel Dockerfile (build-arg + `REQUIREMENTS_CHECKSUM`, default in compose) + job CI di verifica + (`.github/workflows/checksum.yml`). ✅ fatto - [x] **R6** Cleanup gunicorn.conf (opzioni inesistenti; `forwarded_allow_ips` solo per proxy fidato noto). ✅ già fatto - [x] Smoke test Docker nella CI (`docker build` + `docker compose up` +