Promote dev to main: v0.2.0 — Python 3.14.6 runtime, wollomatic socket proxy, {total, items} list envelope, review remediation, onboarding fixes - #122
Merged
Conversation
…us build) (#18) * ci: publish scrye image to Docker Hub on tagged releases and dev pushes Add .github/workflows/publish.yml with two independent publishing paths: - semver tags (v*.*.*) whose commit is on main build the multi-arch (amd64/arm64) image and push <dockerhub-user>/scrye:<version> + :latest - pushes to dev push the moving <dockerhub-user>/scrye:dev continuous-build tag Extract the multi-arch build (QEMU + Buildx + build-push against docker/Dockerfile) into a reusable .github/actions/build-image composite action and refactor ci.yml's multi-arch build-check to consume it, so the build is defined in one place. Publishing uses the DOCKERHUB_USERNAME/ DOCKERHUB_TOKEN repo secrets; ci.yml still never publishes. Update CLAUDE.md and docs/PLAN.md (locked decision 0.6, §9.1, §13, Deviations) and add a CONTRIBUTING.md Releasing section describing both paths. * ci: gate multi-arch build-check to main pushes and PRs to main The image-multiarch job's arm64 leg builds the whole Dockerfile under QEMU emulation, which on a cold type=gha cache takes hours. Only main-scoped runs reliably restore a warm arm64 cache; dev-based PRs rebuild from scratch every time. Gate the check to main pushes and PRs whose base is main. Multi-arch buildability stays proven for dev by publish.yml (builds amd64+arm64 on every dev push and release tag), and dev PRs still run the fast amd64-only image build + dogfood self-scan, so no coverage is lost.
The :dev Docker Hub publish was triggered by on: push: branches: [dev], which fired on any commit reaching the dev ref — including conflict- resolution pushes to an open, unmerged promotion PR. Re-scope it to on: pull_request: types: [closed] with base dev, gated on pull_request.merged == true, and build the merged commit (merge_commit_sha). The :dev tag now publishes only when a PR is actually merged into dev. The tagged-release path (v*.*.* on main) is unchanged. Sync docs/PLAN.md (§0.6 + Deviations entry) and the CONTRIBUTING.md Releasing section to describe the merged-PR trigger.
Comprehensive report-only audit covering infrastructure/deployment, backend security, scanner orchestration, API/data/performance, frontend, feature completeness against docs/PLAN.md, and the previously-logged known limitations. Findings are stably ID'd with file:line references, CONFIRMED/PLAUSIBLE confidence markers, failure scenarios, and fix directions, plus a prioritized action list.
…webhook URLs (#23) Addresses the P0 tier of docs/reviews/full-audit-2026-07-05.md §10: - QUA-1: cap API-token minting against the caller's effective (token-capped) role, not the owner account's role, so a low-privilege token belonging to an admin can no longer mint an admin token. Adds a regression test. - API-2: run database restore (scrypt + full-DB rebuild) in a threadpool so /healthz stays responsive and the container can't be killed mid-restore. - API-3: chunked executemany restore inserts; yield_per streaming build; drop the redundant bundle re-parse; log + document the in-memory size ceiling. - API-10: raw-artifact files don't travel in a bundle, so exclude the artifacts table from the dump and clear it on restore (no dangling file references). - API-11: refuse restore (409) while a scan is queued or running. - SEC-1: treat a generic webhook URL as a write-only credential (encrypted, masked on read) like Discord; frontend renders it as a password field. Deviation logged in docs/PLAN.md; README backup section updated.
* perf(worker,api): P1 audit remediation — off-loop heavy work + bounded memory Addresses the P1 tier of docs/reviews/full-audit-2026-07-05.md §10 (availability and performance under real data volume): - API-5: off-load the scan worker's result persistence (10k+ findings flush + raw-JSON write) to a thread via anyio.to_thread, completing the systemic "no synchronous heavy work on the event loop" fix begun in P0 (API-2/3). - SCN-1: cap captured scanner stdout (SCRYE_SCANNER_MAX_OUTPUT_BYTES, default 512 MiB); output past the budget kills the child and fails the scan instead of buffering unbounded JSON. - API-4: read SBOM/backup uploads through read_upload_capped so an oversized body is rejected by reported size / chunked read, never fully buffered first. - API-7: dashboard/metrics load only needed columns per target (load_only) and serve from a short process-wide TTL cache, cleared on app startup and in tests. - API-1: eager-load scan tags (selectinload) in the two list endpoints. - API-15/API-6: run the maintenance tick (schedules + retention) off the event loop and batch retention deletes into one DELETE ... WHERE id IN (...). New tests cover the output cap and upload cap; the dashboard cache TTL/reset is tested. Deviation logged in docs/PLAN.md; .env.example regenerated. * ci,fix(config,compose): P2 audit remediation — supply chain + deployment hardening Addresses the P2 tier of docs/reviews/full-audit-2026-07-05.md §10: - SCN-3: parse the documented comma-separated env form for cors_origins and filesystem_scan_roots (NoDecode + a before-validator), so the filesystem-scan enable switch (SCRYE_FILESYSTEM_SCAN_ROOTS=/path) no longer fails at startup. Adds env-parsing tests. - INF-1: add .github/dependabot.yml (github-actions ecosystem, weekly, grouped). SHA-pinning each `uses:` needs current action SHAs, which this environment's egress policy blocks from resolving/verifying — flagged for follow-up rather than pinning to an unverified SHA (would risk red CI). - INF-3: align CLAUDE.md §6's :dev wording with the implemented merged-PR-into-dev trigger (doc alignment; no behavior change). - INF-2: document the fork-PR :dev publish limitation in publish.yml as an accepted trade-off; the push-based alternative is a §6 locked-decision change left for a deliberate call. - INF-4: document the trivy-server root exception (upstream image ships no non-root USER; mitigations noted), per the audit's accepted alternative. - INF-5: add a small tmpfs:[/run] to docker-socket-proxy (HAProxy needs a writable /run under read_only), with a live-verify note. Deviation logged in docs/PLAN.md. * feat(scanners,docs): P3 audit remediation — wire dead Settings knobs + README truth Addresses the P3 tier of docs/reviews/full-audit-2026-07-05.md §10 (feature gaps that mislead users): - FEAT-6 (QUA-3): apply the stored Grype ignore config at scan time — a new grype_policy module materializes the YAML into tmpfs and the worker passes it to Grype via a `-c` config flag (mirroring the Trivy policy path). - FEAT-7 (QUA-3): the New Scan form prefills its severity filter and ignore-unfixed toggle from GET /settings/scanners so instance defaults take effect instead of being overridden by hardcoded form values. - FEAT-4 (QUA-3): the maintenance tick honors auto_update_db + interval, running `trivy image --download-db-only` and `grype db update` best-effort when due. - DOC-1/2/5 + FEAT-1/2/3/8: README aligned with reality — Docker Hub publishing is in scope; uploaded image-tar, Docker-env multi-select scan, and filesystem-archive upload are marked not-implemented; VEX/.trivyignore are global (not per-scan); the ECR/GCR/ACR helper-binaries caveat is stated. - FEAT-5/FEAT-10: offline DB import and admin bulk secret re-encryption are listed as not-yet-implemented; the key-rotation README claim is corrected. New tests cover the Grype config flag/materialization and the DB-update tick. Deviation logged in docs/PLAN.md. * fix(frontend): P4 audit remediation — session expiry, UTC times, restore label, RBAC gating Addresses the P4 tier of docs/reviews/full-audit-2026-07-05.md §10 (frontend correctness / UX): - FE-1: the API client emits an auth-invalidated event on any 401 and AuthContext drops to the login screen, instead of leaving a stale authenticated shell whose every action fails. - FE-3: a shared lib/dates.ts (parseUtc/formatWhen) renders backend naive-UTC timestamps; Account/Backups/Scheduled-scans stop showing UTC as local, and the ScanDetail/Scans private helpers are de-duplicated onto it. - FE-4: BackupsPanel's restore file uses useState (not useRef) so the selected file name re-renders on the destructive restore flow. - FE-5: ScheduledScansPanel constrains the scanner Select by target type (SCANNERS_FOR matrix + auto-correct) and gates Add/Run/Delete behind an operator/admin check; the /settings route is now guarded (viewers → /). Verified with tsc, ESLint, Prettier, and a clean vite build (no frontend test runner yet — FE-10 deferred to P5). Deviation logged in docs/PLAN.md. * fix(frontend): track src/lib/dates.ts (was hidden by the Python lib/ gitignore) The shared date helper added for FE-3 lives under frontend/src/lib/, which the generic Python-oriented `lib/` rule in .gitignore silently excluded — so the file was never committed and CI's fresh checkout failed the frontend build (and the image build) with "Cannot find module '../../lib/dates'". Add a .gitignore exception for the frontend source lib directory and commit the file. * fix(backup,tests): P5 audit remediation — envelope KDF params, migration drift test, deviation log Addresses the P5 tier of docs/reviews/full-audit-2026-07-05.md §10 (maintainability, process, long tail): - item (g): backup restore derives the passphrase key from the bundle's advertised scrypt params (kdf.n/r/p) instead of the module constants, so a bundle written under a different (e.g. older) work factor still restores. derive_key / passphrase_cipher take explicit, validated n/r/p; restore passes the recorded values. - QUA-23: new tests/test_migrations.py runs the real Alembic chain to head against a throwaway DB and asserts the tables/columns match Base.metadata (the rest of the suite builds via create_all). alembic/env.py now respects a caller-provided URL. Deviation-logging debt recorded in docs/PLAN.md (required regardless of fix): FE-2 (hand-rolled API client), INF-10 (HIGH/CRITICAL dogfood floor), API-12 (created_at vs started_at index), FEAT-4 (DB-schedule actuation). QUA-4/QUA-9, QUA-16, and FE-10 are explicitly deferred with rationale. * docs: defer INF-2 explicitly until the repo goes public Per user decision (2026-07-05): keep the merged-PR-only :dev publish trigger for now — while the repo is private, fork-based contributions can't happen, so the fork-secrets gap can't be triggered. Record in docs/PLAN.md that INF-2 must be revisited specifically before the repo is made public, since that is the event that enables fork PRs (and therefore the actual bug). INF-3's CLAUDE.md wording stays matched to the current trigger.
Add guidance for landing multi-PR stacked batches: retarget each child PR's base to the true target branch immediately after its parent merges, re-state the full merge procedure before each merge rather than once per batch, and verify the target branch's actual content after the batch is reported complete instead of assuming merge order alone propagates changes through the stack.
) * ci: batch dev image to a nightly GHCR build; trim per-PR CI minutes Restructure dev-image publishing and cut CI-minute usage. - Add .github/workflows/dev-nightly.yml: build the dev branch multi-arch once nightly (04:00 UTC) + manual dispatch and push the moving ghcr.io/iamgroot60/scrye:dev via the built-in GITHUB_TOKEN. Scheduled runs skip when dev has no new commits in 24h. - publish.yml is now release-only: drop the merged-PR :dev job and its pull_request trigger. Docker Hub (<dockerhub-user>/scrye) is referenced only by the release path. - Split registries by role: Docker Hub for tagged releases, GHCR for dev. - Resolve audit INF-2: a schedule trigger is not PR-triggered, so the fork-withheld-secrets gap no longer applies. - ci.yml minute reduction: run the two informational scanner reports on pushes to main only (dev PRs keep just the gate scans); add a cache-scope input to the build-image action and partition the GHA cache (amd64-ci vs multiarch vs dev-multiarch) so amd64-only and multi-arch builds stop evicting each other. - Update CLAUDE.md §6, docs/PLAN.md §0.6 + Deviations, README, and CONTRIBUTING to the two-registry model; remove the obsolete dev-publish smoke-test doc. * docs: prefer read-only default Actions permissions with per-workflow packages:write An explicit permissions: block overrides the repo-level "Workflow permissions" default, so GHCR push does not require raising the repo-wide default to read/write. Recommend keeping the restrictive read-only default and letting dev-nightly.yml declare its own contents:read + packages:write, matching the least-privilege posture. Update docs/PLAN.md and CONTRIBUTING.md accordingly.
…unts, parallel scanner downloads (#34) The image CI work was dominated by the multi-arch build check, whose arm64 leg runs the whole Dockerfile under QEMU emulation. CI logs showed it rebuilding cold every run (0 cached layers): the deliberately-partitioned `type=gha` cache scope it uses (`multiarch`) is only written on rare main/release events, so its entries age out between runs and the emulated arm64 layers get re-executed from scratch instead of restored. Apply the fixes consistently across all four build paths without weakening the supply-chain posture (scanner checksum verification, digest-pinned bases, and the non-root hardened final stage are unchanged): - Cross-seed the GHA cache scopes. Each build path still WRITES exactly one scope (keeping the 10 GB budget partitioning), but now also READS the frequently-warm sibling scope. The shared build-image action gains an `extra-cache-scopes` input (cache-from = primary + extras, cache-to = primary only). image-multiarch and the release build read the nightly's warm `dev-multiarch`; the nightly reads `multiarch`; the amd64 dogfood build reads `dev-multiarch` for warm amd64 layers. - Persist pip/npm download caches with BuildKit cache mounts so an unchanged dependency isn't re-fetched when its install layer rebuilds; drop PIP_NO_CACHE_DIR (the cache lives in the mount, not the image layer). - Parallelize the trivy/grype/syft download+verify+extract pipelines (each in a background subshell joined by `wait`); a checksum mismatch in any still fails the build via `wait` under `set -e`. Documented in docs/PLAN.md § Build performance (with the do-not-undo invariants and a per-path before/after) and a guardrail rule in CLAUDE.md.
…ions bumps (#33) Reconciles dev with main after the dev->main promotion (#32, squash-merged) and the Dependabot github-actions group bump (#33) that landed directly on main. The promotion squash re-introduced main's older copy of already-promoted dev work, which conflicts with dev's newer versions (the #34 build-performance changes); all such conflicts are resolved in favour of dev. The only content this merge actually brings into dev is #33's action version bumps (checkout v7, setup-python v6, setup-node v6, setup-buildx v4, build-push v7, login v4). After this merge main is an ancestor of dev, so dev no longer shows as behind.
…ev (#35) The dev/main release model (squash-merged promotion PRs, plus Dependabot targeting the default branch main) leaves dev showing commits 'behind' main after each release. Two coupled changes stop that recurring: - .github/dependabot.yml: set target-branch to dev so github-actions bumps open against the integration branch instead of landing on main and never reaching dev. Removes the avoidable drift source. - CLAUDE.md and CONTRIBUTING.md: document a back-merge step — after each dev->main promotion (and after any commit that lands on main directly), merge main into dev, resolving squash-divergence conflicts in favour of dev. Handles the unavoidable promotion-squash case. See docs/PLAN.md § Deviations (2026-07-07) for the rationale and the one-time reconciliation performed alongside this change.
…rge process (#36) (#37) * ci: publish Scrye image to Docker Hub (tagged releases + dev continuous build) (#18) * ci: publish scrye image to Docker Hub on tagged releases and dev pushes Add .github/workflows/publish.yml with two independent publishing paths: - semver tags (v*.*.*) whose commit is on main build the multi-arch (amd64/arm64) image and push <dockerhub-user>/scrye:<version> + :latest - pushes to dev push the moving <dockerhub-user>/scrye:dev continuous-build tag Extract the multi-arch build (QEMU + Buildx + build-push against docker/Dockerfile) into a reusable .github/actions/build-image composite action and refactor ci.yml's multi-arch build-check to consume it, so the build is defined in one place. Publishing uses the DOCKERHUB_USERNAME/ DOCKERHUB_TOKEN repo secrets; ci.yml still never publishes. Update CLAUDE.md and docs/PLAN.md (locked decision 0.6, §9.1, §13, Deviations) and add a CONTRIBUTING.md Releasing section describing both paths. * ci: gate multi-arch build-check to main pushes and PRs to main The image-multiarch job's arm64 leg builds the whole Dockerfile under QEMU emulation, which on a cold type=gha cache takes hours. Only main-scoped runs reliably restore a warm arm64 cache; dev-based PRs rebuild from scratch every time. Gate the check to main pushes and PRs whose base is main. Multi-arch buildability stays proven for dev by publish.yml (builds amd64+arm64 on every dev push and release tag), and dev PRs still run the fast amd64-only image build + dogfood self-scan, so no coverage is lost. * ci(publish): scope :dev publish to merged PRs into dev The :dev Docker Hub publish was triggered by on: push: branches: [dev], which fired on any commit reaching the dev ref — including conflict- resolution pushes to an open, unmerged promotion PR. Re-scope it to on: pull_request: types: [closed] with base dev, gated on pull_request.merged == true, and build the merged commit (merge_commit_sha). The :dev tag now publishes only when a PR is actually merged into dev. The tagged-release path (v*.*.* on main) is unchanged. Sync docs/PLAN.md (§0.6 + Deviations entry) and the CONTRIBUTING.md Releasing section to describe the merged-PR trigger. * test: add throwaway marker to smoke-test the :dev publish on merge to dev (#21) * docs: add full-repository audit report (2026-07-05) (#22) Comprehensive report-only audit covering infrastructure/deployment, backend security, scanner orchestration, API/data/performance, frontend, feature completeness against docs/PLAN.md, and the previously-logged known limitations. Findings are stably ID'd with file:line references, CONFIRMED/PLAUSIBLE confidence markers, failure scenarios, and fix directions, plus a prioritized action list. * fix(security,backup): P0 audit remediation — token minting, restore, webhook URLs (#23) Addresses the P0 tier of docs/reviews/full-audit-2026-07-05.md §10: - QUA-1: cap API-token minting against the caller's effective (token-capped) role, not the owner account's role, so a low-privilege token belonging to an admin can no longer mint an admin token. Adds a regression test. - API-2: run database restore (scrypt + full-DB rebuild) in a threadpool so /healthz stays responsive and the container can't be killed mid-restore. - API-3: chunked executemany restore inserts; yield_per streaming build; drop the redundant bundle re-parse; log + document the in-memory size ceiling. - API-10: raw-artifact files don't travel in a bundle, so exclude the artifacts table from the dump and clear it on restore (no dangling file references). - API-11: refuse restore (409) while a scan is queued or running. - SEC-1: treat a generic webhook URL as a write-only credential (encrypted, masked on read) like Discord; frontend renders it as a password field. Deviation logged in docs/PLAN.md; README backup section updated. * Land P1–P5 audit remediation on dev (P0 already merged via #23) (#29) * perf(worker,api): P1 audit remediation — off-loop heavy work + bounded memory Addresses the P1 tier of docs/reviews/full-audit-2026-07-05.md §10 (availability and performance under real data volume): - API-5: off-load the scan worker's result persistence (10k+ findings flush + raw-JSON write) to a thread via anyio.to_thread, completing the systemic "no synchronous heavy work on the event loop" fix begun in P0 (API-2/3). - SCN-1: cap captured scanner stdout (SCRYE_SCANNER_MAX_OUTPUT_BYTES, default 512 MiB); output past the budget kills the child and fails the scan instead of buffering unbounded JSON. - API-4: read SBOM/backup uploads through read_upload_capped so an oversized body is rejected by reported size / chunked read, never fully buffered first. - API-7: dashboard/metrics load only needed columns per target (load_only) and serve from a short process-wide TTL cache, cleared on app startup and in tests. - API-1: eager-load scan tags (selectinload) in the two list endpoints. - API-15/API-6: run the maintenance tick (schedules + retention) off the event loop and batch retention deletes into one DELETE ... WHERE id IN (...). New tests cover the output cap and upload cap; the dashboard cache TTL/reset is tested. Deviation logged in docs/PLAN.md; .env.example regenerated. * ci,fix(config,compose): P2 audit remediation — supply chain + deployment hardening Addresses the P2 tier of docs/reviews/full-audit-2026-07-05.md §10: - SCN-3: parse the documented comma-separated env form for cors_origins and filesystem_scan_roots (NoDecode + a before-validator), so the filesystem-scan enable switch (SCRYE_FILESYSTEM_SCAN_ROOTS=/path) no longer fails at startup. Adds env-parsing tests. - INF-1: add .github/dependabot.yml (github-actions ecosystem, weekly, grouped). SHA-pinning each `uses:` needs current action SHAs, which this environment's egress policy blocks from resolving/verifying — flagged for follow-up rather than pinning to an unverified SHA (would risk red CI). - INF-3: align CLAUDE.md §6's :dev wording with the implemented merged-PR-into-dev trigger (doc alignment; no behavior change). - INF-2: document the fork-PR :dev publish limitation in publish.yml as an accepted trade-off; the push-based alternative is a §6 locked-decision change left for a deliberate call. - INF-4: document the trivy-server root exception (upstream image ships no non-root USER; mitigations noted), per the audit's accepted alternative. - INF-5: add a small tmpfs:[/run] to docker-socket-proxy (HAProxy needs a writable /run under read_only), with a live-verify note. Deviation logged in docs/PLAN.md. * feat(scanners,docs): P3 audit remediation — wire dead Settings knobs + README truth Addresses the P3 tier of docs/reviews/full-audit-2026-07-05.md §10 (feature gaps that mislead users): - FEAT-6 (QUA-3): apply the stored Grype ignore config at scan time — a new grype_policy module materializes the YAML into tmpfs and the worker passes it to Grype via a `-c` config flag (mirroring the Trivy policy path). - FEAT-7 (QUA-3): the New Scan form prefills its severity filter and ignore-unfixed toggle from GET /settings/scanners so instance defaults take effect instead of being overridden by hardcoded form values. - FEAT-4 (QUA-3): the maintenance tick honors auto_update_db + interval, running `trivy image --download-db-only` and `grype db update` best-effort when due. - DOC-1/2/5 + FEAT-1/2/3/8: README aligned with reality — Docker Hub publishing is in scope; uploaded image-tar, Docker-env multi-select scan, and filesystem-archive upload are marked not-implemented; VEX/.trivyignore are global (not per-scan); the ECR/GCR/ACR helper-binaries caveat is stated. - FEAT-5/FEAT-10: offline DB import and admin bulk secret re-encryption are listed as not-yet-implemented; the key-rotation README claim is corrected. New tests cover the Grype config flag/materialization and the DB-update tick. Deviation logged in docs/PLAN.md. * fix(frontend): P4 audit remediation — session expiry, UTC times, restore label, RBAC gating Addresses the P4 tier of docs/reviews/full-audit-2026-07-05.md §10 (frontend correctness / UX): - FE-1: the API client emits an auth-invalidated event on any 401 and AuthContext drops to the login screen, instead of leaving a stale authenticated shell whose every action fails. - FE-3: a shared lib/dates.ts (parseUtc/formatWhen) renders backend naive-UTC timestamps; Account/Backups/Scheduled-scans stop showing UTC as local, and the ScanDetail/Scans private helpers are de-duplicated onto it. - FE-4: BackupsPanel's restore file uses useState (not useRef) so the selected file name re-renders on the destructive restore flow. - FE-5: ScheduledScansPanel constrains the scanner Select by target type (SCANNERS_FOR matrix + auto-correct) and gates Add/Run/Delete behind an operator/admin check; the /settings route is now guarded (viewers → /). Verified with tsc, ESLint, Prettier, and a clean vite build (no frontend test runner yet — FE-10 deferred to P5). Deviation logged in docs/PLAN.md. * fix(frontend): track src/lib/dates.ts (was hidden by the Python lib/ gitignore) The shared date helper added for FE-3 lives under frontend/src/lib/, which the generic Python-oriented `lib/` rule in .gitignore silently excluded — so the file was never committed and CI's fresh checkout failed the frontend build (and the image build) with "Cannot find module '../../lib/dates'". Add a .gitignore exception for the frontend source lib directory and commit the file. * fix(backup,tests): P5 audit remediation — envelope KDF params, migration drift test, deviation log Addresses the P5 tier of docs/reviews/full-audit-2026-07-05.md §10 (maintainability, process, long tail): - item (g): backup restore derives the passphrase key from the bundle's advertised scrypt params (kdf.n/r/p) instead of the module constants, so a bundle written under a different (e.g. older) work factor still restores. derive_key / passphrase_cipher take explicit, validated n/r/p; restore passes the recorded values. - QUA-23: new tests/test_migrations.py runs the real Alembic chain to head against a throwaway DB and asserts the tables/columns match Base.metadata (the rest of the suite builds via create_all). alembic/env.py now respects a caller-provided URL. Deviation-logging debt recorded in docs/PLAN.md (required regardless of fix): FE-2 (hand-rolled API client), INF-10 (HIGH/CRITICAL dogfood floor), API-12 (created_at vs started_at index), FEAT-4 (DB-schedule actuation). QUA-4/QUA-9, QUA-16, and FE-10 are explicitly deferred with rationale. * docs: defer INF-2 explicitly until the repo goes public Per user decision (2026-07-05): keep the merged-PR-only :dev publish trigger for now — while the repo is private, fork-based contributions can't happen, so the fork-secrets gap can't be triggered. Record in docs/PLAN.md that INF-2 must be revisited specifically before the repo is made public, since that is the event that enables fork PRs (and therefore the actual bug). INF-3's CLAUDE.md wording stays matched to the current trigger. * docs: add stacked-PR landing rules to CLAUDE.md (#30) Add guidance for landing multi-PR stacked batches: retarget each child PR's base to the true target branch immediately after its parent merges, re-state the full merge procedure before each merge rather than once per batch, and verify the target branch's actual content after the batch is reported complete instead of assuming merge order alone propagates changes through the stack. * ci: batch dev image to a nightly GHCR build; trim per-PR CI minutes (#31) * ci: batch dev image to a nightly GHCR build; trim per-PR CI minutes Restructure dev-image publishing and cut CI-minute usage. - Add .github/workflows/dev-nightly.yml: build the dev branch multi-arch once nightly (04:00 UTC) + manual dispatch and push the moving ghcr.io/iamgroot60/scrye:dev via the built-in GITHUB_TOKEN. Scheduled runs skip when dev has no new commits in 24h. - publish.yml is now release-only: drop the merged-PR :dev job and its pull_request trigger. Docker Hub (<dockerhub-user>/scrye) is referenced only by the release path. - Split registries by role: Docker Hub for tagged releases, GHCR for dev. - Resolve audit INF-2: a schedule trigger is not PR-triggered, so the fork-withheld-secrets gap no longer applies. - ci.yml minute reduction: run the two informational scanner reports on pushes to main only (dev PRs keep just the gate scans); add a cache-scope input to the build-image action and partition the GHA cache (amd64-ci vs multiarch vs dev-multiarch) so amd64-only and multi-arch builds stop evicting each other. - Update CLAUDE.md §6, docs/PLAN.md §0.6 + Deviations, README, and CONTRIBUTING to the two-registry model; remove the obsolete dev-publish smoke-test doc. * docs: prefer read-only default Actions permissions with per-workflow packages:write An explicit permissions: block overrides the repo-level "Workflow permissions" default, so GHCR push does not require raising the repo-wide default to read/write. Recommend keeping the restrictive read-only default and letting dev-nightly.yml declare its own contents:read + packages:write, matching the least-privilege posture. Update docs/PLAN.md and CONTRIBUTING.md accordingly. * perf(docker): speed up image builds via cache cross-seeding, cache mounts, parallel scanner downloads (#34) The image CI work was dominated by the multi-arch build check, whose arm64 leg runs the whole Dockerfile under QEMU emulation. CI logs showed it rebuilding cold every run (0 cached layers): the deliberately-partitioned `type=gha` cache scope it uses (`multiarch`) is only written on rare main/release events, so its entries age out between runs and the emulated arm64 layers get re-executed from scratch instead of restored. Apply the fixes consistently across all four build paths without weakening the supply-chain posture (scanner checksum verification, digest-pinned bases, and the non-root hardened final stage are unchanged): - Cross-seed the GHA cache scopes. Each build path still WRITES exactly one scope (keeping the 10 GB budget partitioning), but now also READS the frequently-warm sibling scope. The shared build-image action gains an `extra-cache-scopes` input (cache-from = primary + extras, cache-to = primary only). image-multiarch and the release build read the nightly's warm `dev-multiarch`; the nightly reads `multiarch`; the amd64 dogfood build reads `dev-multiarch` for warm amd64 layers. - Persist pip/npm download caches with BuildKit cache mounts so an unchanged dependency isn't re-fetched when its install layer rebuilds; drop PIP_NO_CACHE_DIR (the cache lives in the mount, not the image layer). - Parallelize the trivy/grype/syft download+verify+extract pipelines (each in a background subshell joined by `wait`); a checksum mismatch in any still fails the build via `wait` under `set -e`. Documented in docs/PLAN.md § Build performance (with the do-not-undo invariants and a per-path before/after) and a guardrail rule in CLAUDE.md. * docs,ci: add post-promotion back-merge step; retarget Dependabot to dev (#35) The dev/main release model (squash-merged promotion PRs, plus Dependabot targeting the default branch main) leaves dev showing commits 'behind' main after each release. Two coupled changes stop that recurring: - .github/dependabot.yml: set target-branch to dev so github-actions bumps open against the integration branch instead of landing on main and never reaching dev. Removes the avoidable drift source. - CLAUDE.md and CONTRIBUTING.md: document a back-merge step — after each dev->main promotion (and after any commit that lands on main directly), merge main into dev, resolving squash-divergence conflicts in favour of dev. Handles the unavoidable promotion-squash case. See docs/PLAN.md § Deviations (2026-07-07) for the rationale and the one-time reconciliation performed alongside this change.
…#38) * docs: overhaul README/CONTRIBUTING; split PLAN into ARCHIVE + ROADMAP Rewrite the documentation to match the current codebase and separate the historical build record from forward-looking planning. - README.md: rewritten from the verified codebase. Deeper Docker deployment section (prerequisites, master-key generation, Docker Hub vs GHCR vs local image, compose invocation, first-run admin bootstrap, persistent-data layout, optional sidecars, reverse-proxy setup, first-run troubleshooting incl. the read-only-root /cache cache-path class of issue). Completed the env-var table (adds SCRYE_FORWARDED_ALLOW_IPS, SCRYE_SCANNER_MAX_OUTPUT_BYTES, SCRYE_SCANNER_CACHE_DIR). Added a UI-based "Configuring OIDC" section. Security model, backup/restore, and monitoring refreshed. Roadmap now links to docs/ROADMAP.md (forward) and docs/ARCHIVE.md (history). - CONTRIBUTING.md: merged the two duplicate "Releasing" sections into one with subsections; updated the docs/ tree and deviation-log pointer to ARCHIVE.md/ROADMAP.md; dropped the now-historical phase/PX branch guidance. - docs/PLAN.md -> docs/ARCHIVE.md: preserved verbatim as the historical build record (phase order, locked decisions, deviations log, build-performance notes); header updated to describe its archival role. - docs/ROADMAP.md: new forward-looking roadmap (near/medium/longer-term) plus known limitations and accepted trade-offs (SBOM content-identity, OIDC/MFA policy scope, key-rotation re-encryption tool, offline DB import, etc.). - CLAUDE.md: repointed docs/PLAN.md references to docs/ARCHIVE.md and noted docs/ROADMAP.md for forward-looking work. * docs,ci: go public and consolidate publishing to GHCR-only Scrye's repository is going public; drop Docker Hub entirely and publish everything to GHCR, and update the docs for a public audience. Publishing (GHCR-only): - publish.yml now builds release tags to ghcr.io/iamgroot60/scrye:<version> and :latest, authenticating with the built-in GITHUB_TOKEN (packages:write) instead of the DOCKERHUB_USERNAME/DOCKERHUB_TOKEN secrets. Adds a canonical-repo guard so a fork that pushes a tag skips instead of failing. - The nightly :dev build (dev-nightly.yml) was already GHCR/GITHUB_TOKEN; only its comments changed. ci.yml, the composite build action, and dependabot.yml had their Docker-Hub-era comments corrected. - INF-2 (the fork-PR :dev secrets gap, previously deferred because the repo was private) is now fully closed: both publish paths are triggered outside pull_request and use GITHUB_TOKEN, so no pull_request-triggered workflow carries a registry secret. The DOCKERHUB_* repo secrets are now unused. Public-repo governance: - Add .github/CODEOWNERS (owner review) and SECURITY.md (private vulnerability reporting, supported-tags table, scope). - Branch protection and signed-commit enforcement are repository settings, not files; tracked as a checklist in docs/ROADMAP.md. Docs: - README: GHCR-only distribution; a complete standalone pull-from-GHCR docker-compose.yml so deploying needs no clone (cloning is now a separate "build from source" path); env vars categorized by necessity (required/conditional/optional); real nginx/Caddy/Traefik reverse-proxy examples; clearer optional-sidecar necessity; GHCR + CI badges; dropped the build-history pointer from the Roadmap section. - CONTRIBUTING § Releasing rewritten for GHCR; ROADMAP reflects the public repo (free arm64 runners, governance checklist); CLAUDE.md locked decision §6 rewritten GHCR-only. Dated deviation entry added to docs/ARCHIVE.md.
…39) Three frontend/backend fixes: - Theme: replace Mantine's minty default teal with the Tailwind teal ramp; primary is teal-700 (#0f766e) in light mode and teal-600 (#0d9488) in dark mode, with autoContrast + luminanceThreshold 0.2 so filled controls stay legible. All primary usages clear WCAG AA (light 5.47:1, dark filled 5.61:1, dark text 4.60:1). Applies everywhere color="teal" resolves via primaryShade (wordmark, nav, buttons, badges, loaders, pagination). - Scans: add DELETE /api/scans/{id} (operator role + CSRF, terminal-status only). Removing the scan cascades to its findings, artifact-metadata rows, and tags via the existing ORM/FK cascade; the on-disk artifact directory is removed via a new remove_scan_artifacts() helper. No schema change, so no migration. A confirmation modal + Delete button is added to the scan detail page. Deleted scans stop feeding the dashboard aggregates and drop out of history/diffs. Backend tests cover full cleanup, RBAC, CSRF, and queued/404. - Nav: fix the active-link matching that lit both "Scans" and "New scan" on /scans/new. The active item is now the longest matching nav path (exact or "to/" prefix), so each item highlights only for its own route. See docs/ARCHIVE.md § Deviations for details.
Update all references to the old GitHub username across the repo after the account rename: - GitHub URLs (github.com/IamGroot60/Scrye → github.com/tyler-rich/Scrye) in README, SECURITY.md, and docs. - GHCR image paths (ghcr.io/iamgroot60/scrye → ghcr.io/tyler-rich/scrye) in README, CONTRIBUTING, CLAUDE.md, docs/ARCHIVE.md, and both publish/nightly workflows. - README badges (CI + GHCR container); escape the dash in the shields.io GHCR badge (tyler--rich) so it renders literally. - CODEOWNERS catch-all owner and LICENSE copyright holder. - Git-identity rule in CLAUDE.md, including the noreply email (170156756+tyler-rich@users.noreply.github.com; numeric ID unchanged). - Workflow repo-owner fork guards: github.repository == 'tyler-rich/Scrye' in both dev-nightly.yml (nightly build) and publish.yml (release build). GHCR auth is unchanged: both workflows use github.actor + GITHUB_TOKEN, which are repo-scoped and carry no hardcoded username.
…le) (#43) Docker Scout scan of the published ghcr.io/tyler-rich/scrye:0.1.0 image surfaced ten CVEs, all inside the bundled upstream scanner binaries' embedded Go stdlib / oras-go / moby / sigstore-timestamp-authority modules — none in Scrye's own attack surface. Version-bump-if-available check outcome: Trivy 0.72.0, Grype 0.115.0, and Syft 1.46.0 are each already the latest available upstream release, so no bump resolves any of the findings yet. Left the Dockerfile pins unchanged and logged the CVE -> upstream-fix-version mapping as a tracked limitation in docs/ARCHIVE.md § Deviations, per the existing bundled-binary CVE-tracking pattern. No binary changed, so the image is unaffected and the app version stays 0.1.0 (no 0.1.1 cut).
* Add files via upload * docs: wire up README screenshots Move uploaded screenshots into docs/screenshots/ and replace the README placeholder table with the real dashboard, new-scan, results, and history captures. Drop the now-stale roadmap item.
Cap each screenshot to a fixed width via HTML img tags so the table renders uniformly regardless of each capture's native dimensions.
Crop each screenshot to its actual content, trimming the large blank page background that made captures like New scan and History look tiny next to Results. Render all four at a fixed 260x200 box so the table is visually consistent.
GitHub's markdown CSS forces height:auto on images, so the explicit height attribute from the previous fix had no effect and thumbnails still rendered at different sizes. Pad each screenshot with matching background color to a common canvas height instead, so a single width attribute renders all four uniformly.
The previous fix cropped each screenshot to its content, then padded back up to the tallest capture's height (1241px) — which happened to equal the original canvas size, silently undoing the crop and leaving the thumbnails just as mismatched as before. Crop all four to the same 900px window from the original captures instead, so they share actual pixel dimensions with no padding involved.
…thon 3.13 interpreter CVEs (#51) Pin curl/libcurl3-gnutls/libcurl4 to 7.88.1-10+deb12u15 (the Debian bookworm-security fix for CVE-2026-5773) in the runtime stage; the fix is already in the base image's apt snapshot, so no base-image digest bump was needed. Waive the four CPython interpreter-binary CVEs on Python 3.13.14 (CVE-2026-15308, CVE-2026-12003, CVE-2025-15366, CVE-2025-15367) in ci/grype.yaml with per-group review dates. Decision: stay on Python 3.13 for now; the 3.14 move is deferred to a scoped project (docs/upgrades/python-3.14.md). Full rationale in docs/ARCHIVE.md §14. Tracking: #52
…bian run (#96) The wollomatic migration (#63) left one item open: boot the published image on a Docker-capable host and confirm DOCKER_GID is right for it. That run happened. The allowlist behaved exactly as documented -- digest-pinned image up under 65534:<gid> with a read-only rootfs, GET /images/json and /v1.NN/images/json 200, /info + /containers/json + /version 403, POST 405. Four operational behaviors it surfaced were not documented, or were documented wrongly: - DOCKER_GID was 989 on the host, not the 999 the Compose fallback assumes. The README called 999 "the Debian/Ubuntu default", which reads as probably-right; it is a packaging convention and was wrong here. Reframed as a placeholder, with the measured value cited. The stat derivation is unchanged. - The -allowfrom source check is evaluated before the path and method rules, so a request from a non-matching source gets 403 for everything, including methods that would otherwise answer 405. A wrong source and a disallowed path are indistinguishable from the client, which sends anyone debugging "403 on an endpoint that should work" at the -allowGET regex instead of the source check. The proxy log does distinguish them (blocked request ... forbidden IP). - A wrong GID crash-loops the sidecar rather than stopping it: it logs dial unix /var/run/docker.sock: connect: permission denied, exits, and restart: unless-stopped retries it under Docker backoff. STATUS reads Restarting, never Exited. The README said it "stays down". - From the client that crash loop is a connection error, not an HTTP status (curl reports 000); list_images() raises from httpx.HTTPError rather than from a non-200. Both surface as 502, so the detail text -- not the status code -- is what separates a broken sidecar from a restrictive allowlist. Also re-confirmed structurally that a failing proxy cannot block app startup: no depends_on exists in any Compose file, for any service, in any profile, so there is no service_healthy gate and the proxy healthcheck is consumed by nothing but itself. Scrye starts and stays up while the sidecar cycles. Docs only -- no code, Compose, CI, or configuration change; the -allowGET pattern and the sidecar option set are untouched. See docs/ARCHIVE.md § Deviations (2026-07-26), which also tracks the two non-doc spots left alone by that scope.
…and Compose comment (#97) The non-200 detail in docker_proxy.list_images() advised checking that the proxy is "read-only with IMAGES=1" — a tecnativa env var that has not existed since the wollomatic migration (#89), printed in exactly the 403 case the README teaches operators to diagnose. It now names the real causes: a non-allowlisted path (-allowGET permits only /images/json and /v1.NN/images/json), a non-GET method, or the -allowfrom source check rejecting a non-scrye client, with the note that the proxy's log distinguishes them, and a pointer to the README rather than a restatement of it. The docker-compose.yml DOCKER_GID comment called 999 "the Debian/Ubuntu default"; it now matches the README's corrected wording (a placeholder, not a safe default; 989 on the host this was verified against) and describes the wrong-GID failure as the crash loop it actually is. The stat -c '%g' derivation remains the instruction and the ${DOCKER_GID:-999} value is unchanged. Adds a regression test driving the 403 path through httpx.MockTransport. No existing test asserted on the old string. See docs/ARCHIVE.md § Deviations for the dated entry retiring both follow-ups.
…vers at #98 (#99) Re-checked CVE-2026-15308 and CVE-2026-12003 against the CPython `3.14` branch and the `v3.14.6` tag rather than Grype's FIXED IN column, per CLAUDE.md § Dependency hygiene. Both fixes are merged to the 3.14 maintenance branch (python/cpython#153039, #151682) and absent from 3.14.6, so 3.14.7 is the first release carrying either. Both waivers stand; the 2026-10-25 review date is unchanged. The Group A block already read "next 3.14.x" — no trigger correction was needed. What it lacked was the named release, a record of the fixes being verified rather than assumed, and a tracking reference that matches the current runtime: the shared header pointed all four CVEs at issue #52, which is 3.13-scoped and stale since the 3.14 move. Group A now cites the new Group-A-only issue #98; the header no longer claims one tracker for both groups. Group B is untouched. See docs/ARCHIVE.md § Deviations (2026-07-26) for the full record.
…b backport landed on 3.14 (#100) Group B's rationale — upstream declined the backport to 3.10-3.14, so these are fixed only in 3.15+ — is no longer true of CVE-2025-15366. Lib/imaplib.py on the 3.14 and 3.13 branches carries the _control_chars guard in IMAP4._command() (python/cpython#153137, commit 2981822, merged 2026-07-07); v3.14.6 does not, and the gh-143921 NEWS entry is still under Misc/NEWS.d/next/Security. That is Group A shape: merged, unreleased, closing on 3.14.7. poplib is unchanged — its guard is on main only, and gh-143923 lists no backport PR. The two diverged because imaplib's check was narrowed to NUL/CR/LF (python/cpython#153067), clearing the regression concern behind the original decline; poplib's still rejects the full control-character range. Group B keeps its standing-acceptance framing, its annual review date (2027-07-25), and the do-not-scope-a-3.15-move instruction, now for CVE-2025-15367 alone. The waiver list is unchanged at four entries. The 2026-07-25 entry is superseded on this one point and left unedited; its reading of 3.14.6 was correct, but the backport had already merged 18 days before it was written. See docs/ARCHIVE.md § Deviations (2026-07-26) for the evidence.
…work) (#101) Read-only audit producing findings and proposals only; no code, config, or existing document is changed. - Part 1: true status of all five open issues against the code on dev — #63 and #83 are fully resolved (#89, #87) and only open because closing keywords never fire on a dev-targeted PR; #98 and #52 are live waiver trackers; #75 needs a release. - Part 2: full recursive docs/ inventory with a KEEP/ARCHIVE/DELETE disposition per file, the inbound-reference blast radius for each proposed removal, and a proposed docs/history/ structure. - Part 3: consolidated list of deferred work still outstanding in ARCHIVE.md section 14, separating items with a live tracking reference from the three recorded in prose only. - Part 4: ARCHIVE.md navigability (including 14 deviation entries misfiled under the Build performance heading), ROADMAP items now done, the Unreleased changelog backlog, 33 prunable merged branches, and proposed CLAUDE.md wording for two operational lessons.
…s structure, correct a false CVE claim in the CHANGELOG (#102) * docs(archive): make §14 contiguous, add a newest-first index, and add the finding-ID decoder Twelve dated §14 entries — every one from 2026-07-09 onward, including all the recent work — sat underneath `## Build performance` rather than under §14, so anyone scrolling §14 to the end stopped short of them. Moved the Build performance section to the end of the file instead of re-parenting the entries: it is self-contained and cross-referenced by heading name (from CLAUDE.md and four workflows), not by position, so nothing breaks. All 104 dated entries are now under §14. Added a newest-first index at the top of §14, one anchored line per entry. The entries themselves are deliberately NOT reordered: sixteen of them refer to each other relatively ("the entry below", "superseded by the entry above"), and a sort would silently invert every one. The three ordering regimes are documented instead, and the index is sorted by date regardless of physical position, so lookup no longer depends on the scroll order. Added §15, a finding-ID index: one row per SEC/SC/APIR/CON/P1-P3/D/R/QUA/INF/ FE/API/FEAT/DOC/SCN id with a one-line description and its resolving PR. §14 cites these ids bare and never re-explains them; this is the decoder that replaces the docs/reviews/ reports. It also records the SEC-* prefix collision between the two reviews that reused it. Corrected §0 locked decision #7: it said CVE-2025-15366 and CVE-2025-15367 are both unfixable on 3.14. That is true of released 3.14.6 but not of the 3.14 line — the imaplib backport landed on the maintenance branch and closes on 3.14.7 (issue #98). Only the poplib CVE remains 3.15-only (#52). * docs(changelog): correct the CVE-2025-15366 claim under [Unreleased] The Python 3.14 entry said all four waived CPython CVEs remain unfixable until 3.15 because upstream declined the backport to 3.10-3.14, and pointed at issue #52 for all of them. Both halves are false, and this text ships verbatim as the next release's notes. Checked against ci/grype.yaml and the two 2026-07-26 §14 entries: the imaplib backport for CVE-2025-15366 merged onto the CPython 3.14 branch on 2026-07-07 — 18 days before the entry was written — so it closes on 3.14.7, not 3.15, and it was regrouped into Group A alongside CVE-2026-15308 and CVE-2026-12003, tracked in issue #98. Only CVE-2025-15367 (poplib) is genuinely 3.15-only and still tracked in #52. What was true and is kept: released 3.14.6 carries neither guard, so the upgrade cleared nothing at the pinned version. * docs: strike completed roadmap items, surface the settings-level work, add two process rules ROADMAP: - Struck "Pin GitHub Actions to commit SHAs" (done in #57 — ci.yml has 8 SHA-pinned uses:, dev-nightly 3, publish 3, rescan 2) and "Frontend test runner" (done in #78 — vitest 3.2.7, "test": "vitest run", 20 test files). - Rewrote "Row-bound secret AAD", which was false as stated: row binding is implemented (secret_store.py row_aad(), L1/SEC-7, #64) and every write is row-bound. What remains is only the bulk re-encryption of legacy column-only ciphertext so the read fallback can be dropped, so it is folded into the existing "Admin bulk secret re-encryption" item. - Extended the public-repo governance checklist with five settings-level items that existed only in §14 prose and were therefore invisible: Actions workflow permissions -> read-only, confirm GHCR package visibility is public (the original check asked for Private and its premise inverted when the repo went public), delete the unused DOCKERHUB_* secrets, set the GitHub profile display name to tyler-rich, and confirm Dependabot security alerts are on. CLAUDE.md § Git & PR conventions gains two rules learned the hard way: a stacked child PR retargeted after its parent was squash-merged needs git rebase --onto (flipping the base in the UI re-computes the merge base and balloons the diff), and on: pull_request does not fire on 'edited', so a base change never re-runs CI and the green check you are looking at is from the old base. CONTRIBUTING § Releasing gains a "Before you tag" checklist — CHANGELOG [Unreleased] reviewed (it ships verbatim), THIRD_PARTY_LICENSES verified against the versions actually pulled, Dependabot PRs triaged, requirements.lock regenerated — plus the two after-tag steps: back-merge main into dev, and re-run rescan.yml. * docs: delete docs/reviews and docs/upgrades, sweep every inbound reference The twelve review reports and the Python 3.14 handoff doc held only closed findings, and sat at the same directory level as the two live documents. They are removed rather than moved to a docs/history/ subtree: the only real argument against deleting them was that §14 cites their finding ids bare and never re-explains them, and §15 (previous commit) answers that directly. Git is the archive for the rest. docs/ now contains exactly ARCHIVE.md, ROADMAP.md, and screenshots/. The originals stay retrievable — the pre-deletion commit is 0780b07, and the §14 entry records the git show incantation. Nothing was rewritten before deletion. Swept every inbound reference the audit enumerated, plus the ones it did not: CONTRIBUTING § Project layout (both directories dropped, screenshots/ added), CONTRIBUTING § API conventions, CHANGELOG's L13/APIR-8 citation, and both dependabot.yml D3 comments now point at ARCHIVE §15. Inside §14, 59 docs/reviews/ and 7 docs/upgrades/ path prefixes were stripped so the entries name the reports as documents rather than as paths that no longer resolve, with a note at the top of §14 sending the reader to §15. Fixed the dead claude-md-compliance-review.md link (a filename that never existed). Left one docs/upgrades/ mention deliberately: the 2026-07-25 entry's record of what CONTRIBUTING's layout listing omitted is a statement about that date, marked '(as it then was)'. Verified no workflow, test, or source file referenced either directory, and that the four '§ Build performance' cross-references are by heading name and survive that section's move. Added the dated §14 entry recording all of it, including that docs/history/ was considered and rejected, and two corrections to the audit: twelve entries were misfiled under Build performance (not fourteen — the other two are that section's own sub-headings), and there are 42 remote branches with 36 prunable (not 39/33).
…103) - actions/checkout 7.0.0 -> 7.0.1 - actions/setup-python 6.3.0 -> 7.0.0 - actions/setup-node 6.4.0 -> 7.0.0 - docker/login-action 4.4.0 -> 4.5.1 - actions/attest-build-provenance 2.4.0 -> 4.1.1 Every pinned SHA was verified against the upstream tag it claims. The two runner-action majors drop inputs we do not use (setup-python's pip-install, setup-node's dummy NODE_AUTH_TOKEN export) and are otherwise ESM/dependency refreshes. attest-build-provenance is the only bump with real semantics: v3 moved to the node24 runtime and added create-storage-record (default true), and v4 reduced the action to a wrapper over actions/attest. Storage records are only created for organization-owned repos, so this user-owned repo skips that path and needs no artifact-metadata: write scope; the subject-name/subject-digest/ push-to-registry inputs are unchanged. Note both facts inline next to the step.
#104) Rolls the pinned digest for the scanner-download stage forward to the current debian:bookworm-slim (verified against the registry: the tag resolves to 7b140f37... today). That stage curls and checksum-verifies the trivy/grype/syft binaries, so its curl and CA bundle should not drift; none of its layers ship in the runtime image. Split out of the grouped Dependabot docker-images bump, whose other half (node 22 -> 26 for the frontend builder) is a major on a non-LTS Node line and is evaluated separately.
…5 updates (#105) Runtime: - fastapi 0.139.0 -> 0.140.0 - pydantic-settings 2.7.1 -> 2.14.2 - alembic 1.14.0 -> 1.18.5 Dev-only: - pytest 9.0.3 -> 9.1.1 - black 26.3.1 -> 26.5.1 Regenerates backend/requirements.lock with the pinned uv command from CONTRIBUTING § Backend dependency lock (uv 0.8.17, uv pip compile pyproject.toml --group build --generate-hashes --python-version 3.14), which the Dependabot PR did not do — without it CI's lock-drift gate fails. The lock diff is confined to the three bumped runtime packages plus a new typing-inspection 'via' edge; no new distributions enter the closure. Verified on CPython 3.14.6 against the regenerated lock: 580 passed, 3 skipped; ruff and black --check clean under black 26.5.1; alembic upgrade head and downgrade base both run end to end under 1.18.5; and regenerating .env.example from the Settings model produces no diff, with init > env > dotenv precedence and the NoDecode comma-split fields still behaving as documented.
…ion §2) (#106) The npm entry now ignores major updates for @mantine/*, react, react-dom, @types/react and @types/react-dom. Minor and patch updates inside Mantine v7 and React 18 still flow as before, and no other ecosystem entry changes. CLAUDE.md locked decision §2 fixes the frontend at React 18 + Mantine v7, but Dependabot had no way to know that, so the weekly grouped frontend PR proposed @mantine/* 7.15.2 -> 9.4.2 and react/react-dom 18.3.1 -> 19.2.8 alongside a dozen updates that were fine. One unmergeable entry makes the whole grouped PR unmergeable, and closing it discards the mergeable bumps with it — so the lock has to be expressed in the config or it gets re-litigated every week. @types/react* are included because their major tracks React's: @types/react 19 against react 18 fails the type-aware ESLint gate enabled 2026-07-24. See docs/ARCHIVE.md § Deviations for the dated entry.
… Node 24 and frontend-tooling work (#107) Rolls the frontend-builder base digest forward to the current node:22-bookworm-slim. Verified against the registry rather than taken from bump metadata: registry-1.docker.io resolves the 22-bookworm-slim tag to 6c74791e..., that index carries both linux/amd64 and linux/arm64, and Docker Hub's tag listing shows the same digest on 22.23.1-bookworm-slim and jod-bookworm-slim, which is what establishes the version behind the moving tag. No stage boundary, layer ordering, or cache scope changed. Same case as the debian refresh in #104: Dependabot's docker-images group offers node 22 -> 26, a major on a line that is not LTS, not a digest refresh within 22, so declining the major left the pin stale. Node 22 is supported through 2027-04-30; 24 (Active LTS, through 2028-04-30) is the target and 26 is not LTS until 2026-10-28. Also records three items that had been decided but written down nowhere a reader would find them, in docs/ROADMAP.md near-term: the Node 22 -> 24 move (Dockerfile + ci.yml + CONTRIBUTING together), the frontend tooling majors left over from Dependabot #86 after the Mantine/React ignores (TypeScript 5.7 -> 7.0, ESLint 9 -> 10, typescript-eslint 8.65, Vite 6 -> 8, Vitest 3 -> 4, jsdom 26 -> 29, all landing on the type-aware ESLint gate), and the deprecated Starlette HTTP_422_UNPROCESSABLE_ENTITY / HTTP_413_REQUEST_ENTITY_ TOO_LARGE constants at 22 and 2 call sites. See docs/ARCHIVE.md section 14 for the dated entry.
… NAS platforms (#108) Synology Container Manager and QNAP Container Station reject or mishandle `deploy:` keys, so docker-compose.yml would not deploy there at all, even though Compose v2 honours the block standalone. All three services carried a deploy.resources block (and a fourth copy sat in the README's paste-in stack). Memory limits stay on by default, re-expressed with the portable mem_limit / mem_reservation keys: scrye 2g + 256m reserved, trivy-server 1g, docker-socket-proxy 64m. Memory is the containment control that matters most — it bounds the OOM blast radius, and the RAM-backed /tmp tmpfs is charged against it. CPU limits move to a new opt-in overlay, docker/docker-compose.cpu-limits.yml, rather than being deleted; the caps are a hardening measure and the 0.5 cap on the socket proxy (the only container mounting docker.sock) is deliberate. The overlay uses the portable `cpus:` key, not `deploy:` — Compose rejects a merged project whose overlay contributes deploy.resources.limits against the base file's mem_limit ("can't set distinct values on 'mem_limit' and 'deploy.resources.limits.memory'"), so a deploy-based overlay would not apply. `cpus:` is the same limit, not a weaker one: Compose refuses a project that sets `cpus` and `deploy.resources.limits.cpus` to different values, which a parsed-and-ignored key could not do. Verified with `docker compose config` across base-alone and base+overlay, each with and without the sidecar profiles; the merged config reproduces the pre-change limits exactly. Adds five regression guards to test_compose_hardening.py covering both halves of the split. See docs/ARCHIVE.md § Deviations for changes made in this pass.
…g an existing one (#109) * feat(crypto): generate the master key on first launch, never replacing an existing one A new deployment could not start until the operator had produced a master key by hand. Because Compose validates a file-backed `secrets:` entry before it reads the rest of the stack, a missing `secrets/app_secret_key` failed `docker compose up` outright rather than producing a startup error anyone could act on — a first-run blocker rather than a misconfiguration. With no key supplied, Scrye now mints one from the OS CSPRNG (48 random bytes base64-encoded — the documented `openssl rand -base64 48` equivalent, so it clears the SEC-3 entropy floor without the weak-key opt-out), writes it `O_CREAT|O_EXCL` + fsync, chmods it 0600, re-stats it to verify mode and owner, and logs one INFO line naming the path and warning that the file must be backed up. Two new settings: SCRYE_APP_SECRET_KEY_AUTOGENERATE (default true) and SCRYE_APP_SECRET_KEY_AUTOGEN_FILE (default /data/app_secret_key). Precedence is preserved with generation strictly last: the Docker secret at SCRYE_APP_SECRET_KEY_FILE still wins, then a previously generated key file, then generation. No key is ever read from an env var or an image layer, and the KDF and token format are unchanged, so existing ciphertext keeps decrypting. The invariants matter more than the convenience, since a second key silently orphans every field-encrypted secret: - An existing key file is used, never replaced. Unreadable, empty, non-base64, too short or malformed fails startup; generation follows only from a proven absent file. Absence itself is proven — only ENOENT/ENOTDIR count, any other stat error refuses. - An explicitly configured SCRYE_APP_SECRET_KEY_FILE pointing at a missing file refuses to start rather than substituting a generated key. - A supplied secret alongside a previously generated key it does not cover (version-aware, since tokens name their key version) refuses to start. - Concurrent starts cannot both generate: the O_EXCL loser reads the winner's key, and a key file caught empty mid-write is retried (only that case, via a dedicated MasterKeyFileEmptyError) instead of failing. - A generated key that fails its permission check is removed by the call that created it, so the next start cannot adopt the file this one refused. Compose and the README stack no longer require the secret; the Docker-secret blocks are kept commented out for deployments that want the key and the data on separate mounts. Docs cover where the key lives, that it is generated on first run, that it must be backed up, what is lost if it isn't, and how this differs from a passphrase backup bundle (which does not need the key). See docs/ARCHIVE.md § Deviations (2026-07-29) for the full rationale, including the amended CLAUDE.md master-key sourcing rule. * fix(docker): preflight the data directory before migrations; make the ownership errors actionable An unwritable /data surfaced only as `sqlite3.OperationalError: unable to open database file` from `alembic upgrade head`, which names neither the path nor the cause. That is the same first-run-blocker class as a missing master key — and the one a bind-mounted NAS volume actually hits, since a bind mount keeps the host directory's ownership while a named volume inherits the image's. The entrypoint now checks that the database directory exists and is writable before Alembic runs, and fails with the directory, the container uid:gid, a literal `chown -R <uid>:<gid> <host path>`, the `user:`-matching alternative, and the note that a named volume gets this right automatically. A dozen lines of sh: a probe file created and removed, no validation framework. Tracing the same scenario through key generation showed the ordinary NAS bind mount is fine — on Linux a new file always belongs to the creating euid, so a foreign-owned but writable directory yields a 0600 key owned by the app uid. The owner check only fires where the filesystem synthesizes ownership (CIFS/SMB `uid=`, NFS squashing), where 0600 protects nothing. Both master-key messages in this class are now as actionable as the preflight: the unwritable-directory error carries the same uid/chown guidance, and the synthesized-ownership error states that chown cannot help and points at matching `user:` or a Docker secret. Tests execute the shipped entrypoint with sh against real directory permissions, stubbing alembic/uvicorn on PATH so "did the boot stop before migrations?" is observable, and rewriting only `cd /app/backend` (a path that exists solely in the image). Two cases skip under root, which bypasses permission bits; they run in CI, which is non-root. Message content is asserted for both key errors too. See docs/ARCHIVE.md § Deviations (2026-07-29) for the full trace table.
) * feat(settings): show which master key is in force on the About tab The auto-generated master key logs its back-this-up warning once, at generation. An operator who deploys and returns six months later sees nothing — and a per-boot warning would be worse, firing on every start of the documented default until people learn to skip startup logs. The About tab is where an admin actually looks, so the fact lives there. One row through the existing core/system_info.py -> api/settings.py path: master_key_info() reports the resolved source and path, AboutOut carries it, and AboutPanel renders "Auto-generated at <path> - back this up; a Docker secret gives stronger at-rest separation" or, for a supplied secret, "Supplied as a secret file at <path> - keep your copy backed up". Accurate for both sources rather than autogen wording shown unconditionally. Constraints: - No key material, and no key version either: source and path only, so this relocates an existing fact instead of disclosing a new one. - Admin-only on an endpoint that is not. GET /settings/about is readable by every role, so the row is populated per request for admins and omitted otherwise -- the path is deployment layout. Gated on effective_role, so an admin's role-capped API token sees what its role sees. - Never the reason a page fails: master_key_info() reads the cached startup resolution, so it does no filesystem work and cannot generate a key from a request; an unresolvable key omits the row rather than erroring the response. Tested through the API, matching how the other About rows are covered: both sources, no key material and no extra fields in the payload, and omission for a viewer, for a role-capped admin token, and when the resolution raises. See docs/ARCHIVE.md § Deviations (2026-07-29) for why this channel over a log line. * test(frontend): cover both master-key variants on the About tab The master-key row is the only conditional logic on this tab, and inverting it is not a cosmetic bug: it would tell a deployment that supplied a Docker secret -- the recommended posture -- that its key was auto-generated on the data volume, and the reverse. That is wrong security guidance shown to the operators who did the right thing, so each branch is pinned to text the other cannot produce. Covers: the auto-generated source renders the auto-generated wording with the at-rest separation advice and not the supplied-secret wording; the secret-file source renders the supplied-secret wording without the separation advice; both render their back-it-up guidance; and the row is absent entirely when the response omits the field (a non-admin caller, or no resolvable key). Verified against a deliberately inverted conditional before accepting it: all four source-dependent cases fail, and the omission case correctly still passes since it does not exercise the branch.
…g sign-in (#112) Session cookies are Secure by default, and a browser discards a Secure cookie set on an http:// page. Deploying over plain HTTP therefore produced repeated 401s with valid credentials, with nothing in the logs or the UI explaining why: the login returned 200, the cookie never landed, and every request after it was unauthenticated. The cookie posture is unchanged; the failure is now legible. - Startup logs whether HTTPS enforcement is on and, when it is, that logins over plain HTTP will fail unless the operator opts out — naming SCRYE_SESSION_COOKIE_SECURE=false explicitly. Enforcement off warns that cookies now travel in cleartext. Wildcard, unparseable, and empty SCRYE_FORWARDED_ALLOW_IPS values each get their own warning. - Every session-minting path (password login, first-admin setup, MFA verify, OIDC login start) refuses with 503 and a transport-specific message rather than issuing a session the browser will throw away. Setup is refused before the admin is created, so bootstrap stays re-runnable. - The refusal logs at ERROR and states whether the submitted credentials were valid, so a valid-credential rejection never reads as a bad-password 401, and records a distinct auth.login_blocked_insecure_transport audit action. The client-visible refusal is identical for valid, invalid, and unknown accounts. - The login and setup screens show a banner explaining this is an HTTPS configuration issue, not wrong credentials, driven by two transport-only fields on /auth/status so it never reflects credential state. - X-Forwarded-Proto is honored so a TLS-terminating proxy can report the client's real scheme, trusted only from the peers already configured in SCRYE_FORWARDED_ALLOW_IPS and only ever upgrading http -> https. Secure is never dropped from an auto-detected scheme, which would silently downgrade every deployment behind such a proxy. See docs/ARCHIVE.md § Deviations (2026-07-29) for the full rationale.
#113) * docs: scope the OIDC identity auto-retrieval (account-linking) feature Handoff document only — no code. Corrects the request's premise (Scrye has no manual subject field; the gap is linking an existing account), surveys per-provider subject semantics, specifies the reuse of the existing authorization-code machinery with a link purpose, enumerates the abuse cases and their prevention, details the L2/SEC-8 interaction and its compensating fresh-auth gate, and recommends building at minimal scope. Per the 2026-07-26 cleanup decision, the durable record goes to docs/ARCHIVE.md §14 and this file is deleted once the work lands. * docs: green-light the OIDC linking scope and cover IdP-side subject change Marks the scoping green-lit at the minimal scope (self-link + guarded self-unlink), cross-references the new tracking issue #114, and adds §7 on stale links: an IdP account recreated with a new sub, or an Authentik subject-mode change re-keying every user, leaves a link row that still reads as healthy while OIDC logins recreate the duplicate-account / dead-end failure this feature exists to remove. Recommends fail-closed detection at the login callback (distinct identity_stale error + audit event, never an auto-rebind), last_login_at surfaced on the linked status, and a re-link runbook — scoped into the §6 deliverables.
…the waiver blocks (#117) CVE-2026-11940 and CVE-2026-11972 (both HIGH, both tarfile) began tripping the dogfood gate on 2026-07-30 when they reached Grype's DB. CVE-2026-0864 is a non-gating Medium in the same module, waived alongside them for a clean report. Verified at the source per CLAUDE.md § Dependency hygiene, not from Grype's FIXED IN column — which reports 3.15.0b4 for all three and does not record the 3.14 backports. All three fixes are merged to the 3.14 maintenance branch and absent from released v3.14.6 (commit c63aec6, 2026-06-10), so all three close on 3.14.7: CVE-2026-11972 e86666c gh-151981/#151992 2026-06-23 (+13d) CVE-2026-11940 79c06bd gh-151558/GH-151559 2026-06-23 (+13d) 5e0ef3f gh-151987/#152609 2026-06-29 (+19d) CVE-2026-0864 2cf26d0 gh-151497/GH-151979 2026-06-24 (+14d) None is an ancestor of v3.14.6, confirmed with git merge-base --is-ancestor. No 3.15 move is warranted and none should be scoped off the back of this. Scrye does not use tarfile — the string appears nowhere in the repo and backup bundles are a JSON envelope — so the residual risk is unreachable stdlib code in the image, the same rationale the poplib acceptance rests on. Tracked in a new issue rather than appended to #98, because #98 carries its own dated verification scoped to its own trio. The blocks are now labelled A-1 (#98) / A-2 (#116) / B (#52) with an index at the top of the section, so the block-to-issue mapping is legible from the file — a single shared "tracked in #NN" header is what previously made two unrelated decisions look like one. See docs/ARCHIVE.md § Deviations for the dated entry.
…d declarations (#115) * chore(release): bump the app version to 0.2.0 and guard the duplicated declarations The version is declared independently in backend/app/__init__.py, backend/pyproject.toml and frontend/package.json, and nothing derives one from another or from the git tag — publish.yml computes the image tag from the pushed ref but never stamps a version into the image. Tagging v0.2.0 would therefore have published :0.2.0 running an app reporting 0.1.0 on the About tab, /healthz, the OpenAPI document, backup bundles and the scrye_build_info metric. Bump all three (plus the lockfile's root fields) and the four docs/Compose references to the example scrye:0.1.0 image tag and /healthz sample output. Add backend/tests/test_version.py asserting pyproject, package.json and package-lock.json agree with app.__version__, so a future release cannot half-land. The About endpoint test asserted only that version was truthy; it now asserts equality with __version__. AboutPanel gains a version-stat test bound to its own card, since the Scanners table also has a Version header. Historical 0.1.0 references in CHANGELOG.md and the backup-record fixtures are release history and stay as they are; converting [Unreleased] to [0.2.0] is a tag-time step per CONTRIBUTING.md § Releasing. See docs/ARCHIVE.md § Deviations for the single-sourcing recommendation. * docs(roadmap): fold image version-stamping into the single-sourcing item publish.yml computes the image tag from the pushed ref but never stamps a version into the image itself: there is no LABEL in docker/Dockerfile and no labels:/build-args: in publish.yml, dev-nightly.yml or ci.yml — none of them uses docker/metadata-action, which is what normally generates the OCI label set. So `docker inspect` on a published image reveals nothing about what is inside it. This is metadata hygiene rather than a defect: the running app reports its version correctly because app/__init__.py is baked in, and both /healthz and the About tab serve it. It belongs beside the single-sourcing work because both are the same problem — a version derived from nothing — seen from the inside and the outside. Records that the image is not wholly opaque today: publish.yml already attaches BuildKit SLSA provenance and an SPDX SBOM plus a GitHub-signed attestation, so the build is describable, just not through the one-command channel operators reach for. Docs only; no code or workflow change.
…#118) * ci(grype): print waived matches with --show-suppressed The gate printed a bare "No vulnerabilities found", which is ambiguous between "nothing matched" and "everything that matched was waived" — Grype's ignore rules drop matches from the report before it prints, so a clean-looking line is exactly what a seven-entry waiver list also produces. Confirming a waiver had taken effect meant diffing against a run from before it landed rather than reading the log in front of you. With the flag the waived matches print as (suppressed) rows next to the verdict, so the log says which CVEs were let through and why the run is green. Same legibility principle as the A-1/A-2/B block-to-issue index. Display-only: Grype evaluates --fail-on against non-ignored matches, so listing the ignored set cannot change pass/fail. Three of the seven waivers are HIGH, so if --show-suppressed did fold them back into --fail-on high, this change's own run would fail rather than pass. Touches only the gate step. ci/grype.yaml's entries, ci/trivyignore and both informational scans are unchanged. See docs/ARCHIVE.md § Deviations for the dated entry. * ci(grype): keep the gate's verdict and list its waivers from one scan Replaces the --show-suppressed approach, which was tried and rejected on the evidence from its own CI run. --only-fixed is itself implemented as an ignore rule, so its matches share the ignoredMatches bucket with the by-ID waivers. --show-suppressed therefore printed every won't-fix package too — ~250 rows against the 7 wanted — and, having rows to print, Grype emitted the table instead of the "No vulnerabilities found" line. That removed the verdict and buried the waivers: a worse legibility problem than the one being fixed. Instead the gate now takes two outputs from a single invocation (-o table -o json=), keeping its clean verdict on stdout and writing the machine-readable report. A new non-gating step jq-filters ignoredMatches for entries whose appliedIgnoreRules carry a `vulnerability` key, which selects the deliberate ci/grype.yaml waivers and drops both the --only-fixed set and the location-based bundled-binary rules. It is labelled as exceptions rather than findings and runs if: always(), so the list still prints when the gate fails. One invocation, not two: a second grype run would repeat the vuln-DB load and the full image scan, and this job is already the slowest in CI. The gate's flags are unchanged and the new step is non-gating, so pass/fail behaviour is identical. See docs/ARCHIVE.md § Deviations for the dated entry. * docs(roadmap): track the test_cancel_queued_scan timing flake test_cancel_queued_scan is timing-dependent by construction: a monkeypatched scanner sleeps 0.2s to hold the only worker slot while a second scan must still be `queued` when the cancel POST lands. On a loaded runner that window closes, the scan has left `queued`, and the endpoint correctly returns 409 — queued-only cancellation is a deliberate decision (ARCHIVE §14, 2026-07-03). The test is wrong, not the code. Records that the fix is to make the worker's slot acquisition observable to the test, not to widen the sleep, which would only lengthen the odds while leaving the race in place and slowing every run. Also records why it is worth fixing rather than tolerating: a test that reddens CI intermittently trains people to re-run without reading, which is how a real failure gets waved through. Docs only; the test itself is unchanged here.
Bumps the npm_and_yarn group with 1 update in the /frontend directory: [brace-expansion](https://github.com/juliangruber/brace-expansion). Updates `brace-expansion` from 1.1.15 to 1.1.16 - [Release notes](https://github.com/juliangruber/brace-expansion/releases) - [Commits](juliangruber/brace-expansion@v1.1.15...v1.1.16) --- updated-dependencies: - dependency-name: brace-expansion dependency-version: 1.1.16 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…bump, Dependabot security-PR routing (#121) Cuts CHANGELOG `[Unreleased]` to `[0.2.0] - 2026-07-31` with a fresh empty `[Unreleased]` above it and a `[0.2.0]` compare link, and adds a `### Security` entry covering the base-image and dependency currency that answers issue #75. Bumps brace-expansion 1.1.16 -> 1.1.18 and the nested @typescript-eslint/typescript-estree copy 2.1.3 -> 2.1.4, regenerated with npm rather than hand-edited. Dependabot's #120 proposed this but opened against main, and retargeting left its diff stale, so it was closed in favour of doing this on dev. Documents in CLAUDE.md and CONTRIBUTING.md that Dependabot security updates always open against the default branch, ignoring target-branch, and that retargeting such a PR does not work. See docs/ARCHIVE.md § Deviations (2026-07-31) for the full pass.
Reconciles #110 (brace-expansion 1.1.15 -> 1.1.16), which was merged directly into `main` as a Dependabot security update, ahead of the v0.2.0 promotion. `main` was not an ancestor of `dev` — #119 replicated #110's change on `dev` rather than merging `main` — so both branches had independently edited frontend/package-lock.json and the promotion PR would not merge. frontend/package-lock.json resolved in favour of `dev`. Verified this cannot resolve away from the security fix: no package is present on `main` and absent from `dev`, and the only version differences are the app version (0.1.0 -> 0.2.0) and brace-expansion 1.1.16 -> 1.1.18 / 2.1.3 -> 2.1.4, both of which move forward past what `main` carries. # Conflicts: # frontend/package-lock.json
The [0.2.0] section carried only what had been written into [Unreleased] since roughly 2026-07-24. Everything promoted in #70 (2026-07-13, i.e. #53-#67) and the #77-#88 batch that followed had never been changelogged at all — v0.1.0 was tagged 2026-07-09 and #70 landed four days later — so about two dozen PRs of security and correctness work that ships in 0.2.0 was absent. Backfilled from the #70 commit range and the §14 entries for that batch, merged into the existing Added/Fixed/Changed/Security sections rather than added as a separate block: 0.2.0 is one release, and a changelog-within-a-changelog would make a reader track which half applies to them. Three of the release's upgrade-affecting items live here and were invisible before: the SSRF egress guard (SCRYE_ALLOW_INTERNAL_EGRESS, default off), the remote-clone-URL requirement for repository targets, and the master-key entropy floor — which refuses to start a v0.1.0 deployment whose key file holds a raw passphrase, and whose remedy is the boot-and-rotate escape hatch plus a backup/restore cycle, not a fresh key. Also records three contract-visible API changes narrower than the envelope: timestamps serialize with an explicit Z, /api/audit renamed entries -> items, and scan list rows dropped options/error in favour of has_error.
The image-tag table and the `docker pull` block both used `:1.4.0` as the "pin a release" example, for a project whose only release is v0.1.0. Corrected to `:0.2.0`. It sits in the Quick start path, on the page a reader lands on the day a release goes out.
Squashing a promotion replaces dev's commits with one new commit on main that has no ancestry link to them, so main and dev diverge the instant it lands and every subsequent back-merge becomes a conflict-resolution exercise against a squashed copy of work dev already has. That is not theoretical here. Because main was not an ancestor of dev, #110 (a Dependabot security update, which lands on main) could not be back-merged cheaply, so #119 replicated its change on dev instead of merging main. Both branches had then independently edited frontend/package-lock.json and the v0.2.0 promotion PR opened dirty — requiring a hand-picked side on a file where picking wrong silently reverts a security fix. A merge commit removes the cause: main keeps the individual commits a release should preserve, dev stays an ancestor of main, and the back-merge becomes a fast-forward. Feature and contribution PRs into dev are unchanged and still squash-merged; the rule is asymmetric on purpose. Corrects the three passages that assumed squashing: the back-merge guidance in both documents (a promotion's back-merge now fast-forwards; the resolve-in- favour-of-dev rule stays for commits that landed on main independently, with a new check that dev is genuinely ahead first), and the git-identity note, which described squash-merge authorship but applies equally to a merge commit. Also logs the release-prep follow-ups in §14: the CHANGELOG backfill, the README tag-example fix, and tracking issues for three dependency findings — #123 (react-router GHSA-qwww-vcr4-c8h2, runtime dep but not reachable outside RSC mode), #124 (postcss, dev-only, patch-level fix available) and #125 (brace-expansion GHSA-mh99-v99m-4gvg, no fix on the 1.x or 2.x lines, so the 1.1.18 bump clears nothing).
tyler-rich
added a commit
that referenced
this pull request
Aug 2, 2026
* backend: bump fastapi 0.140.0 -> 0.140.13 and regenerate requirements.lock Reapplies the bump Dependabot proposed in #127, which could not merge as opened: Dependabot edits pyproject.toml without touching backend/requirements.lock, so CI's drift gate rejects it. The lock is regenerated with the pinned command from CONTRIBUTING.md § Backend dependency lock (uv 0.8.17, --group build --generate-hashes --python-version 3.14) and moves three lines: fastapi's version and its two wheel hashes. starlette stays at the explicitly-pinned 1.3.1. All thirteen patch releases are internal dependency-tree/OpenAPI refactors (0.140.1-0.140.7) or fixes to paths this backend does not use - SSE/JSONL streaming, response_model_* on Iterable returns, jsonable_encoder's exclude_defaults, and nested-Annotated sequence params. Verified by grep: no jsonable_encoder, no StreamingResponse, no SSE helper, no response_model_* anywhere in backend/app. Confirmed against the regenerated lock rather than the version pin: a venv on CPython 3.14.6 built the way the image builds (pip install --require-hashes -r requirements.lock, then pip install --no-deps --no-build-isolation .) runs the full suite green - 666 passed, 5 skipped, identical to the pre-bump baseline. * ci: bump docker/login-action 4.5.1 -> 4.5.2 Reapplies what Dependabot proposed in #128, SHA-pinned per the repo's Actions convention with the tag as a trailing comment. The commit SHA was resolved from upstream with git ls-remote --tags against docker/login-action rather than read off the bump description - refs/tags/v4.5.2 maps to 371161bbe7024a29a25c5e19bfcbc0804fe9ad2c. Trusting a rendered SHA is the substitution a SHA pin exists to prevent. v4.5.2 contains one substantive commit, "surface Docker Hub OIDC error responses" (docker/login-action#1058): it improves the error text when a Docker Hub OIDC login fails. All three call sites here log in to ghcr.io with the built-in GITHUB_TOKEN as username/password - not Docker Hub, not OIDC - so the changed path is never entered. No input, output, or breaking changes. This matters because these workflows run only on the tag-gated publish path and the nightly, which CI cannot exercise. * ci(dependabot): ignore node majors and the local scrye build tag Two ignore entries, both closing a recurring failure. node majors, on the docker entry. #126 proposed node 22-bookworm-slim -> 25-bookworm-slim. Node's odd-numbered lines never become LTS: v25 reached end-of-life on 2026-06-01, before that PR was opened and years before the 22 line's 2027-04-30, so it would have moved the frontend builder onto an unsupported runtime. The wanted move is 22 -> 24 (Active LTS, 2028-04-30) and is tracked in docs/ROADMAP.md because it spans the Dockerfile, ci.yml and CONTRIBUTING.md together. The ignore is scoped to version-update:semver-major so digest refreshes of the pinned 22 tag still come through - declining a major previously left Dependabot offering nothing for this image and the digest went stale until #107 refreshed it by hand. dependency-name: scrye, on both docker entries. docker/docker-compose.yml pins image: scrye:0.2.0, Scrye's own locally-built tag; Dependabot resolves the unqualified name as Docker Hub's library/scrye, gets a 401, and fails the run with private_source_authentication_failure. Reading the files confirms the compose file carries the only such reference and docker/Dockerfile carries none, so the substantive entry is on the docker-compose ecosystem; it is repeated on the docker entry because the failing run's ecosystem is visible only in the Dependabot UI, and suppressing a name the Dockerfile never mentions costs nothing. The image tag itself is unchanged: qualifying it as ghcr.io/tyler-rich/scrye:0.2.0 would make the compose file pull a published image instead of building locally, changing what the documented quick start does. * docs: record the post-v0.2.0 dependency cleanup and the base-branch diagnosis A version update sitting on main is not the documented security-update case. #126, #127 and #128 were all opened against dev correctly - their head branches carry the /dev/ target-branch segment - and each records an automatic_base_change_succeeded event 2-3 seconds after the v0.2.0 promotion (#122) merged. GitHub retargets open PRs whose base branch is deleted to the merged PR's base, and auto-delete-on-merge deleted dev as the promotion's head branch. dependabot.yml was not involved, and neither was grouped security updates: all three are version updates across three different ecosystems. The remedy is already in place (auto-delete disabled on 2026-08-02); target-branch and the grouping setting stay as they are. CLAUDE.md § Dependency hygiene and a new CONTRIBUTING.md subsection carry the rule and a two-signal table for telling the two causes apart. docs/ROADMAP.md: the Node 22 -> 24 item rewritten to state that it must be its own PR with a registry-verified digest pin, and to name the three files that have to move together - bumping the Dockerfile alone would leave CI on 22 while the image builds on 24. New item for enabling GitHub code scanning (CodeQL) on Python and TypeScript: free for public repos, default setup adds a workflow running on every push and PR, and the first run surfaces a triage backlog, so it wants its own session. docs/ARCHIVE.md gains the dated §14 entry covering all of it, including the correction to #125: GHSA-mh99-v99m-4gvg was re-scoped per major on 2026-07-31 and 1.1.18 / 2.1.4 are both above their line's first patched version, so the #121 bump did clear it. Verified at the source - 1.1.17 and 2.1.3 introduce EXPANSION_MAX_LENGTH and name CVE-2026-14257 in their comments - not from the advisory metadata that produced the wrong claim in the first place. #125 closed on its own stated criterion; #123 and #124 re-verified and still accurate. * docs: distinguish base_ref_changed from automatic_base_change_succeeded The retarget diagnosis said #110 and #120 have "no such event", which is true of automatic_base_change_succeeded but reads as though their timelines are bare. #120 carries two ordinary base_ref_changed events - the manual retarget-to-dev-and-back already documented on 2026-07-31 - so a reader checking a timeline needs the event types told apart, not just the presence of a base change. * docs: state the evidence for the node digest-refresh assumption The ignore is scoped to version-update:semver-major so digest refreshes of the pinned 22 tag keep arriving, but that rests on documented update-types semantics plus reported updater behaviour - not on an observed run in this repository. Says so, names the first scheduled docker run as the test, and records the fallback if it is wrong (refresh the digest by hand, as #107 did). * ci: pin docker/login-action to v4.6.0 rather than v4.5.2 Supersedes the 4.5.2 applied earlier in this branch. 4.5.2's only substantive commit improves Docker Hub OIDC error text, a path none of the three call sites can reach - all three log in to ghcr.io with the built-in GITHUB_TOKEN as username/password. 4.6.0 lands a day later and its change is at least adjacent to what this repo does: it hardens the buildx-scoped config path used by the login -> buildx -> push chain, and carries the action's own bundled dependency bumps. SHA resolved from upstream with git ls-remote --tags, not from a changelog: refs/tags/v4.6.0 -> dbcb813823bdd20940b903addbd779551569679f. The moving v4 tag currently points at the same commit; the pin is the immutable v4.6.0 commit, not the alias. Read at the source by diffing v4.5.2...v4.6.0 rather than from the release notes, because these workflows run only on the tag-gated publish path and the nightly, which CI cannot exercise. action.yml is byte-identical, src/main.ts and src/docker.ts are unchanged, and the whole change is in src/context.ts's buildx scoped-config-dir helper - path.resolve containment on the registry and scope inputs behind a new isChildPath() helper. Behaviourally a no-op here: that helper returns early on 'if (scopeDisabled() || !scope || scope === "")', and no call site passes a scope input. Currency plus defence-in-depth, not a fix for anything reachable. * frontend: bump postcss 8.5.16 -> 8.5.25 (GHSA-r28c-9q8g-f849) Applied by hand rather than waiting for Dependabot: it has not proposed this, and after closing #126/#127/#128 there are no open Dependabot PRs at all, so a HIGH advisory was waiting on a bot that was not going to act. Which version actually clears it was verified in the published source, not taken from the advisory range - GHSA-mh99-v99m-4gvg was re-scoped mid-flight and #125 was wrong because of it. postcss 8.5.18's lib/previous-map.js loadFile() gains the containment check the advisory describes (relative(dirname(cssFile), path) rejected when it is '..', starts with '../', or is absolute); 8.5.17 has none of it. So 8.5.18 is the real floor. The same check is still present in 8.5.25, which is what is pinned - current release on the pinned 8.5 line, per CLAUDE.md § Dependency hygiene. No overrides entry needed and no parent bumped: postcss is a direct devDependency here, and every package that also reaches it declares a peer/caret range 8.5.25 satisfies (vite's ^8.5.3 included), so raising the single pin lifts the tree and npm ls shows one deduped copy. Regenerated with npm pkg set + npm install --package-lock-only, not by editing version strings, so resolved URLs and integrity hashes moved with the version. Diff is postcss 8.5.16 -> 8.5.25, its nanoid floor ^3.3.12 -> ^3.3.16, and nanoid 3.3.15 -> 3.3.16 - all dev-only, no new packages. npm ci installs 8.5.25 with the containment check present; ESLint, Prettier, the 20-file/69-test Vitest suite and npm run build all pass; npm audit no longer reports postcss. Nothing ships either way - postcss runs during vite build and the image copies only dist/. * docs: record the 4.6.0 pin and the postcss bump in the dated entry The §14 entry's login-action item now covers 4.6.0 and why it was taken over the 4.5.2 that #128 proposed, including the source-level diff and the honest note that the hardened path is gated on a scope input this repo never passes. The advisory-issues item gains the full postcss treatment: why it was applied by hand, the 8.5.18 fix floor verified in the published source rather than read off the advisory range, why no overrides entry or parent bump was needed, and the post-bump verification. #124 closed on its own stated criterion, the same standard applied to #125. CHANGELOG [Unreleased] gains a Security entry for the postcss advisory - and says explicitly that unlike the brace-expansion bump in 0.2.0, this one does clear its advisory - plus Changed entries for the login-action and fastapi pins.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes
devtomainfor the v0.2.0 release.mainhas had nothing since #70 (2026-07-13) apart from the #110 brace-expansion security merge.Merge with a regular merge commit — "Create a merge commit", not "Squash and merge". This is now the documented convention for promotions (
CONTRIBUTING.md§ Releasing step 1, changed in this PR — see Process changes below).Do not merge until reviewed — tagging is a separate maintainer step after this lands.
What this release contains
Runtime and platform
python:3.14-slim-bookworm), with the dependency bumps it required —pydantic2.13.4,uvicorn0.51.0,sqlalchemy2.0.51, newgreenlet3.5.4 pin (build(python): move the backend runtime from Python 3.13 to 3.14.6 #91). No interpreter CVE is cleared by the move; the CHANGELOG says so explicitly.tecnativa→wollomatic/socket-proxy, allowlist narrowed to the single endpoint Scrye calls,GET /images/json(feat(docker): migrate socket proxy to wollomatic, pin allowlist to GET /images/json #89, closes Evaluate migrating the Docker socket proxy from tecnativa to wollomatic/socket-proxy #63).API
{total, items}envelope (feat(api): standardize list responses on a shared {total, items} envelope #90). Breaking for API-token consumers — the reason this is a minor, not a patch.GET /api/scansdeprecated in favour ofGET /api/scans/history; its response shape is unchanged.Z,GET /api/auditrenamed its envelope keyentries→items, and scan list rows carryhas_errorinstead ofoptions/error.Onboarding fixes from user reports
deploy:keys retired so the stack deploys on Synology/QNAP; memory limits kept, CPU limits moved to an opt-in overlay (fix(docker): drop Swarm-oriented deploy: keys so the stack deploys on NAS platforms #108).X-Forwarded-Protois honoured from configured proxies only.Security and supply chain
:latesthas been the v0.1.0 image from 2026-07-09, and [image-rescan] Fixable HIGH/CRITICAL CVEs in ghcr.io/tyler-rich/scrye:latest #75 has been reporting fixable HIGH/CRITICAL CVEs against it since 2026-07-20. This release is the rebuild that answers it — see the### Securityentry in the CHANGELOG for what moved and what is deliberately still waived.requirements.lock, SHA-pinned Actions, SLSA provenance + SBOM attestation, cosign verification of the scanner binaries, and the weekly published-image re-scan.The CHANGELOG was missing half of this
[0.2.0]originally carried only what had been written into[Unreleased]since roughly 2026-07-24. Everything promoted in #70 and the #77–#88 batch after it had never been changelogged at all — v0.1.0 was tagged 2026-07-09 and #70 landed four days later — so about two dozen PRs of security and correctness work that ships in this release was absent.Backfilled from the #70 commit range and the §14 entries for that batch, merged into the existing
Added/Fixed/Changed/Securitysections rather than added as a separate block. Three of the release's six upgrade-affecting items live in that backfill and were invisible before it: the SSRF egress guard (SCRYE_ALLOW_INTERNAL_EGRESS, default off), the remote-clone-URL requirement for repository targets, and the master-key entropy floor — which will refuse to start a v0.1.0 deployment whose key file holds a raw passphrase.Process changes in this PR
CONTRIBUTING.md§ Releasing step 1,CLAUDE.md§ Git & PR conventions). Feature PRs intodevare unchanged and still squash-merged. The three passages that assumed squashing — both back-merge descriptions and the git-identity note — were corrected in the same pass. The argument is the divergence this PR had to reconcile; see below.main, notdev, ignoringtarget-branch, and retargeting such a PR does not work. Documented in both files after ci: bump brace-expansion from 1.1.16 to 1.1.18 in /frontend in the npm_and_yarn group across 1 directory #120.A conflict had to be reconciled before this PR could merge
mainwas not an ancestor ofdev: #110 was merged directly intomain, and #119 replicated its change ondevas a separate commit rather than mergingmain. Both branches had therefore independently editedfrontend/package-lock.json, and this PR opened asdirty.Resolved by merging
origin/mainintodev(commit05219b7), takingdev's side of the lockfile. Verified this cannot resolve away from #110's security fix: no package is present onmainand absent fromdev, and the only version differences are the app version (0.1.0 → 0.2.0) and brace-expansion 1.1.16 → 1.1.18 / 2.1.3 → 2.1.4 — both moving forward past whatmaincarries. The net diff of the resolved merge againstdev's pre-merge tip is empty, which is the checkCLAUDE.md§ Git & PR conventions requires.This is exactly what the merge-commit change above prevents recurring.
Why this also fixes CI and the scheduled workflows
main's push CI is currently red at086fb1e: its image job still builds on Python 3.13.14 and trips the Grype gate on CVE-2026-11940 (tarfile) and CVE-2026-15308 (html.parser), both HIGH.devwaived both in #117 with dated source-verification and moved to 3.14.6 in #91, so the promotion is what returnsmainto green — the red is staleness, not a regression this PR introduces.Scheduled and tag-triggered workflows run from the default branch's copy of themselves, so
dev-nightly.ymlandrescan.ymlhave been executing versions that predate every workflow change ondev.ci.ymlandpublish.ymlare likewise stale onmain. The promotion lands all four.Before-you-tag checklist
Worked per
CONTRIBUTING.md§ Releasing:CHANGELOG.mdreviewed in full and re-verified claim by claim against the tree — the false-CVE correction is present and accurate; one sentence amended where "all four stay waived" had become misleading as a total.[Unreleased]cut to[0.2.0] - 2026-07-31with a fresh empty[Unreleased]above it and a[0.2.0]compare link, somainreceives a correct CHANGELOG rather than a commit landing on it after the tag.THIRD_PARTY_LICENSES/re-verified — version table matches the Dockerfile args, and all four bundled files re-fetched from upstream at those tags and compared byte-for-byte.devin chore(release): v0.2.0 release prep — CHANGELOG cut, brace-expansion bump, Dependabot security-PR routing #121.backend/requirements.lockneeds no regeneration —pyproject.tomldependencies are unchanged since the last compile, and CI's drift gate is green.0.2.0everywhere it is declared or displayed;backend/tests/test_version.pybinds them and passes. The README's "pin a release" examples no longer advertise a:1.4.0that does not exist.Open dependency findings, none release-blocking
Given live tracking issues so they do not go invisible between
npm auditruns:react-router7.18.1. The only one on a runtime dependency, and not reachable: the advisory is specific to RSC mode and Scrye is a declarative SPA. The fix is a major;npm audit fix --forcewould "fix" it by downgrading below the vulnerable range.postcss8.5.16, dev-only. Fix is a patch-level bump inside the pinned major.brace-expansion. Covers<=5.0.7, so the 1.1.18 / 2.1.4 bump in chore(release): v0.2.0 release prep — CHANGELOG cut, brace-expansion bump, Dependabot security-PR routing #121 is currency and clears nothing; no fix exists on the 1.x or 2.x lines. Both copies are devDependencies under the ESLint tree.See
docs/ARCHIVE.md§ Deviations for the two dated entries covering this cycle.