Appearance
Print protection, claim by claim
WIN-DLP-EPIC-014 asks to observe and control printing, because paper leaves no audit trail once it is out of the building.
Observation is built on Windows (DLP-1141), and control is built nowhere. Linux has neither. The capability matrix an endpoint reports says exactly that: print is monitored on Windows while the job watcher runs, and never blocked.
The Windows mechanism is now decided (SPIKE-011, DLP-183): an out-of-process watcher using FindFirstPrinterChangeNotification and GetJobW, and nothing loaded into spoolsv.exe. The criterion decided it without a trade-off - the out-of-process API sees the same job metadata a print monitor would, so a monitor DLL is not safer-versus-richer but strictly worse. It also fixes what the channel can claim: metadata, never the document's content. What is built (agent/core/src/print.rs) reports each job once, when its spooling has finished and its page count is known, as an event with the document name, printer, the spooler's account for it, pages, copies and duplex, when a policy on the print channel applies. No cancel is attempted yet; the matrix does not claim one.
Printers added while the service runs (DLP-1460)
The watcher subscribes to local print-server add/delete notifications before enumerating queues. The server notification has the first wait slot, so busy job queues do not postpone inventory changes. A monotonic 30-second deadline also refreshes the inventory regardless of job traffic; if the server subscription is unavailable, the agent warns and uses this polling fallback. An empty inventory still waits on the server notification.
Every startup or inventory refresh enumerates already queued jobs after opening their notifications. The same reportable-job tracker deduplicates snapshots and later notifications. All 64 previously supported queues remain registered: with a server subscription, rotating 63-queue wait windows include the omitted queue in the next wait, with waits capped at one second. Synchronous spooler calls can still delay the thread; this is not a real-time latency guarantee. More than 64 queues retain the existing warning/limit.
This is not a lossless print journal. A job that completes and disappears before its queue is opened cannot be recovered by EnumJobsW. Event-log 307 cross-checking remains separate work, not silently enabled. No source document, spool content, cancellation, printer configuration or default-printer change is introduced.
Regression tests cover the fixed deadline, wait-handle budget/rotation and first-snapshot deduplication. The Windows calls require native acceptance: add an inert file-backed queue while the service runs, submit a retained job immediately, confirm one event before 30 seconds, repeat while another queue is busy, and restore the original default printer/remove fixture resources. Cross-compilation alone does not prove that native scenario.
What this audit found is not about print. It is that an operator can write a print policy, publish it, watch it compile into a signed bundle and reach every endpoint in the fleet, and nothing tells them it can never fire.
1. Absent, and honest about it
| Claim | Proved by |
|---|---|
| Print is reported unavailable on Linux, with the reason | platform.rs: "requires a CUPS backend filter; not implemented" |
| On Windows it is monitored while the watcher runs and never claims to block | platform::tests::print_is_monitored_only_while_the_watcher_runs_and_never_claims_to_block |
| A job is reported once, after spooling, and a reused id is a new job | print::tests::a_job_is_reported_once_its_spooling_is_done_and_never_twice, …a_job_id_reused_after_the_job_left_the_queue_is_a_new_job |
| A print event names the document, printer and person, matches a policy scoped to printers, and carries no content | nothing_sensitive_leaks::a_print_job_is_recorded_by_name_printer_and_person_and_never_its_content |
| Every unavailable capability explains itself | platform::tests::every_unavailable_capability_explains_itself |
| No channel claims blocking it cannot perform | platform::tests::no_channel_claims_blocking_it_cannot_perform |
| The matrix covers every channel, and none twice | platform::channel_coverage_tests::the_capability_matrix_covers_every_channel |
| Nothing warns about a channel it cannot even see | platform::tests::nothing_warns_about_a_channel_it_cannot_even_see |
| The published matrix says so too | Agent capability matrix, Print: ❌ ❌ ❌ ❌ on both platforms |
The two reasons being different is the detail worth keeping. "Not implemented" would be true and useless; "needs a CUPS backend filter" and "needs a print monitor" name the work, and they are not the same work.
Channel::Print is nonetheless a fully-formed channel: in Channel::ALL, with a wire name and a risk weight of 0.6. That is correct - a policy must be expressible before it can be implemented, and the alternative is a contract that changes shape every time a capability lands.
2. The gap this audit actually found
An operator can write "block card numbers on the print channel", publish it, and see it compiled into a signed bundle and delivered to every endpoint. It will never fire. The console, the API and the bundle all say nothing.
That is worse than having no policy, because somebody now believes printing is controlled.
It is the one place in this product where a capability claim goes uncorrected. Everywhere else the discipline is relentless: the endpoint clamps an action it cannot perform and records the clamp; the matrix explains every unavailable channel; a CI job fails the Windows build if a channel claims enforcement it does not have. Here the claim is made by the operator, and nothing answers.
And it is not only print - the same is true of email_attachment, bluetooth, mobile_device, optical_disc, and of clipboard on Linux.
The server already knows enough to say so. Every agent reports its full capability matrix at enrolment and on check-in, and enrollment.rs stores it in agents.capabilities, per device, with can_monitor, can_block and unavailable_reason per channel. GET /api/v1/endpoints/{id} reads it back. So "can any endpoint in this tenant observe the channel this policy names?" is a query, not a research project.
Filed as DLP-270, with the shape: warn, do not refuse - a policy written the day before a fleet upgrade is legitimate, and so is one written for a group that has not enrolled yet - and say it with the count, because "none of your 400 endpoints" and "none of your 2 endpoints" are different sentences.
3. What implementing print would take
Recorded so the next person starts from the question rather than the code.
| Platform | The extension point | The problem with it |
|---|---|---|
| Windows | A print monitor, or a Print Processor, or the XPS/EMF spool file | DLP-183 is the spike that decides which. A print monitor is a DLL loaded into the spooler service, which is a trust boundary and a stability risk of the same family as a kernel driver |
| Linux | A CUPS backend filter | Simpler, and reaches far fewer endpoints |
The content problem is the harder half on both: what the spooler holds is a rendered page, not the document. Detecting a card number in EMF or XPS means reading laid-out glyph runs rather than text, which is closer to OCR than to extraction - and the OCR decision is ADR-0012.
Printer control - blocking a job rather than observing it - is DLP-91, with Bluetooth and mobile storage.
4. What is not covered
- Native evidence is limited. DLP-1460 records a Windows reproduction of the pre-fix missed-job defect. The new topology subscription and rescan behavior still require the controlled native acceptance scenario above; host decision tests and Windows cross-compilation are not that evidence.
- No cancel, and no Linux. A cancel from outside the spooler is racy and is not attempted; Linux needs a CUPS backend.
- No measurement of how much printing matters to a customer, which is what would justify the spooler risk.
DLP-183should probably start there.