Skip to content

feat(audit): optional best-effort webhook for clipboard and file events - #256

Merged
ehfd merged 6 commits into
selkies-project:mainfrom
DL6ER:audit-webhook
Sep 12, 2026
Merged

feat(audit): optional best-effort webhook for clipboard and file events#256
ehfd merged 6 commits into
selkies-project:mainfrom
DL6ER:audit-webhook

Conversation

@DL6ER

@DL6ER DL6ER commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Reference the issue numbers and reviewers

Closes #249.

Explain relevant issues and how this pull request solves them

#249 asks for a way to observe the data-transfer channels Selkies handles - an audit trail for regulated environments, without forking Selkies. Inside the Selkies Python package that means the bidirectional clipboard and file upload; file download is served by the nginx libnginx-mod-http-fancyindex module and is out of scope here.

This pull request implements the webhook design proposed in #249: a single optional audit_webhook_url setting. When unset (the default) Selkies behaves exactly as before. When set, Selkies POSTs a small JSON object to that URL on each clipboard / file-upload event, carrying metadata only - never the payload contents.

Describe the changes in code and its dependencies and justify that they work as intended after testing

  • New module selkies/audit.py: a fire-and-forget JSON-over-HTTP sink. Owns one lazily-created aiohttp.ClientSession, exposes module-level configure() / emit() / is_enabled() / close(). emit() schedules the POST on the running loop and returns immediately; all failures (DNS, refused, timeout, HTTP >= 400, anything aiohttp raises) are caught and logged, so a slow or unreachable collector can never stall the streaming pipeline. When no URL is configured, emit() is a no-op.
  • settings.py: three new str settings - audit_webhook_url, audit_webhook_token, audit_webhook_timeout (defaults keep the channel disabled).
  • __main__.py: audit.configure(...) is called once at startup from the parsed settings.
  • emit() call sites: clipboard.send (in send_ws_clipboard_data), clipboard.receive (text / binary / multipart paths in the input handler), file.upload.end and file.upload.error. Each carries event id, RFC 3339 timestamp, byte size, mime type and/or filename as applicable.
  • Dependency: aiohttp, already a Selkies dependency.

Testing: the module ships in our EL/XFCE (Python 3.9) build and is imported and configured on every session start; the default (no URL) no-op path is exercised by the end-to-end suite, which is green, confirming the additions do not affect existing behavior when the channel is disabled. The enabled path was exercised against a local collector: configure() with a URL + Bearer token, then emit() for clipboard and file events, produces one POST per event with the expected JSON metadata and Authorization: Bearer header; a down/refusing collector only logs a warning and does not interrupt the session.

Describe alternatives you've considered

  • Logging transfers to a file and shipping logs out-of-band: leaves correlation and delivery to the operator and ties the record to the container's filesystem lifetime; a webhook lets the collector own retention and auth.
  • A blocking/awaited POST in the data path: rejected - audit must never be able to stall or fail a transfer, hence the fire-and-forget, best-effort design.

Additional context

The Bearer token is sent as Authorization: Bearer ... specifically so the POST can be terminated at any standard reverse proxy with mature auth middleware, as outlined in #249.

  • I confirm that this pull request is relevant to the scope of this project. If you know that upstream projects are the cause of this problem, please file the pull request there.
  • I confirm that this pull request has been tested thoroughly and to the best of my knowledge that additional unintended problems do not arise.
  • I confirm that the style of the changed code conforms to the overall style of the project.
  • I confirm that I have read other open and closed pull requests and that duplicates do not exist.
  • I confirm that I have justified the need for this pull request and that the changes reflect the fix for the specified problem.
  • I confirm that no portion of this pull request contains credentials or other private information, and it is my own responsibility to protect my privacy.
  • I confirm that the authors of this pull request does not willfully breach or infringe legal regulations, in any and all global law, regarding trademarks, trade names, logos, patents, or any and all other forms of external intellectual property, as well as adhering to software license terms of open-source and proprietary software projects.

Copilot AI review requested due to automatic review settings June 14, 2026 12:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds an opt-in audit webhook channel that emits best-effort JSON metadata for clipboard and file-upload events.

Changes:

  • Introduces audit.py with an async, fire-and-forget AuditClient and module-level configure/emit helpers.
  • Emits audit events on clipboard send/receive and on file-upload completion/error paths.
  • Adds new settings and configures the audit client at startup.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/selkies/settings.py Adds settings for audit webhook URL/token/timeout.
src/selkies/selkies.py Emits audit events for clipboard send and file-upload end/error.
src/selkies/input_handler.py Emits audit events when clipboard content is received (single- and multi-part).
src/selkies/audit.py New module implementing the audit webhook emitter (aiohttp, fire-and-forget).
src/selkies/main.py Configures the audit client from settings at startup.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/selkies/audit.py Outdated
Comment thread src/selkies/audit.py Outdated
Comment thread src/selkies/__main__.py Outdated
Comment thread src/selkies/settings.py

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a best-effort, non-blocking audit-event emitter to log metadata for clipboard and file-transfer events via an optional webhook. Key feedback focuses on securing the sensitive webhook token by marking it as sensitive in settings, maintaining strong references to background asyncio tasks to prevent premature garbage collection, safely reading response bodies to avoid memory exhaustion, and offloading the blocking os.path.getsize call to a separate thread to prevent event loop lag.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/selkies/settings.py Outdated
Comment thread src/selkies/audit.py Outdated
Comment thread src/selkies/audit.py Outdated
Comment thread src/selkies/audit.py Outdated
Comment thread src/selkies/audit.py Outdated
Comment thread src/selkies/selkies.py Outdated
@DL6ER

DL6ER commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review. Addressed:

  • audit_webhook_token is now marked sensitive: True, so it is excluded from the server_settings broadcast (it was otherwise sent to all connected clients).
  • Background POST tasks are kept in a strong-reference set with a done-callback, so they can't be garbage-collected mid-flight; close() now drains them (asyncio.gather) before closing the session.
  • close() is awaited in a finally around run(), and configure() schedules teardown of a previous client's open session, so reconfiguring/shutdown no longer leaks the connector.
  • Enforce https://: a non-HTTPS URL disables the channel with a warning, so the token and metadata never traverse cleartext.
  • Bounded the error-response read to 2 KiB so a misbehaving collector can't make us buffer a huge body.

Left audit_webhook_timeout as str since the settings layer has no float type; it is parsed once in run().

Smoke-tested on the EL/XFCE (py3.9) image: default no-op, http:// rejected, the https:// path tracks and drains the background task, and a dead collector only logs a warning.

@DL6ER

DL6ER commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Heads-up for reviewers: the failing py-build / gst-web image build & publish checks are not caused by this change. They fail identically on main (the image-build workflow currently can't resolve its base ghcr.io/selkies-project/selkies/selkies-web:main), and this PR only touches src/selkies/*.py - none of the image Dockerfiles. The code check (validate) passes.

@PMohanJ

PMohanJ commented Jun 15, 2026

Copy link
Copy Markdown
Member

I think the decision is yet to be taken on this.
Either way here's a pointer - the file download part is out of the scope of Selkies code. It's primarily handles by nginx module libnginx-mod-http-fancyindex and these are its counter UI parts: https://github.com/selkies-project/selkies/tree/main/addons/selkies-web-core/nginx. Only clipboard (outbound, inbound), file upload is handled by Selkies python pacakge.

@DL6ER

DL6ER commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Agreed, thanks for the pointer. File download is the nginx libnginx-mod-http-fancyindex domain, not the Selkies Python package, so it is out of scope for this emitter. The code already only emits the channels Selkies handles - clipboard.send, clipboard.receive (text/binary/multipart) and file.upload.end / file.upload.error; there is no download event. I have dropped the stray "download" wording from the module docstring and the PR description so the scope reads as clipboard + file upload.

Happy to hold for the decision on whether to take this at all.

@ehfd

ehfd commented Jul 6, 2026

Copy link
Copy Markdown
Member

Note that #254 removes the explicit NGINX dependency (it can still be sped up by NGINX).

@ehfd

ehfd commented Aug 19, 2026

Copy link
Copy Markdown
Member

I'd like to defer this work until after v2.0.0; this is a non-critical component.

Add an opt-in audit channel that POSTs small JSON metadata objects to a
configurable webhook on clipboard send/receive and file upload events.
Only metadata is sent (event type, byte size, mime type, timestamp);
payload contents are never logged. The emitter is non-blocking and
best-effort, so a slow or unreachable collector cannot stall the
streaming pipeline. Disabled by default - an empty URL short-circuits
emit() to a no-op.

New settings: audit_webhook_url, audit_webhook_token, audit_webhook_timeout.

Signed-off-by: DL6ER <dl6er@dl6er.de>
File download is served by the nginx fancyindex module, outside the
Selkies Python package, so it cannot be observed from here. The emitter
already only emits clipboard and file-upload events; drop the docstring
claim that it also covers downloads.

Signed-off-by: DL6ER <dl6er@dl6er.de>
@ehfd
ehfd force-pushed the audit-webhook branch 5 times, most recently from 57428e8 to c986690 Compare September 12, 2026 15:08
@ehfd
ehfd merged commit 17eef1f into selkies-project:main Sep 12, 2026
2 checks passed
@DL6ER
DL6ER deleted the audit-webhook branch September 12, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Optional audit webhook for clipboard and file-transfer events

4 participants