fix(scans): require repository targets to be remote clone URLs (SEC-1) - #53
Merged
Merged
Conversation
A `target_type=repository` scan was validated only for length and a leading `-`, then passed straight to `trivy repo -- <target>`. Trivy's `repo` subcommand accepts a local filesystem path, so an operator could submit a target like `/data` or `/run/secrets` and have Trivy walk the container filesystem, persisting the results as a downloadable artifact — bypassing the `SCRYE_FILESYSTEM_SCAN_ROOTS` allowlist that exists precisely to keep the SQLite DB and master-key file unreadable as scan output (SEC-1 / Top 5 #1). Restore the invariant that a repository target must be a remote git clone URL: add `is_remote_repo_url` (reusing `is_http_url` plus an ssh/git scheme allowance) and enforce it with a `ScanCreateIn` model validator, which rejects local paths at request time (422). Because `ScanScheduleIn` subclasses `ScanCreateIn`, the guard covers scheduled scans too. Add regression tests: `/data`, `/run/secrets`, `/`, `/app`, and `file://` targets are rejected, a valid remote clone URL still runs, and direct coverage of the `is_remote_repo_url` helper.
tyler-rich
added a commit
that referenced
this pull request
Jul 20, 2026
* 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.
* Promote dev to main: Docker build performance, Actions bumps, back-merge 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.
* docs,ci: documentation overhaul + go public on GHCR-only distribution (#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.
* feat(ui): deeper teal theme, scan deletion, and nav active-state fix (#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.
* chore: migrate GitHub username IamGroot60 → tyler-rich (#40)
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
([email protected]; 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.
* docs: add CHANGELOG with 0.1.0 entry (#42)
* docs: record v0.1.0 bundled-binary CVE check (no upstream fix available) (#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).
* docs: wire up README screenshots (#44)
* 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.
* docs: fix inconsistent README screenshot sizing (#45)
Cap each screenshot to a fixed width via HTML img tags so the table
renders uniformly regardless of each capture's native dimensions.
* docs: make README screenshot thumbnails uniform (#46)
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.
* docs: pad README screenshots to a shared aspect ratio (#47)
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.
* docs: crop README screenshots to a genuinely fixed height (#48)
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.
* fix: resolve CI dogfood self-scan failures — curl CVE-2026-5773 + CPython 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
* docs: consolidate six /code-review reports and add severity-ranked summary (#50)
* docs: gather six /code-review reports onto feat/reviews
Consolidates reports from api-db-models-review, claude-md-compliance-audit,
mantine-frontend-review, scrye-concurrency-review, scrye-security-review,
and supply-chain-security-review branches.
* docs: add consolidated severity-ranked summary of the six review reports
* fix(scans): require repository targets to be remote clone URLs (#53)
A `target_type=repository` scan was validated only for length and a leading
`-`, then passed straight to `trivy repo -- <target>`. Trivy's `repo`
subcommand accepts a local filesystem path, so an operator could submit a
target like `/data` or `/run/secrets` and have Trivy walk the container
filesystem, persisting the results as a downloadable artifact — bypassing the
`SCRYE_FILESYSTEM_SCAN_ROOTS` allowlist that exists precisely to keep the
SQLite DB and master-key file unreadable as scan output (SEC-1 / Top 5 #1).
Restore the invariant that a repository target must be a remote git clone URL:
add `is_remote_repo_url` (reusing `is_http_url` plus an ssh/git scheme
allowance) and enforce it with a `ScanCreateIn` model validator, which rejects
local paths at request time (422). Because `ScanScheduleIn` subclasses
`ScanCreateIn`, the guard covers scheduled scans too.
Add regression tests: `/data`, `/run/secrets`, `/`, `/app`, and `file://`
targets are rejected, a valid remote clone URL still runs, and direct
coverage of the `is_remote_repo_url` helper.
* fix(worker,backup): retry contended commits, add stale-scan watchdog, make restore guard atomic, clamp bundle scrypt params (#54)
Remediates the compounding CON-1/CON-11/CON-3/SEC-2 findings from the
2026-07-12 review batch (docs/reviews/00-summary.md Top 5 #2):
- Worker DB commits (claim, persist, fail, raw-output artifact) now retry
SQLite lock-contention OperationalErrors with bounded exponential backoff,
and a successful scan's artifact files are only unlinked after the final
attempt fails - transient contention no longer loses results or strands
scans running forever (CON-1).
- A stale-scan watchdog in the maintenance tick re-submits queued scans with
no live task and fails task-less running scans via an atomic conditional
update, so shutdown races and broken commit chains self-heal within a tick
instead of at the next restart (CON-11).
- restore_bundle takes BEGIN IMMEDIATE and re-checks the active-scan guard
inside the write transaction (RestoreConflictError -> 409), closing the
check-then-act window across the upload await; the restore endpoint pauses
the worker for the duration (CON-3).
- Restore-supplied scrypt cost parameters are clamped and maxmem is a fixed
budget rather than derived from the bundle's own untrusted n/r, so a
crafted bundle can no longer OOM the container pre-passphrase (SEC-2).
The ScanWorker seam gains optional default-no-op reconcile_stale/pause/resume
hooks; the core submit/recover/shutdown interface is unchanged.
See docs/ARCHIVE.md § Deviations (2026-07-13) for the full record.
* fix(security): contain scanner-URL XSS sink and add security-header baseline (#55)
Close the audit's account-takeover chain (FE-9 + missing response-header
baseline) with two coupled changes.
Frontend:
- Add safeHttpUrl() (frontend/src/lib/url.ts): admit only well-formed
http(s) URLs, rejecting javascript:/data:/vbscript:/relative/malformed.
- ScanDetailPage renders a finding's scanner-derived primary_url as an
Anchor only when it passes safeHttpUrl (inert text otherwise) and adds
rel="noopener noreferrer". This was the only place scanner-derived data
was rendered as a link.
- Add vitest as the frontend unit-test runner (npm test) with a
Node-environment test config; unit-test safeHttpUrl; wire npm test into CI.
Backend:
- Add SecurityHeadersMiddleware: every response gets X-Frame-Options: DENY,
X-Content-Type-Options: nosniff, Referrer-Policy, and a Mantine-SPA-tuned
Content-Security-Policy (script-src 'self'; style-src 'self'
'unsafe-inline' for Mantine's runtime style injection; connect-src 'self';
object-src/frame-ancestors locked down). The /docs and /redoc UIs are
exempt from the CSP only (they need inline scripts + CDN assets). The
CSRF cookie's double-submit design is unchanged.
- Test asserts the headers on responses and the docs-CSP exemption.
Verified the built SPA renders under the new CSP with zero policy violations.
Docs: README security model, CONTRIBUTING testing, ARCHIVE deviation entry.
* fix: kill scanner subprocess groups, not just the direct child (CON-2/CON-14) (#56)
proc.kill() only signals the direct child; git clone spawns
git-remote-https and scanner binaries can spawn their own helpers, so on
timeout, output-cap overflow, or shutdown cancellation those grandchildren
kept running with SCRYE_GIT_PASSWORD/GIT_ASKPASS still in their environment.
run_command now starts children with start_new_session=True and kills the
whole process group via a new _kill_process_group helper on all three abort
paths, suppressing ProcessLookupError so an already-exited group can't
replace the abort's own error (CON-14).
See docs/ARCHIVE.md § Deviations for details.
* ci: SHA-pin workflow actions, expand Dependabot coverage, harden checkouts (#57)
Resolve SC-2 (HIGH): pin every `uses:` in .github/workflows/*.yml and the
composite build-image action to a full commit SHA with the version kept as a
trailing comment, so a compromised action can no longer move a tag under
publish.yml/dev-nightly.yml's packages:write token. While pinning, also align
the composite action's action majors with ci.yml (setup-qemu-action and
setup-buildx-action v3 -> v4.2.0, build-push-action v6 -> v7.3.0) so build
behavior and audit surface match across all three workflows (L25/D3).
Resolve SC-3 (HIGH): extend dependabot.yml beyond github-actions with pip
(backend), npm (frontend), docker and docker-compose (base/sidecar images)
ecosystems, all targeting dev on a weekly grouped schedule. Also split the
composite action into its own github-actions directory entry, since
Dependabot's github-actions ecosystem does not recurse into composite actions
outside .github/workflows/ (D3).
Resolve L24/SC-11 (LOW): set persist-credentials: false on the checkout steps
in publish.yml and dev-nightly.yml, the two token-bearing (packages: write)
workflows, since no later step in either needs a credentialed git remote.
* backend: bump pytest 9.0.3, pytest-asyncio 1.4.0, black 26.3.1 (#59)
Dependabot's grouped pip bump (PR #58) raised pytest to 9.0.3 but left
pytest-asyncio at 0.25.1, which pins pytest<9, producing an unsatisfiable
resolution. Bump pytest-asyncio to 1.4.0 (allows pytest<10) alongside the
pytest and black bumps so the dev toolchain resolves.
Verified on Python 3.13: full suite 476 passed / 3 skipped, black --check
clean, ruff clean.
* fix(concurrency): remediate CON-5–CON-20 async-path, shutdown, and pool findings (#60)
* fix(worker,api): offload synchronous DB work off the event loop (CON-5)
The maintenance tick's scanner-DB policy read, the worker's per-scan
Trivy/Grype policy loads, and the scan-queue insert/audit/commit ran
synchronously on the event loop. A concurrent long writer holding the
SQLite write lock would stall the whole loop inside busy_timeout (up to
5s), freezing every request, healthcheck, and subprocess pump. Hop each
off the loop via a worker thread, matching the pattern already used for
result persistence and restore.
* fix(worker,compose): fit graceful shutdown within the container stop budget (CON-6)
The worker's 10s drain grace alone equalled Docker's default 10s
SIGTERM->SIGKILL budget, so a busy instance was SIGKILLed mid-commit
before the cancel/gather and scheduler shutdowns could run. Add an
explicit stop_grace_period (30s) to the Compose file, shrink the worker
drain grace to 5s, and bound each scheduler's shutdown wait with
asyncio.wait so a task wedged in a non-cancellable threaded pass is
abandoned after a timeout instead of blocking the stop path.
* fix(app): shield and fault-isolate the lifespan shutdown sequence (CON-7)
The lifespan finally ran maintenance/backup/worker shutdown as three
sequential, unshielded awaits: a second cancellation (uvicorn forced
exit / second SIGINT) mid-sequence aborted it before worker.shutdown(),
so live scanner subprocesses were never cancelled and ran until SIGKILL;
an exception from one shutdown likewise skipped the rest. Wrap the
teardown in asyncio.shield and run each component's shutdown under its
own try/except so the worker is always stopped last.
* fix(auth): make PendingMfaStore thread-safe (CON-8)
The store is shared across the sync login/verify_mfa endpoints, which
run on different threadpool threads, but _prune iterated _pending with
no lock — a concurrent issue() inserting mid-iteration raises
'dictionary changed size during iteration' and 500s a valid login. Guard
issue/consume/prune with a threading.Lock, mirroring the rate limiter.
* fix(backup): consistent-snapshot dump + defer scheduled backup while scans run (CON-9)
build_bundle read each table in its own implicit transaction, so a scan
committing between the scans and findings reads produced a torn bundle.
Wrap the whole dump in one explicit BEGIN so it reads a single WAL
snapshot. Additionally, run_due_backup now skips (and logs) while any
scan is queued or running, mirroring the manual restore guard, so an
in-flight scan can't be captured mid-running and restored as stuck;
last_run_at is left unset so the backup retries once the scan finishes.
* fix(worker,db): stop pinning a pooled connection across the scan subprocess (CON-10)
A running scan held its pooled DB connection for its full wall-clock, so
concurrency was silently capped by the pool (5+10) and every API call
500'd past that. The worker now resolves all DB inputs (policy +
credentials/paths, with decrypts) up front into detached values, rolls
back to return the connection to the pool, runs the minutes-long scanner
subprocess holding none, and re-acquires only for persistence. As
defense-in-depth the pool is sized from max_concurrent_scans (+headroom)
and the setting is capped at 32. Regenerated .env.example.
* fix(worker): advance DB-update marker only on success (CON-12)
maybe_update_scanner_dbs set its last-run marker before running the two
updates, so a transient failure (e.g. a registry outage) silently left
the vulnerability DBs stale for a full db_update_interval_hours while the
UI implied freshness. _run_update now returns success and the marker is
advanced only when at least one engine actually updated; a total failure
leaves it unset to retry on the next tick.
* fix(worker): run scanner-DB refresh off the maintenance tick's critical path (CON-13)
The tick awaited maybe_update_scanner_dbs (two subprocesses, each capped
at 600s) inline, so a slow mirror could delay the next tick's due
schedules and retention by up to ~20 min. The refresh now runs as its
own detached task, guarded so at most one runs at a time; the tick
returns promptly after schedules + retention.
* fix(worker): dispatch scan notifications after releasing the semaphore slot (CON-15)
Notification dispatch ran inside the concurrency-semaphore block, so a
scan finishing against slow or dead channels (10-15s transport timeouts
each, sent sequentially) kept holding its slot while queued scans waited.
_run now returns whether a terminal state was reached and _execute
dispatches the notification only after the async-with semaphore block
exits, freeing the slot first.
* fix(worker): retrieve task exceptions and bound task spawning (CON-16)
The per-scan task's done callback only discarded the task, so an error
escaping _execute (e.g. the session factory failing in a shutdown race)
died as a silent GC-time 'Task exception was never retrieved'. The
callback now retrieves and logs it; the session-factory call is guarded
so such a failure leaves the scan QUEUED for the watchdog. submit() also
caps concurrently-live tasks (generously, scaled from max_concurrent) so
a submission flood is deferred to the watchdog instead of piling up
unbounded pending tasks.
* fix(schedules): stamp last_run_at on 'Run now' to stop same-minute tick duplicate (CON-17)
run_schedule_now created a scan without touching last_run_at, so the cron
tick (which fires on last_run_at) could fire the same schedule again in
the same minute — a duplicate back-to-back scan and a raced last_scan_id.
'Run now' now records the run (last_run_at + last_status), so the tick
finds the schedule not due for the rest of the minute.
* fix(dashboard): don't abandon the scanner-DB probe on a DB error (CON-18)
The dashboard gathered DB aggregation and the subprocess-backed scanner-DB
freshness probe without return_exceptions, so a DB failure propagated
immediately and left the probe's subprocesses running detached (repeated
reloads under contention accumulated duplicate probes). gather now uses
return_exceptions=True and handles each branch: a DB error re-raises after
the probe is awaited to completion; a probe error degrades to an empty
scanner-DB list instead of failing the whole dashboard.
* fix(worker): re-read the scan before notifying to catch a concurrent delete (CON-19)
_notify's session.get was served from the identity map (expire_on_commit=
False keeps the just-committed scan cached), so a scan deleted the instant
after it reached a terminal state was still announced — e.g. a webhook
whose link 404s. Passing populate_existing=True forces a fresh read, so a
concurrent DELETE is seen and the notification is skipped.
* fix(scanners): remove the repo checkout off the event loop (CON-20)
generic_repo_checkout's finally ran shutil.rmtree on the clone directory
inline; a multi-GB working tree blocked the event loop for seconds while
walking and unlinking it. Hop the checkout removal to a thread, shielded
so the cleanup still completes when the finally runs under cancellation
(worker shutdown). The small tmpfs cred dir removal stays inline.
* test(worker): make the CON-16 crashed-task log assertion config-independent
Spy on the module logger instead of caplog so the assertion doesn't
depend on logging propagation set up by other tests in a full run.
* docs: log CON-5–CON-20 concurrency-review remediation in ARCHIVE deviations
* fix: API-review findings (APIR-1…APIR-10) (#61)
* fix(trivy-policy): normalize ignore-rule expires_at to naive UTC (APIR-1)
A timezone-aware expires_at (e.g. 2026-08-01T00:00:00+09:00) was persisted
into the naive-UTC DateTime column with its offset silently dropped, so a
Trivy ignore rule kept suppressing a CVE for the offset's worth of extra
time. Add a shared to_naive_utc helper in core.timeutil (the single source
of truth scan_filters now reuses) and apply it as a field_validator on
IgnoreRuleIn.expires_at, covering both the create and replace paths.
* fix(api): flatten schema-validation 422s to a string detail envelope (APIR-2)
FastAPI's RequestValidationError put a list in detail while hand-raised
HTTPException(422) put a string there; the SPA only renders the string
shape, so every schema-level validation failure (too-long tag, bad cron,
target starting with -) showed as a blank "Request failed (422)". Add a
RequestValidationError handler that flattens the first error into the same
string detail envelope every other 422 already uses, prefixed with the
offending field.
* fix(api): serialize response timestamps with an explicit UTC Z (APIR-5)
Naive-UTC timestamps were serialized as bare ISO-8601 with no zone
designator, so a consumer parsing them as browser-local shifted every
instant by its UTC offset; ScansPage.runCompare did exactly this and could
invert a diff's base/compare ordering across a DST boundary. Add a shared
UtcDatetime field type (PlainSerializer appending Z in JSON mode only) and
apply it to every response-model datetime; storage stays naive. Route
ScansPage's compare ordering through parseUtc as belt-and-suspenders.
* fix(scan-diff): gate Compare on target_type and add location to diff payload (APIR-3)
The SPA enabled Compare for two scans sharing scanner+target string but
differing in target_type, which the diff endpoint then rejects with 422 —
a dead end. Add the target_type equality to canCompare. Separately, the
diff identity keeps location for every non-vulnerability class (one rule
fires across many files), but DiffFindingOut dropped it, so distinct
per-file occurrences serialized as byte-identical rows; add location to
the payload, the DiffFinding type, and a Location column on the diff view.
* fix(history-export): signal truncation when the 5000-scan cap fires (APIR-4)
A filtered-history export capped at _MAX_HISTORY_EXPORT_SCANS gave no
indication the cap fired, so a consumer read a partial download as the
complete filtered set. Count the full matching set in export_history_view
and thread it through export_history: JSON gains total/truncated metadata,
Markdown a visible note line, CSV a leading '#' comment, and every format
an X-Scrye-Truncated / X-Scrye-Total response header. Signals are only
emitted when truncation actually happened.
* fix(api): align secret-bearing update validation with create (APIR-6)
Update paths could reach states create forbids. PATCH secret="" on a
notification channel cleared a mandatory secret, leaving an enabled channel
that only fails at send time — now rejected 422 unless the type is in
SECRET_OPTIONAL_TYPES. RegistryUpdateIn didn't strip name/registry_host, so
' ghcr ' could shadow 'ghcr' past the 409 duplicate check — add the same
strip validator create uses; and blanking the username on a
username_password registry is now rejected.
* test(schedules): assert run-now stamps last_status (APIR-7)
APIR-7 (run-now leaving last_run_at/last_status stale) was already fixed by
the CON-17 remediation in #60, which stamps all three last_* fields in
run_schedule_now. Add the last_status assertion the review specifically
called out so the resolved behavior stays locked in; no code change needed.
* refactor(audit): rename pagination envelope entries -> items (APIR-8)
The audit list was the only paginated endpoint using a third key name
(entries) instead of the {total, items} envelope shared by history and
findings. Rename it for consistency; the endpoint is admin-only with no
frontend consumer, so the blast radius is the backend tests. Broader
standardization of the unpaginated bare-array admin lists is intentionally
out of scope per maintainer direction.
* refactor(scans): trim list/history/dashboard rows to a summary shape (APIR-9)
List, history, and dashboard-recent rows shipped the full ScanOut on every
row — including options (which also leaks internal registry_id/
git_credential_id to viewers) and unbounded scanner error text — none of
which those views render. Split a ScanSummaryOut (drops options/error, adds
a has_error flag) used by those endpoints; the full ScanOut, extending the
summary with options + error, is returned only by the single-scan detail
endpoint where the SPA actually reads them. Frontend mirrors the split with
a ScanSummary type.
* refactor(scanners): extract the scanner-target matrix to one source (APIR-10)
The scanner-to-target-type compatibility matrix was duplicated in the scans
and scan-schedules routers and had already drifted (the schedules copy
omitted the SBOM row). Move it to app/scanners/support.py as
SCANNER_TARGET_SUPPORT + scanner_supports(); both routers import it, so
adding a combination is a one-line change neither router can miss. The
schedules router keeps only its extra 'SBOM cannot be scheduled' rule.
* docs(archive): log the APIR-1..APIR-10 API-review batch
* fix(frontend): frontend-review wave 2 (M19–M21, L16–L22) (#62)
* fix(frontend): gate settings forms until initial GET resolves (M19)
Settings forms rendered editable with hardcoded defaults and enabled
Save from first paint, so on a slow connection an admin could Save before
the initial GET resolved — silently writing the built-in defaults over
the live policy — or have a late response overwrite in-progress edits.
- RetentionPanel/GeneralPanel: track a `loaded` flag; disable the inputs
and show a loading Save button until the GET resolves, and only seed
fetched values into a form the admin hasn't already started editing
(`!form.isDirty()`).
- BackupsPanel: split the schedule-form hydration out of the shared
load() so list mutations (create/delete/restore) no longer re-hydrate
and reset unsaved schedule edits; gate the schedule controls on load.
- NewScanPage: apply the FEAT-7 instance-default prefill only to a
pristine form so a slow response can't revert a user's severity/
ignore-unfixed edits.
* fix(frontend): back off and halt the scan-detail poller on errors (M20)
The 2.5s status poll was gated only on isActive(scan.status), and a
failed loadScan() kept the previous (still-active) scan in state — so the
interval fired forever against a failing endpoint while the badge kept
claiming "running". A restarted backend, expired session, or a scan
deleted by another admin would be hammered every 2.5s indefinitely.
The poller now uses a self-scheduling timeout with exponential backoff
(2.5s → 30s cap) and halts after MAX_POLL_FAILURES consecutive failures,
surfacing an "Auto-refresh paused" alert with a Retry affordance; a 404
is treated as terminal (scan gone). Backoff math lives in a pure
lib/polling helper with unit tests.
* fix(frontend): guard history fetch against stale responses (M21)
The 250ms debounce delayed sending history requests but never cancelled
in-flight ones, and load() had no latest-wins check before setData(). A
slow request for one filter could resolve after a newer request and
overwrite the table and pagination with rows for a filter no longer
selected. A per-view latest-wins guard (new lib/latest, unit-tested) now
tags each request and drops out-of-order resolutions.
* fix(frontend): stop the status poll from wiping tag edits (L16)
loadScan() unconditionally reset the tag draft to the server's list, and
the 2.5s status poll calls it while a scan is active — so an operator
typing into the TagsInput on a running scan had their half-entered draft
reset on every tick. Track the last server tags synced into the draft and
re-adopt the server value only while the draft still matches it (ordered
equality via a new lib/arrays helper, unit-tested), preserving any
in-progress edit.
* fix(frontend): reset per-scan state on scan-detail navigation (L17)
React Router reuses the ScanDetailPage instance when only :scanId
changes, and no state was reset — so navigating between scans left the
previous scan's header, findings, artifacts, and tag draft on screen
until the new fetch landed, and the artifacts/findings effects (gated on
the still-stale scan.status) fired for the new id against the old status.
Reset scan/findings/artifacts/filters/tag draft and poll state in an
effect keyed on the id.
* fix(frontend): add findings loading state and stale-response guard (L18)
The findings panel initialized to [] with no loading flag, so a succeeded
scan flashed "No findings match the current filters." until the first
fetch resolved — false for a scan with thousands of findings — and rapid
filter toggles rendered the previous filter's rows with no indication a
load was pending. Track loading/loaded flags (show a Loader on first load
instead of the empty state; dim the table with a LoadingOverlay during
filter changes) and reuse the latest-wins guard so only the final
response renders.
* fix(frontend): guard double-fire on in-flight mutations (L19)
Several mutation triggers had no in-flight disable, so a double-click
fired them twice. Most seriously, double-clicking Create in ApiTokensPanel
minted two tokens while the plaintext alert showed only the second —
leaving an active bearer token the user never saw and…
tyler-rich
added a commit
that referenced
this pull request
Jul 20, 2026
…w-remediation batch (#74) Re-verifies every finding across all six original code-review reports (including findings never carried into 00-summary.md) against the current state of dev, after the H5/CON-4 follow-up (#67) and the dev->main promotion (#70). Records the current STILL-OPEN backlog (frontend Priority-3 batch, SC-12/SC-14, D5b, test debt), deferred-by-decision items with their tracking refs, a resolved index, and the ARCHIVE.md section-14 entry gaps (#53 H1/SEC-1, #57 H9+H10, #65 D1/D2/R1-R6, #59).
tyler-rich
added a commit
that referenced
this pull request
Jul 20, 2026
…r-strip rule (#76) Back-fill four docs/ARCHIVE.md §14 entries for merged fixes that landed without a dated entry, using docs/reviews/STATUS.md § "ARCHIVE.md §14 gaps" and the actual PR history as the source of truth: - #53 — H1/SEC-1: repository scan targets must be remote clone URLs (local-path arbitrary-read closed). Notes explicitly that this SEC-1 is distinct from the older webhook-URL "SEC-1" already in §14. - #57 — H9/SC-2 + H10/SC-3: SHA-pin all Actions, expand Dependabot to pip/npm/docker/docker-compose + composite-action dir, harden publish checkouts, converge D3/SC-10/L25 version skew. - #65 — D1/D2/R1–R6 compliance-drift closure pointing at already-logged deviations. - #59 — backend dev-dependency bumps (pytest, pytest-asyncio, black; ruff held at 0.8.6). Each entry is dated to its merge date (2026-07-13) and marked as a back-fill written 2026-07-20. Also add a CLAUDE.md § Git & PR conventions rule: after opening any PR, re-check the live PR body and strip any auto-appended attribution footer, so PR bodies carry no Claude/Anthropic identity — with a matching dated §14 entry (2026-07-20) recording the addition. Docs only.
tyler-rich
added a commit
that referenced
this pull request
Jul 31, 2026
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.
tyler-rich
added a commit
that referenced
this pull request
Aug 2, 2026
…on (#134) * docs: triage the first CodeQL run and record the default-setup decision Code scanning (CodeQL) was enabled via default setup on 2026-08-02. The first run on main @ bb354a5 produced five alerts, all Python: two py/path-injection on the filesystem-scan containment gate and three py/incomplete-url-substring-sanitization on test assertions. Every alert was read against the source and classified. All five are false positives, with the reasoning recorded per finding rather than asserted: - The two targets.py alerts are unclearable by construction. CodeQL's PathInjection config models only os.path.normpath/abspath/realpath as normalizations, so pathlib's Path.resolve() never moves the taint out of NotNormalized and the SafeAccessCheck barrier is unreachable regardless of the check written. Its only recognized check is str.startswith - the idiom this code deliberately avoids because of prefix confusion. - The three test-file alerts come from a purely syntactic query that matches any `"<host>" in <anything>` comparison, with no dataflow and no requirement that the operand is a URL or the result a security decision. Also records the overlap with the 2026-07-03 filesystem-allowlist entry and H1/SEC-1 (#53), which built and deliberately kept the gate now being flagged. Nothing was fixed, dismissed, or excluded; the ROADMAP item is struck with the remaining disposition work and its two dependencies called out. Docs only. See docs/ARCHIVE.md section 14 for the full triage. * docs: record that CodeQL does not run on dev PRs Default setup's pull-request trigger targets the default branch, so it covers PRs into main; dev - where day-to-day work is actually PR'd - gets no CodeQL check at all. Confirmed on #134 itself: four check runs, none of them CodeQL. Both docs previously left this as a question to confirm against a real dev PR. It has now been confirmed, so record the observation and its consequence: the roadmap item's worry that enabling CodeQL would immediately join the per-PR gate is inverted - on the branch that receives PRs, it does not run at all, and main is only analysed on push after a promotion has landed, plus weekly. Adding dev to the trigger requires a committed workflow, so this becomes a concrete second reason to revisit the default-vs-advanced choice alongside path filters and query-suite tuning. Docs only. * docs: correct the CodeQL triage - six alerts, and the suite is security-extended The first triage was wrong twice, in a way worth recording rather than quietly fixing. The run uses the security-extended query suite, not the default code-scanning one. "Default setup" names the setup mode; the suite is a separate dropdown and is set to Extended. The evidence is exact: the Python job interpreted 52 queries, python-code-scanning.qls resolves to 45, python-security-extended.qls resolves to 52, and the runner's 52 interpreted paths are a set-identical match to the extended list. Because the local reproduction ran code-scanning.qls, it ran 45 of 52 Python queries and reported five alerts instead of six. The seven omitted queries include py/log-injection, which is alert #6. Notably the wrong reproduction matched the runner's file-extraction counts (174/78/5) exactly - so matching extraction coverage proves nothing about query coverage, and a reproduction is not equivalent until its query set is checked against the run's. The corrected run reproduces the Security tab exactly: six alerts, same rules, files, lines and severities. Counts are 5 High + 1 Medium. Adds the triage for alert #6, py/log-injection at api/scans.py:574: scan_id is an int-annotated FastAPI path parameter, coerced by Pydantic before the handler runs and rendered with %d, so no newline can reach the log record. CodeQL treats route parameters as tainted regardless of type annotation, and its sanitizer set is only constant-comparison, explicit line-break replacement, and models-as-data barriers. Also corrects the default-vs-advanced reasoning: suite choice is not an advanced-setup exclusive, so the real exclusives are custom query packs, path filters, and trigger control - and trigger control is the one that matters, given CodeQL does not run on dev PRs. Docs only. * docs: close out the filesystem-gate symlink/TOCTOU risks; assess CodeQL advanced setup Item 1 - both residual risks the CodeQL entry named are closed, and one of them was simply wrong. The symlink escape does not happen. Filesystem targets are Grype-only, grype 0.115.0 embeds syft v1.46.0 (the pinned version), and syft's dir provider defaults its base to the scan directory, which activates chroot-style re-rooting of every symlink target under that root. Five planted variants - absolute and relative, to directories and files, plus one to /etc - were all re-rooted and dropped; only the genuine in-root package was catalogued. Trivy fs behaves the same. Note that a comment in indexAllRoots says the opposite; the re-rooting runs first, so the comment describes an intent the code no longer implements. Hardlinks are followed, but that is not a bypass: same inode, requires read access the attacker already has, cannot cross filesystems. Recorded with the methodology slip that produced a false negative first time - the probe file must be named what the cataloger globs. Impact ceiling matters for future severity ratings: grype dir: output carries no file contents, so this class cannot reproduce H1/SEC-1, whose severity came from secret values reaching downloadable output. TOCTOU is a real mechanism - syft re-resolves the root through EvalSymlinks at scan time - but needs the feature enabled, host write access to the target's parent, a concurrent operator-triggered scan, and a won race, for an inventory disclosure. Accepted, no work proposed. The one actionable item is a regression test: containment rides on syft's basePath(), which upstream annotates "FIXME why is the base always being set", so a routine scanner bump could silently make the escape real. Recommended, not implemented. Item 2 - advanced-setup assessment recorded, with the measured CI cost (~0 added wall clock), the maintenance cost (~0 marginal, since dependabot already groups action bumps weekly), confirmation that security-extended is reproducible via the queries: input, what migrating does and does not lose, and the case against. Recommendation is to migrate after branch protection. Corrects the premise that findings arrive after :latest is published - :latest comes from a tag push, so the real gap is :dev. Docs only. Nothing implemented. * docs: correct the CodeQL sequencing premise; branch protection on dev is already live The item-2 recommendation said to migrate to advanced setup only after the branch-protection governance item, on the premise that a CodeQL check on a dev PR could not block a merge until then. Reading the ruleset via the API shows that premise was wrong. protect-dev is enforcement: active and already carries pull_request (1 approval, dismiss-stale-on-push, thread resolution, squash-only), required_status_checks, deletion, and non_fast_forward. protect-main is equivalent. The conclusion survives for a sharper reason: required_status_checks is an explicit allowlist of contexts, currently naming only "Backend - lint + tests" and "Frontend - lint + build". Neither image job is on it, and CodeQL's contexts would not be either - so CodeQL would run and be visible without blocking a merge. But the remedy is adding two or three strings to a ruleset that already exists, done alongside the migration, not a governance project. The sequencing dependency is withdrawn. On the admin bypass: it does not change much for the owner, but required-ness still buys enforcement for external contributors and converts "merge anyway" from a non-event into an explicit act - which matters here specifically because §14 already records this project normalizing red checks. The bypass list itself is not readable at this token's permission level, so that part cites the maintainer's statement and the observed dev deletion rather than an API dump. Also flags an operational hazard: a required context that never reports blocks a PR forever, so the CodeQL workflow must not carry path filters if its contexts become required. Re-scopes the ROADMAP governance bullet, which listed branch protection as wholly open when most of it is done - what remains is code-owner review, tag push restrictions on main, and a decision about the unrequired image jobs. Cross-references #135 (the Syft basePath regression test) from the entry. Docs only. * docs: track the two ruleset gaps as issues; strike an item that was already done The 2026-08-02 ruleset readout produced two settings-level gaps. Both are now issues rather than prose, on the same reasoning the governance checklist exists for - a settings gap leaves no artifact in the repo, so untracked means invisible. #136 - the dogfood self-scan is not on required_status_checks, so a PR can merge into dev with the image scan red. That job is the control CLAUDE.md mandates: it caught CVE-2026-5773, verifies the SC-14 dev-tree exclusion, and demonstrates the seven waived interpreter CVEs are the only outstanding findings - unverifiable if the gate can be merged past. Includes the paths:-filter hazard, and the distinction that a job skipped by if: still reports and satisfies a required check while a workflow that never triggers does not. #137 - no tag-targeted ruleset exists, and a v*.*.* tag push triggers publish.yml: GHCR push, the :latest move, provenance and SBOM attestation. publish.yml's repository guard and main-ancestry check bound the blast radius but do not constrain who may tag. Theoretical with a sole maintainer; trigger is before any collaborator is added. Split into two issues rather than one, per the #98/#116 precedent that an issue closes on its own trigger - #136 closes on a settings edit now, #137 on an event that may be far off. Separately, auditing the checklist found private vulnerability reporting was already enabled - the API returns {"enabled": true} - while the roadmap still listed it as open. Struck, with a note that this is the same drift the checklist exists to prevent arriving from the opposite direction: a completed item left listed as outstanding. Signed-commit enforcement is confirmed genuinely open (no required_signatures rule on either ruleset). Also records that the attribution footer could not be stripped from the issue bodies - the ingress layer re-appends it on issue writes as it does on the PR body - so the inconsistency with #98/#116 is explained rather than looking like a style lapse. Docs and issues only.
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.
Summary
Fixes SEC-1 / Top 5 #1 from the consolidated review: an operator (not admin) could read arbitrary host files via a "repository" scan, bypassing the
SCRYE_FILESYSTEM_SCAN_ROOTSallowlist.A
target_type=repositoryscan was validated only for length and a leading-, then passed straight totrivy repo -- <target>. Trivy'sreposubcommand accepts a local filesystem path, not just a remote URL — so a target like/dataor/run/secretsmade Trivy walk the container filesystem and persist the results as a downloadable artifact. That is exactly the SQLite-DB / master-key exposure the filesystem allowlist exists to prevent, reached through an ungated code path.Invariant restored
A
repositoryscan target must be a remote git clone URL (schemehttp,https,ssh, orgit) — never a local filesystem path. This keepsSCRYE_FILESYSTEM_SCAN_ROOTSas the only way any scan can be pointed at local paths.Approach
Chose "require repository targets to be remote clone URLs" over routing local paths through the filesystem gate. Local git-repo scanning is not a feature Scrye offers (the target field is documented as a "repo URL", there is no UI/config for it, and the roadmap's local-path capability is filesystem-only) — routing bare paths through the allowlist would add a capability rather than close a hole. Requiring a remote URL fits the existing validation architecture:
ScanCreateInalready rejects option-like targets/refs at the schema layer, so scheme validation lives there too, rejecting at request time (422).What changed
app/scanners/credentials.py— newis_remote_repo_url()helper (reusesis_http_urlplus anssh/gitscheme allowance).app/api/scan_schemas.py—ScanCreateInmodel validator rejects a repository target that is not a remote clone URL. BecauseScanScheduleInsubclassesScanCreateIn, the guard also covers scheduled scans./data,/run/secrets,/,/app, andfile://repository targets are rejected (422, scanner never reached); a valid remote clone URL still runs tosucceeded; direct unit coverage ofis_remote_repo_url.Verification
ruff==0.8.6,black==24.10.0):ruff check .andblack --check .both clean.Minimal diff, no unrelated refactors. README already documents repository targets as an HTTPS clone URL, so no doc change was needed. Not a divergence from the plan's design, so no
docs/ARCHIVE.mddeviation entry.