* Run the long-lived process as the configured non-root user
start.py used to stay root for its whole life; only the individual
seaf-cli/seaf-daemon subprocess calls were unprivileged, via a per-call
runuser wrapper. The orchestrating Python process itself, including the
RPC socket to seaf-daemon and the watch_status() loop, ran as root the
entire time.
main() now drops privileges in-process (dsc.misc.drop_privileges, using
os.initgroups/setgid/setuid) right after setup_uid()/create_dir() -
which still need root to remap the seafile user's UID/GID and chown
/dsc - and before SeafileClient is constructed. Since the process is
already the target user by the time any seaf-cli call happens, runuser
is removed from user_cmd(); it also could not be invoked from an
already non-root process.
Verified against the real image, not just mocked unit tests: a running
container's PID 1 shows Uid: 1000 1000 1000 1000 in /proc/1/status, and
tests/smoke.sh gained a check exercising setup_uid/create_dir/
drop_privileges together against the image's real /etc/passwd.
* Isolate os.environ mutation in drop_privileges test
drop_privileges() writes HOME/USER/LOGNAME directly to os.environ, and
the test asserting that didn't restore them, leaking state into later
tests. monkeypatch.delenv registers the keys so its teardown restores
them regardless of who wrote to os.environ.
---------
Co-authored-by: Maxim Shulga <maxim.shulga@alludo.com>
* Run the image vulnerability scan on a weekly schedule
The Trivy scan added in #29 only ran against changed code (PRs and
pushes), so a CVE disclosed against an otherwise unchanged base image
would go unnoticed until the next actual change. Add a weekly cron
trigger, matching Dependabot's cadence in .github/dependabot.yml.
The push job's existing `if: github.event_name == 'push'` guard means
a schedule-triggered run builds and scans but never publishes.
* Add workflow_dispatch so the scan can be run on demand
Copilot review on PR #30 suggested this: waiting for the Monday cron
to validate a fix is unnecessary friction. workflow_dispatch has no
inputs, so it just re-runs the same build/test/scan on demand; the
push job's event_name guard still keeps it from publishing.
---------
Co-authored-by: Maxim Shulga <maxim.shulga@alludo.com>
* Scan the built image for vulnerabilities and fix what's fixable
Ran trivy against the built image locally: 67 HIGH/CRITICAL findings,
65 of them in debian:bookworm-slim packages with no upstream fix yet.
The remaining 2 were both setuptools 66.1.1 (CVE-2024-6345,
CVE-2025-47273) — bundled by ensurepip when the venv is created, not
pinned by requirements.txt, so nothing was upgrading it.
- Upgrade setuptools to >=78.1.1 in the Dockerfile, which fixes both
CVEs. Verified with a local rebuild + rescan: 0 HIGH/CRITICAL
fixable findings remain.
- Add a Trivy scan step to the CI `test` job, after the smoke test.
Uses --ignore-unfixed so the build fails only on findings this repo
can act on, not on upstream Debian CVEs with no available patch.
- Check off the corresponding P0 item in IMPROVEMENTS.md; note that
scheduled (as opposed to PR/push-triggered) scanning is still open.
* Fix trivy-action tag (needs v prefix)
* Tighten scan-coverage wording per review
Copilot review on PR #29 flagged "every pull request and push" as
ambiguous: the test job's push trigger is scoped to master and tags,
not every branch. Spell that out.
---------
Co-authored-by: Maxim Shulga <maxim.shulga@alludo.com>
* Bound HTTP request timeouts and retry budget
With no request timeout, Retry(total=30, backoff_factor=2,
backoff_max=60) let a single unreachable server hang the client for
around 26 minutes: backoff sleeps alone sum to that, and each of the
30 connection attempts could itself stall indefinitely without a
timeout. Measured a black-holed connect attempt taking 75s on this
machine with no timeout set, confirming the mechanism.
- Add HTTP_TIMEOUT (connect, read) and a tighter retry budget
(HTTP_RETRY_TOTAL/BACKOFF_FACTOR/BACKOFF_MAX) in dsc/const.py,
bounding the worst case to a few minutes.
- Pass timeout=HTTP_TIMEOUT on both SeafileClient HTTP calls (token
fetch, remote library listing).
- Add NetworkError(DscError) so a network failure is caught by
start.py's existing top-level DscError handling instead of
producing a bare traceback.
- Add tests/test_http.py covering the retry policy values, timeout
propagation, and error wrapping.
- Check off the corresponding IMPROVEMENTS.md items.
* Include URL and status code in NetworkError messages
Copilot review on PR #28 flagged that the non-200 path for remote
library listing raised NetworkError with only the raw response body,
losing which request failed and its status code. The token-fetch
error had the same gap. Both now include the URL and HTTP status.
---------
Co-authored-by: Maxim Shulga <maxim.shulga@alludo.com>