Appearance
Clipboard protection, claim by claim
WIN-DLP-EPIC-013 asks for clipboard content to be inspected under an explicit policy, in the user's session, without becoming a keylogger - because copy-and-paste is the most common data movement nobody records.
The mechanism is built and the privacy discipline around it is the most carefully argued in the product. What this audit found is that its central claim - that a harmless copy goes no further - was half-tested, and the test said so itself.
Implementation: agent/core/src/clipboard.rs for reading, agent/core/src/session_link.rs for the channel, Agent::queue_clipboard_report in runner.rs for the decision.
1. Off unless a policy asks - and a policy, not a checkbox
Reading what somebody copied is the most invasive thing in the product, so turning it on is an act somebody performs, reviews and can point to.
| Claim | Proved by |
|---|---|
| A policy naming the clipboard channel turns it on | session_link::clipboard_switch_tests::a_policy_naming_the_clipboard_turns_it_on |
| A broad policy with no channel scope does not | session_link::clipboard_switch_tests::a_broad_policy_with_no_channel_scope_does_not |
| No policies means off | session_link::clipboard_switch_tests::no_policies_means_off |
| Removing the policy turns it off again | session_link::clipboard_switch_tests::removing_the_policy_turns_it_off_again |
| The clipboard and browser switches are independent | session_link::clipboard_switch_tests::the_two_switches_are_independent |
| Every state that is not live says why | session_link::clipboard_switch_tests::every_state_that_is_not_live_says_why |
| The channel is live only when both halves are true | session_link::service::tests::the_clipboard_channel_is_live_only_when_both_halves_are |
| A copy is dropped if the tenant switched it off | session_link::service::tests::a_clipboard_is_dropped_when_the_tenant_switched_it_off |
| The switch is a policy naming the channel, not a checkbox | the_switch_is_a_policy_naming_the_channel_and_not_a_checkbox (agent/core/tests/clipboard_flow.rs) |
The second row is the one that matters most and is easiest to get wrong. Somebody writing "block card numbers leaving this company" with an empty channel scope has not asked to read everybody's clipboard, and the code treats an empty scope as "no constraint" everywhere else. This is the one place that reading would be indefensible, and it is tested.
The drop-if-switched-off row is the other half: the switch is the tenant's, and a session agent that ignores its Welcome and reports anyway is not trusted to honour it. The agent drops the report rather than believing the peer.
2. Reading, without becoming a keylogger
| Claim | Proved by |
|---|---|
| The same copy is reported once | clipboard::tests::the_same_copy_is_reported_once |
| Copying something else and back is two copies | clipboard::tests::copying_something_else_and_back_is_two_copies |
| Switching reporting on does not report what was already there | clipboard::tests::switching_reporting_off_and_on_does_not_report_what_was_already_there |
| An image on the clipboard is its own answer, not nothing | clipboard::tests::an_image_on_the_clipboard_is_its_own_answer_not_nothing |
| A platform that cannot read a clipboard says why | clipboard::tests::a_platform_that_cannot_read_a_clipboard_says_why |
| A bitmap is an image, and every other format only not text | clipboard::tests::a_bitmap_is_an_image_and_every_other_format_is_only_not_text |
| A copied image is a fact, recorded only under a policy naming the clipboard, audit only, once a minute | nothing_sensitive_leaks::a_copied_image_is_recorded_as_a_fact_only_where_a_policy_names_the_clipboard, runner::tests::an_image_copy_is_only_monitored_where_a_text_copy_would_warn, runner::tests::an_image_copy_is_recorded_at_most_once_a_minute |
The third row is a privacy property rather than a correctness one: turning the switch on must not sweep up whatever happened to be on the clipboard from before, which the person copied under no policy at all.
The distinction between rows one and two is what makes this a clipboard monitor and not a sampler. Reporting changes means a person who copies the same thing twice produces two copies; a naive "poll and compare to last" would report one.
3. The claim the whole feature rests on
From runner.rs, in the branch where a copy matched nothing:
"Nothing sensitive was copied. The content is dropped here and never reaches the queue, the disk or the network - which is the difference between a DLP agent and a keylogger."
agent/core/tests/clipboard_flow.rs proves a copy crosses the channel and that a harmless one matches no detector, and its own comment admits the limit: "the assertion here is only the first half". It cannot construct a running agent, so it cannot follow the content past the scan.
tests/clipboard_privacy.sh is the second half (DLP-127). A real agent, a real published policy naming the clipboard, a real session connection, two copies - one harmless, one carrying a card number - and then:
| Claim | How |
|---|---|
| Exactly one clipboard event reaches the server | Counted in the database, scoped to the run's tenant. One says two things at once: the sensitive copy produced an event, and the harmless copy did not |
| The stored event carries no copy of the matched value | The event row is searched for the card number |
| The harmless copy is in no file the agent owns | Every file under the state directory, and the agent's log, searched for the words that were copied |
| The matched value is in no file either | The same search, for the card number, which reached the queue as a redacted preview and must not have reached it as itself |
That third row is the one that catches the regression nobody means to write: a tracing::debug!(content = %report.content) added while chasing something else would put every copy anybody makes into a log file, and every test in the repository would still pass.
Two things went wrong while writing it, both worth recording, because both would have made it a test that passed without proving anything:
- The session channel opens before the first check-in, with reporting off, because no policy has arrived yet. Adopting the bundle replaces the server - that is how the switch reaches a connected session agent - so a client that connects too early is told the clipboard is off, has its copies dropped, and is disconnected. The test now waits for the bundle, not for the socket.
- Counting clipboard events without scoping to the tenant inherits every event any previous run left behind. The e2e database is shared between scripts.
4. What is absent
| Capability | State |
|---|---|
| Reading a clipboard on Linux | Absent. dlp-agent session connects and runs, and there is nothing behind it: reading needs an X11 or Wayland client. The subcommand says so and reports nothing. DLP-180 |
| Preventing a paste | Absent, and it is not obviously possible - DLP-182 is the spike asking whether a paste can be prevented at all |
| Attributing a copy to the application it came from | Built: the clipboard owner the session agent captured (pid and start time) is resolved by the service as SYSTEM, checked against its start time, and that verified name is what an application-scoped policy is evaluated against (DLP-181, DLP-1132). The session agent's own foreground_app is never believed, and with no verified owner such a policy matches nobody. Not yet run on a Windows desktop |
| A clipboard with a real interactive session on Windows | Never run. DLP-1176 |
5. What is not covered
- Nothing here has been run with a person at a keyboard. The Windows pipe, the handshake and the clipboard reader compile for Windows, and the CI job written to exercise them on a real Windows kernel has not yet passed: its pipe tests fail on the runner (
DLP-1489, Windows CI status). Even that job could not supply a desktop session and somebody copying something. The capability follows the live connection, so an endpoint where this has not happened reportsclipboardas unavailable rather than claiming it. tests/clipboard_privacy.shdrives the channel directly, over the unix socket, rather than throughdlp-agent session- because the session agent reads a real clipboard, and a CI runner has none. It tests the agent's half of the contract, which is the half the privacy claim lives in.- Image content is detected and not read. An image on the clipboard is reported as its own answer. Since
DLP-1168it is recorded as an audit event (destination.kind = "clipboard_image"), under a policy that names the clipboard channel, at most once a minute. OCR of it would beDLP-25.