Appearance
Agent capability matrix
What the agent can actually do, per channel, per platform. This document exists because the most common failure mode in DLP products is a console that implies protection the endpoint is not providing.
Rule enforced in code: ChannelCapability::clamp() reduces every policy decision to the strongest action the endpoint supports on that channel, and the event records both the requested and the applied action. The dashboard counts clamped decisions under "actions the endpoint could not enforce". Nothing in the product can claim a block that did not happen.
Verify for yourself, two ways. The per-endpoint matrix is on the Endpoints page, sourced from AgentCapabilities reported at check-in. And on any endpoint, enrolled or not:
dlp-agent capabilities # or --jsonThe same report includes non-baseline policy_actions. Windows reports approval_required because its session agent can keep a supported operation blocked while the separate approval workflow runs. Other platforms report no such action until they implement that workflow. The server checks this field against the exact signed bundle before delivery; absence from older agents is not inferred as support (ADR-0054). This is a wire/evaluation compatibility declaration, not proof that every channel can hold an operation or that the separate request/status UX is complete; per-channel coverage remains authoritative.
That answers for the machine in front of you rather than for its platform, which is the distinction that matters once any channel is conditional.
Legend
| Column | Meaning |
|---|---|
| ⚠️ | The capability exists but is conditional. The endpoint reports at check-in whether it is actually running, and the console shows that endpoint's answer - not this table's |
| Monitor | The agent observes the operation and can raise an event |
| Warn | The agent can show the user a notice |
| Block | The agent can prevent the operation from completing |
| Quarantine | The agent can prevent it and move the file to protected storage |
Open enforcement on Linux (#387)
One channel can be prevented rather than only reported. It is the only one, and the conditions are worth stating plainly because a capability that is sometimes there is more dangerous than one that never is.
It is not a kernel component. fanotify in FAN_CLASS_CONTENT lets an ordinary userspace process deny an open(). ADR-0004 still holds: there is no kernel module, no minifilter, no driver to unload, and killing the agent ends enforcement with no reboot. SPIKE-001 measured all of this before any of it was built.
Three things must all be true, and the endpoint reports which of them are:
- a monitored path sets
enforce_opens- off by default, per path; - the agent has
CAP_SYS_ADMIN- without it the endpoint reports exactly that, in those words, because it is a deployment fix and not a policy one; fanotify_marksucceeded on at least one of those paths.
The claim follows the running process, not the build. The same binary on a host without the capability reports file_open as unavailable with the reason. This is the whole point of the matrix: a policy clamped to audit on an endpoint whose console row says "block" is the failure this document exists to prevent.
Detected is not monitored. Windows can say a phone is attached (#317) and cannot say a file went to it. Those are two sentences, and the matrix keeps them apart: mobile_device stays unavailable, the presence list sits under its own heading in dlp-agent capabilities, and its JSON carries "monitored": false next to every entry. A row that said "monitored" on the strength of presence detection would have an operator write a policy that never fires - which is the failure this whole document exists to prevent.
What it costs. About 6× on every open inside a marked directory - 89 µs median against 13 µs unguarded (SPIKE-001 §3). On a folder of documents that is invisible. On a build tree or a package cache it is seconds per command, which is why the server refuses to mark a system directory at all and why the flag is per path rather than per tenant.
How it fails. If a verdict takes longer than two seconds, a watchdog outside the decision path closes the fanotify group. The kernel then allows every open still waiting, the endpoint reports enforcement as withdrawn rather than as off, and the machine keeps working. Every internal error allows: nothing here denies because it is broken.
What it does not do. require_justification on this channel allows the open. There is nobody to ask - the process is suspended inside open() and there is no session on this path - and a prompt nobody can answer is a hang. The operation is recorded; the justification is collected by the path that can actually ask a person (#471).
Linux - implemented
Data discovery, filesystem events, health, policy sync and event delivery.
| Channel | Monitor | Warn | Block | Quarantine | Mechanism | Limitation |
|---|---|---|---|---|---|---|
| Discovery (data at rest) | ✅ | - | - | - | filesystem walk, symlinks not followed | Files above the size cap are reported as unscanned, not skipped silently |
| File create | ✅ | ❌ | ❌ | ❌ | inotify via notify | Blocking needs an LSM or fanotify permission hook; deliberately out of MVP scope |
| File modify | ✅ | ❌ | ❌ | ❌ | inotify | As above |
| File rename | ✅ | ❌ | ❌ | ❌ | inotify | As above |
| File delete | ✅ | ❌ | ❌ | ❌ | inotify; answered from the fingerprint cache | A deletion carries remembered categories and no preview; a file deleted before it was scanned produces nothing |
| Removable storage | ✅ | ❌ | ❌ | ❌ | /proc/mounts polling with sysfs device identification | Blocking removable writes needs the fanotify enforcer from SPIKE-001 |
| Cloud sync folder | ✅ | ❌ | ❌ | ❌ | folders declared in the agent configuration | Declared folders only on Linux. There is no provider discovery here: the Linux OneDrive and Dropbox clients keep their configuration in places that are neither documented nor stable across the third-party re-implementations people actually run, and a guess that is right on one distribution puts a cloud destination on the wrong files everywhere else. Recognising the destination is not preventing the write (DLP-189) |
| Archive creation | ✅ | ❌ | ❌ | ❌ | inotify | ZIP/deflate contents are inspected locally under the extraction limits |
| File copy | ❌ | ❌ | ❌ | ❌ | - | Observed as create-plus-read; not attributable as a copy without a kernel hook. The event does carry where else the same content was seen - see below |
| File open | ⚠️ | ⚠️ | ⚠️ | ❌ | fanotify FAN_OPEN_PERM, over the directories the bundle marks | Conditional - see below. Only where a monitored path sets enforce_opens and the agent has CAP_SYS_ADMIN. Nothing is quarantined: denying an open does not move a file |
| Phone or tablet (MTP/PTP) | ✅ | ❌ | ❌ | ❌ | MTP and PTP mounts read from /proc/mounts | Seen, not stopped. A phone on USB is not a block device - it is a FUSE mount - which is why it was previously invisible. Blocking a write needs the fanotify enforcer |
| Optical disc | ✅ | ❌ | ❌ | ❌ | iso9660 and udf mounts from /proc/mounts | Seen, not stopped. A burnt disc cannot be revoked once it leaves |
| Bluetooth | ❌ | ❌ | ❌ | ❌ | - | Requires a BlueZ D-Bus listener for OBEX transfers; not implemented |
| Clipboard | ❌ | ❌ | ❌ | ❌ | - | dlp-agent session connects and reports, but nothing reads an X11 or Wayland clipboard behind it (#317) |
| ❌ | ❌ | ❌ | ❌ | - | Needs a CUPS backend filter | |
| Browser upload | ⚠️ | ⚠️ | ⚠️ | ❌ | the managed browser extension, over native messaging | Conditional, and the only channel besides file_open that can actually stop something. Monitored and blocking only while a browser host is connected and a policy names the channel - the extension holds the upload open and does not send it until this endpoint answers, so a block here is real rather than a claim. Nothing is quarantined: the bytes arrive over a socket and there is no file on this endpoint to move. Host discovery exists on both platforms (DLP-236), but the extension remains manually loaded until the organisation supplies a signed published id (DLP-1203), and only Chrome and Edge are covered (DLP-1207 is Firefox). The row says which browsers - coverage is per browser, so a machine running Chrome with the extension and Firefox without it carries a partial_reason naming the gap rather than reporting the channel as wholly covered |
| Email attachment | ❌ | ❌ | ❌ | ❌ | - | Needs a mail gateway or client integration |
| Network share | ⚠️ | ❌ | ❌ | ❌ | inotify on mounted cifs, smb3, nfs and nfs4 paths | Conditional - the local side only. Monitored where a writable share is mounted, and the endpoint reports how many it is watching. A write by another client to the same share is invisible: inotify on a network filesystem reports what the local kernel did, and the protocol has no change notification a client is obliged to receive. Blocking needs the same write-side hook the removable row waits on |
Privacy impact. File paths and names are collected; the tenant privacy setting can hash file names while retaining the directory, which is what investigation actually needs. No file content leaves the endpoint, unless the organisation's Owner has switched full-file evidence on and a policy asks for the file; the copy is then sealed into the organisation's own bucket (ADR-0038, PRIVACY_MODEL §39).
Performance. Measured, not estimated: a full 21-detector scan sustains 13.9 MiB/s on the benchmark host (cargo bench -p dlp-detector-engine; see Testing for the method and the hardware). The fingerprint cache means an unchanged file costs a stat.
Windows - primary production target, monitoring implemented
The Windows agent compiles and reports capabilities. Blocking is not implemented and is not claimed. Warning is, since #317.
Telling the person at the keyboard
Most of what a DLP deployment actually prevents is prevented by people knowing they are being recorded. Until #317 nothing on any platform showed anybody anything: enforce() returned "applied" for a warn policy whenever the capability allowed it, and no capability did - so the branch was unreachable and the first platform to claim it would have reported "the user was warned" about a dialog nobody had written.
The agent runs as LocalSystem in session 0 and cannot draw on a desktop. Each signed-in person's session agent owns its notification route. Windows supplies the named-pipe client's kernel session identifier, so an event is sent only to the session that produced it. An event without that attribution, which is every file-system event, goes to the sole signed-in session when exactly one person is signed in and that session is active, and otherwise to nobody; it is never guessed from the active console (DLP-1125, DLP-1127).
Three properties worth knowing before writing a warn policy:
- It waits for a display receipt, not a person's answer. The service waits at most four seconds for that connection to report a toast/dialog or a withheld notice. Unconfirmed delivery is never recorded as shown. After an unanswered wait only that connection enters its silence state (
DLP-1355). - There is no console-session fallback. Warnings require the exact originating connection, including its generation; a reconnect cannot receive a queued warning from the retired connection or inherit its burst window. An unattributed file event warns only the sole signed-in person, when there is exactly one and their session is active (
DLP-1125). - Nobody logged in is not a failure. A scheduled task writing a file on an unattended server has no one to warn. That event carries
not_delivered, which is deliberately notfailed: a server estate is mostly unattended, and counting every one as a fault would put the estate into a failure figure and teach an operator to ignore it.
The message comes from the policy's user_message, because a sentence written by whoever wrote the rule is the one that makes sense in that organisation. It is refused and replaced with a neutral default if it names a detector, the word "detector", or a regex - a warning that names a detector teaches the reader how to avoid it, and one that quotes the match puts the sensitive value on screen.
Linux cannot do this yet and says so. Drawing on a desktop there needs a process in the user's session with their bus address - the same session agent the clipboard channel is waiting on - so a warn policy clamps to audit and the incident records that the endpoint could not warn.
CI compiles and tests this on Windows (#317). Until that job existed every Windows claim in this document was made from a Linux runner - and the Windows agent had in fact not compiled for two releases, because a field added to a struct reached the Linux arm of one function and not the Windows arm. Two gates now cover it: a cross-compile check on the lint job, which catches that class in seconds, and a windows-latest job written to build natively and run the agent crates' tests on the platform they describe. The second has never run: no GitHub-hosted job ran from 2026-09-15 to 2026-10-01 (DLP-1215), and none of the Windows jobs has passed since (Windows CI status), so the cross-compile is the gate that has actually been run.
| Channel | Monitor | Warn | Block | Quarantine | Mechanism | Limitation |
|---|---|---|---|---|---|---|
| Discovery | ✅ | - | - | - | filesystem walk | As Linux |
| File create/modify/rename/delete | ✅ | ⚠️ | ❌ | ❌ | ReadDirectoryChangesW; development B1 minifilter when live | The production watcher has no requestor session. A live B1 lab driver supplies verified requestor process/session attribution, but it is not production-packaged and has no deny callback. Without it, warning uses the sole active signed-in session or records not_delivered rather than guessing (DLP-1125, DLP-1127). A fallback deletion uses remembered categories and carries no preview |
| Removable storage | ✅ | ⚠️ | ❌ | ❌ | user-mode volume classification; development B1 volume callback when live | The development B1 observer can attribute the file operation but cannot block it. The production fallback has no originating session and is warned as the file-event row describes. The class follows the device's storage bus; an external USB disk Windows reports as fixed is included (DLP-1128) |
| Optical disc | ❌ | ❌ | ❌ | ❌ | Presence only, via GetDriveTypeW | The drive is identified, but the transfer is not monitored. Burn observation needs IMAPI session monitoring |
| Cloud sync folder | ✅ | ⚠️ | ❌ | ❌ | provider/configured roots; development B1 attribution when live | A write into a synced library is an upload with a delay, and the event names the provider and, only where the provider publishes it, personal or business. A live B1 lab driver may supply the requestor; the production fallback has no originating session. Recognising the destination is not preventing the write (DLP-189). The fallback warns in the sole active signed-in session or records not_delivered rather than guessing (DLP-1125, DLP-1127) |
| Archive creation | ✅ | ⚠️ | ❌ | ❌ | ReadDirectoryChangesW; development B1 attribution when live | ZIP/deflate contents are inspected locally under the extraction limits. A live B1 lab driver may supply the requestor; the production fallback has no originating session and uses the same non-guessing warning route (DLP-1125, DLP-1127) |
| File copy | ❌ | ❌ | ❌ | ❌ | - | Attribution needs a minifilter. The event does carry where else the same content was seen - see below |
| File open | ❌ | ❌ | ❌ | ❌ | - | Its own channel (file_open). The released Windows endpoint has no qualified live minifilter enforcement; the development B1 driver observes create/open completion but does not adjudicate it. Linux does this with fanotify - see ADR-0069 |
| Mobile device (MTP/PTP) | ❌ | ❌ | ❌ | ❌ | Presence only: Windows Portable Devices enumeration | The device is detected; the transfer is not. A file copied to a phone crosses the WPD COM data path and never touches the filesystem, so a filesystem minifilter cannot observe or adjudicate it. dlp-agent capabilities lists attached phones under their own heading. Customer-applied Group Policy can deny every write to the WPD class (DLP-179); content-aware coverage needs a separate WPD data-path integration and is absent |
| Bluetooth | ❌ | ❌ | ❌ | ❌ | - | Requires a Windows.Devices.Bluetooth listener for OBEX transfers; not implemented |
| Clipboard | ⚠️ | ⚠️ | ❌ | ❌ | dlp-agent session over a named pipe, polling GetClipboardSequenceNumber | Conditional. Monitored only while a session agent is connected and a policy names the clipboard channel. Every connected Windows session has a separate kernel-attributed route (DLP-1127); a policy that says warn tells only the person whose session reported the copy, through that session's toast, and the notice names no file. Nothing is prevented; paste prevention was refused (SPIKE-013). Pure routing is unit-tested; two real concurrent Windows sessions have not been run because hosted CI has no interactive desktop (#317) |
| ⚠️ | ❌ | ❌ | ❌ | FindFirstPrinterChangeNotification + EnumJobsW, out of process (DLP-1141, SPIKE-011) | Conditional: monitored only while the job watcher is running, which the endpoint reports. Job metadata only - document name, printer, the submitting account, pages, copies, duplex - and never the document's content, so a content rule on the print channel cannot fire; a rule on who printed what, where, can. Never blocks: a cancel from outside the spooler is racy (SPIKE-011 §4). Nothing is loaded into spoolsv.exe. Not yet run on a Windows host | |
| Browser upload | ⚠️ | ⚠️ | ⚠️ | ❌ | the managed browser extension, over native messaging | Conditional. Windows writes the host manifest and machine-wide Chrome, Edge and Chromium registration, and Firefox's when a Firefox add-on id is configured (DLP-1207; not yet run in a managed Firefox), and its pipe serves browser hosts beside the session agent (DLP-236). The row becomes monitored and blocking only while a host is connected and a policy names the channel. A signed, published extension id is still external to this repository (DLP-1203), and coverage is limited to browsers carrying it (DLP-1207) |
| Email attachment | ❌ | ❌ | ❌ | ❌ | - | Needs an Outlook add-in or mail gateway |
| Outbound connections | ❌ | ❌ | ⚠️ | ❌ | user-mode WFP filters at ALE_AUTH_CONNECT_V4/_V6, in a dynamic session | Blocks connections, not content (ADR-0057, DLP-2072). A connection policy keeps named applications from connecting, by address and port. Nothing is read from the connection. The filters lapse with the service, which fails open, and health says when they are not in force. Known bypass: an application is named by file name and found by path, in the system folders, PowerShell's and the PATH, so a copy elsewhere or a renamed one is not covered. A refused connection is a network_connection event naming the application and the address, at most one a minute per application and destination, with a notice to the sole signed-in person. Compiled and not yet run on a Windows host |
| Network share | ⚠️ | ❌ | ❌ | ❌ | a UNC path's own shape, and GetDriveTypeW/WNetGetConnectionW for letters mapped in the service's session | Conditional (DLP-1145). A write under a monitored root written as \\server\share, or under a letter mapped for the service itself, is network_share, with the share named. \\tsclient\… is a Remote Desktop client drive. A person's own mapped drive is not seen: mappings belong to a logon session and the service runs as LocalSystem. The row follows the shares the agent knows. Pure logic unit-tested; not run against a real SMB share on Windows by this audit |
Classification metadata (DLP-191)
| Platform | Read agent/customer labels | Apply agent label | Boundary |
|---|---|---|---|
| Linux | ❌ | ❌ | No provider-specific label reader; the agent does not guess from paths |
| Windows | ✅ | ✅ | Reads MyDLP.Label, MSIP_Labels and OOXML custom properties; writes only MyDLP.Label and never overwrites customer/Purview labels |
Cloud placeholders are reported, not downloaded
A file in a OneDrive or SharePoint-synced library may be a placeholder: its metadata is on disk and its content is not. Reading it downloads it. A discovery scan over a synced library therefore does not read a directory - it downloads a document library, one file at a time, over whatever link that endpoint has.
So the agent reads the file attributes before the content and refuses to open anything carrying FILE_ATTRIBUTE_RECALL_ON_OPEN, FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS or FILE_ATTRIBUTE_OFFLINE. Such a file is reported as not scannable with the reason, and is never counted as clean - the same rule that already applies to a file over the size cap and to a format the endpoint cannot parse.
Two consequences an operator needs before pointing a policy at a synced folder: a mostly-dehydrated library produces mostly not_scannable rows, visibly; and that content is scanned once something else hydrates it, because a hydration is a write, which is an event. The reasoning is in .
Where a synced library is watched, it is said everywhere the watched paths are (DLP-1523). dlp-agent status (text and --json sync_roots) and doctor list it from the summary the running service publishes, whoever runs them; the About page lists it; and the health report carries it to the console's endpoint page. The path the console gets has the profile folder written as %USERPROFILE% (C:\Users\ahmad\OneDrive becomes %USERPROFILE%\OneDrive), so it learns that the person's OneDrive is watched and not their account name. A library outside <drive>:\Users\ is sent as it is.
The matrix is only true for builds it has been tried on
A Windows endpoint reports its real build - read with RtlGetVersion, which is not subject to the compatibility shim that makes GetVersionExW answer 6.2 on every modern build - together with a verdict: supported, untested, unsupported or undetermined. A build newer than anything this agent has been run on reports untested rather than supported, and a build whose version cannot be read reports undetermined rather than defaulting to something reassuring. dlp-agent capabilities prints it above the table it qualifies.
Not yet verified on Windows. The agent has been exercised end to end on Linux. The Windows binary is cross-compiled; the Windows CI job that would build it natively and run enrollment to incident has not yet passed on windows-latest (Windows CI status). Recorded in Known limitations.
How it runs. As a Service Control Manager service, started by dlp-agent.exe service - the entry point that registers a ServiceMain and a control handler (#375). dlp-agent.exe run is the same loop as a console process, kept for diagnostics; it cannot be used as the service binary, because a console process never reports SERVICE_RUNNING and the SCM kills it at the start timeout. MyDLPAgent.msi installs it with delayed automatic start, recovery actions and an Application event log source. A stop, a shutdown and a pre-shutdown all drain the event queue and release the run marker, so an ordinary stop is not reported as interference. A stop is seen between files rather than between batches, and reads nothing on its way out: the files a batch had not reached, and those still being written, are listed in the state directory and read first by the next start. The queue gets five seconds to deliver and is otherwise kept for the next run. A run ended after it was asked to stop but before it finished is reported as a stop that was cut short, not as a crash (DLP-1672). One file already being read is still read to the end, so a stop can wait as long as that file's scan. The lifecycle and MSI upgrade path are written for one hosted Windows CI image and have not yet passed there (Windows CI status); fleet and reboot limits are in Known limitations §2.
Privilege context. The agent reads TokenElevation off its own process token (OpenProcessToken + GetTokenInformation, no privilege beyond the token itself) and records it as UserContext.elevated. Integrity level and AppContainer state are not read. Where elevation cannot be determined the field is absent, not false: see Known limitations §5.5 for what the risk model still does with an absent value.
macOS - not implemented, and now scoped
Contract exists (OsFamily::MacOs); no implementation. SPIKE-007 scoped what one would take and found the expected long pole is the wrong one.
The Endpoint Security entitlement is not on the path to partial support. ESF is the blocking mechanism - the macOS equivalent of a minifilter or fanotify permission events - and everything in this product's monitoring set is reachable without it.
Full Disk Access is the real gate. macOS TCC denies any process, however signed, from reading ~/Documents, ~/Desktop, ~/Downloads and Mail until it is granted, and the agent cannot tell that it is being denied rather than finding nothing. "We scanned and found nothing" and "we were not allowed to look" are the same silence, which is the failure this document exists to prevent. It is not an entitlement and Apple does not review it: it is a PPPC payload in an MDM profile, which a managed fleet has and an unmanaged Mac cannot.
| Channel | Mechanism if built | Needs Apple's approval |
|---|---|---|
| Discovery | filesystem walk | no - but useless without Full Disk Access |
| File create/modify/rename/delete | FSEvents, file-level flags | no |
| Removable storage | DiskArbitration | no |
| Cloud sync folder, archive creation | FSEvents | no |
| Clipboard | NSPasteboard.changeCount, in a session agent | no |
| Warn the user | UNUserNotificationCenter | no |
| Service lifecycle | launchd daemon plus a LaunchAgent | no |
| Anything blocking | Endpoint Security Framework | yes, reviewed, may be declined |
Same shape as Windows: everything monitors, nothing blocks, and the blocking mechanism is the one with a gatekeeper in front of it.
What an endpoint reports today. Discovery available with the Full Disk Access caveat; every other channel unavailable, naming what it would take. It previously reported file_create and file_modify as monitored with the limitation not implemented - both at once, and the console reads the capability first, so a Mac would have shown a green monitoring cell for a channel no code watches. Fixed, and pinned by a test that no channel on any platform may claim monitoring while its reason says the thing does not exist.
Why no Windows kernel component
A minifilter is what would turn Windows filesystem monitoring into pre-operation, content-aware blocking. No Windows driver is built, deliberately. Linux's fanotify open enforcer is a userspace mechanism and does not change this decision:
- A defect in a kernel component can render a fleet unbootable. The blast radius of a userspace agent bug is one process.
- A distributable filter driver requires an EV-backed signing identity and the full Hardware Compatibility Program: an HLK lab, testing and submission per supported Windows release. Attestation is not a distribution path for filter drivers on Windows Server 2016 and later.
- It requires a separate threat model - the driver runs with more privilege than anything else the product ships - plus a staged rollout and a tested safe-mode recovery path.
- The security review surface grows by an order of magnitude.
The trade is explicit: Windows detects and records the filesystem channels above and prevents no file operation. An organisation that needs Windows prevention today should know that before deploying, which is why it is stated here, in the capability matrix on every endpoint page, and in the incident record for every clamped decision.
What the vendors this product is compared with do, what a minifilter would unlock and what it would cost are set out for the owner in ADR-0037 (Proposed).
If named customer demand triggers the work in DLP-188, it gets its own design document and threat model and ships behind a feature flag with a staged rollout. ADR-0004, SPIKE-001 and SPIKE-004 record the decision, the missing prototype evidence and the distribution cost.
Whose file it is (DLP-169)
On a shared machine or a file server the owner changes what a finding means: a payroll spreadsheet in the finance director's own directory and the same bytes in a contractor's are different events.
The event carries the owner's SID in file.owner_sid, and the resolved principal name in file.owner_name where a lookup succeeded. The legacy file.owner display value remains for older consumers. The SID is the answer and the name is a convenience - a lookup crosses the network on a domain member and can fail, and when it does the SID is still complete and stable. A failed lookup reports the SID with no name, never a guess.
A policy's file_condition.owners list matches either field exactly and case-insensitively; it deliberately does not use substring matching. Events and incidents store the two identities separately, and the incident view shows both so a friendly name never erases the durable identity.
| Platform | Owner |
|---|---|
| Windows | ✅ GetNamedSecurityInfoW, needing only READ_CONTROL - reading who owns a file does not require the ability to read the file |
| Linux | ❌ not collected |
It never stalls a scan. Well-known SIDs - SYSTEM, Administrators, the service accounts - are answered from a table, which is most of what a scan meets and all of what a disconnected laptop meets. Everything else goes to a two-worker, 128-entry bounded queue away from the scan path. A scan waits at most 50 ms for the convenience name; a late answer fills the cache for later files while this event keeps the SID and an absent name. Lookups are cached by SID, including the failures: an unreachable domain controller makes every lookup slow, and retrying three hundred slow failures on each discovery pass is how a scan stops finishing. The cache is per-run, so a name that resolves once the controller comes back is picked up later in that run, or after the next service start.
The owner is a person, is declared in the privacy register, and is subject to the tenant's redaction settings like any other identifier. When privacy.collect_usernames is false, neither the owner name nor SID is stored server-side. It is not logged per file: a log line naming every file's owner is a record of who works on what.
Which device a file went to (DLP-178)
A drive letter is not an identity: E: is whatever was plugged in last. Neither is the volume serial GetVolumeInformationW returns - it changes when the stick is reformatted, so a policy keyed on it approves a different device tomorrow.
The agent asks the hardware. IOCTL_STORAGE_QUERY_PROPERTY against the volume returns the device's own vendor, product, serial and bus type, and the volume is opened with no access rights at all, because a standard query needs none and a handle onto a whole volume is a capability worth not having.
| Platform | Vendor / product | Serial | Bus |
|---|---|---|---|
| Windows | ✅ from the device | ✅ hardware serial, falling back to the volume serial | ✅ usb, sata, sd, nvme… |
| Linux | ✅ from sysfs | ✅ | ✅ |
A device that supplies no serial is not admitted by an approval list. Not every device yields one, and a list that silently admitted every unidentifiable device would read as an approval list and behave as a decoration.
A serial is firmware data, and firmware is attacker-controlled. Two devices can report one serial, and on hardware that permits it a serial can be rewritten. It identifies a device for convenience and is never an authentication - an allow-list keyed on it is a workflow control, not a security boundary. POLICY_MODEL.md says so where the rule is written.
Which process was behind an event (DLP-222)
A finding without a process is a finding nobody can act on. "A card number was copied" is an incident an analyst closes; "copied by 7z.exe, medium integrity, running as CONTOSO\alice, started four seconds earlier" is one they act on.
It depends entirely on whether a pid exists to look up, and the answer differs by channel rather than by effort:
| Channel | Process context | Why |
|---|---|---|
| Clipboard | ✅ full | The session agent runs on the person's desktop and reports the foreground window's pid; the agent resolves everything else itself, as SYSTEM, against the operating system |
| File create / modify / rename / delete | ⚠️ requestor process in a B1 lab; otherwise none | ReadDirectoryChangesW reports what changed and never who changed it. The development-only B1 minifilter captures the requestor PID, process generation and session while callback data is live, and the service verifies/enriches them. It is not production-packaged; without a live compatible driver the event stays explicitly unattributed |
| Discovery | ❌ none, and none is meaningful | A scan of data at rest has no process behind it; the file has been there |
| Removable storage | ⚠️ requestor process in a B1 lab; otherwise none | It arrives on a file event, so the row above applies |
| Browser upload | ⚠️ name only | The native-messaging host knows which browser connected and no pid |
An event with no process says so, in context_note. An empty process and a process that could not be read look identical in an incident queue and mean different things.
What is collected, from one handle opened with PROCESS_QUERY_LIMITED_INFORMATION and nothing more - no debug privilege is taken anywhere in this product: image path, file name, BLAKE3 of the executable, parent pid, start time, mandatory integrity level, user SID, and whether it is in an AppContainer.
Pid reuse is guarded, and it is the whole difficulty. Windows reuses pids quickly; an incident naming the wrong program is worse than one naming none, because somebody acts on it. A process is identified by pid and creation time together, every field comes off a single handle, and a mismatch leaves fields absent rather than stale.
The parent pid is advisory. It is reused and reaped like any other, so the process named may no longer be the one that started this. A lead, not an attribution.
No command line is collected, and there is no option to collect one. They routinely carry credentials - a connection string, a token on an msiexec line, a password typed into a script - and an option is a thing somebody turns on.
The publisher is established, where there is one. WinVerifyTrust answers whether the binary's Authenticode signature verifies, and the signing certificate's subject is read separately - only when it did. A certificate's subject is a string its holder chose, so taking one off a signature that failed would put an attacker's chosen words in the publisher column.
The verdict travels beside the name as signature, because the absence of a publisher is ambiguous on its own: unsigned, signed-but-broken and could-not-check all produce no name and are three different facts. Only invalid is worth attention by itself - somebody signed this and then it changed. unsigned is the ordinary state of most software, and a flag that fires on all of it is a flag nobody reads.
It is a fact about the file, never a judgement about it. A valid signature says the bytes have not changed since the publisher signed them. It does not say the publisher is trustworthy or the file is safe: malware is signed regularly, with stolen certificates and with bought ones. The console shows the publisher and an analyst decides what it is worth.
Verified once per binary, not once per event: the verdict is cached by path, size and modification time, because building a certificate chain - which may go to the network for a revocation list - is not affordable in the path of somebody pressing Ctrl+C.
Signing the agent's own artefacts is a different thing and is still DLP-1175: it needs a certificate this repository will never hold. Verifying somebody else's binary needs none, which is why this exists and that does not.
Where the content came from, which is not the same as a copy (DLP-49)
file_copy is unavailable on every platform and stays unavailable: observing a copy means correlating the read with the write, and that needs a kernel hook this product does not have.
What an event does carry is content_seen_at - other paths on the same endpoint where the same exact content was seen earlier. That is usually the fact a policy wanted: this document is now on a USB stick, and the same bytes were on the finance share an hour ago.
Three limits travel with it, and an incident must not imply otherwise:
- Exact content only. A copy that was edited, re-saved by an application or converted has a different hash and is invisible here.
- Seen at, not moved from. Two people saving the same template produce the same answer. It is evidence, not attribution.
- Only what this endpoint scanned. A file that came from a share nobody monitors has no origin to report, and an absent origin means nothing.
It costs nothing: the hash is already computed for the scan-skip cache, and the lookup is against that cache.
Known bypasses
Stated plainly, because a security product that hides its gaps is worse than one that has none.
| Bypass | Applies to | Mitigation today |
|---|---|---|
| Write to an unmonitored path | All channels | Monitored paths are policy-controlled; discovery scans find data at rest elsewhere |
| Change a great many files at once, so the watcher drops the burst | File create/modify/rename | On Windows, stable notify 8.2.0 does not report a dropped event at all - a buffer overflow arrives as zero bytes with ERROR_SUCCESS and every other error unwatches the directory without telling the caller (SPIKE-019, DLP-300). Upstream PR #964 fixes the callback in the 9.0.0-rc line, but production remains on the stable crate. The mitigation does not depend on being told: the agent re-walks its monitored paths on a schedule, hourly by default, so what was missed is found on the next pass rather than never. A file created and deleted inside that window is gone, and the health report says the paths were re-walked rather than that the gap was closed |
| Encrypt or compress before writing | Content detection | The agent flags high-entropy content and archive creation, and reports an encrypted ZIP entry, a password-protected Office document, a PDF that needs a password to open or an AES-encrypted 7z as coverage encrypted, which a policy can act on (DLP-1130, DLP-1297); encrypted contents are not inspected on the endpoint. A PDF with only an owner password opens without one and is read (DLP-1606) |
| Upload directly from a browser | Browser upload | Covered on Linux only where the managed extension is loaded and a policy names the channel - there the upload is held and can be stopped. Everywhere else, and in any browser without the extension, it is not covered and no claim is made: the endpoint compares the browsers that connected against the browsers actually running and reports the difference (DLP-1207) |
| Copy to removable media | Removable storage | Detected and recorded with the device identified; not prevented |
| Hide data in an alternate data stream | Content detection, NTFS only | Not read, and no longer unnoticed (DLP-166). The agent scans the default stream only - reading every stream of every file multiplies a discovery scan's I/O by whatever an attacker chooses to attach. Named streams are enumerated, reported as labels with their names and sizes, and the file is marked not fully scanned with how many bytes went unread. Zone.Identifier and Explorer's own streams are skipped by exact name, because they are on nearly every downloaded file and a column that is always full is one nobody reads - a stream called Zone.Identifier.payload is reported |
| Stop the agent as an administrator | All | The service restarts; absence is visible as a stale endpoint on the dashboard. Tamper protection is future work (DLP-33) |
| Boot from external media | All | Out of scope for endpoint DLP; disk encryption is the control |
| Screenshot the data | All | Not prevented, and not preventable by any endpoint agent - see SPIKE-016. The only supported mechanism, SetWindowDisplayAffinity, is available to an application for its own windows and has no cross-process form, so protecting another vendor's window would require injecting into it, which this product refuses. Remote desktop, capture below the compositor and a phone camera defeat any answer regardless. On Windows, where a policy names the clipboard channel, an image copied to the clipboard (where a screenshot usually lands) is recorded as an audit event, at most once a minute (DLP-1168). Its content is not read, and the agent never captures the screen itself - see Privacy model §40 |
What the agent will never do
Constraints, not omissions. These are refused by design:
- Keylogging
- Password or credential collection
- Credential interception
- Covert screenshots
- Hidden persistence
- Security-tool bypass or evasion
- Surveillance beyond what a published policy describes
An agent that did these would be indistinguishable from the threat it exists to detect. See Privacy model.
Classification elements, per channel (DLP-416)
The matrix above is about enforcement: what this endpoint can monitor, warn on, block and quarantine. This section is about evaluation: which typed elements of a classification a channel can decide at all.
The two are different questions and are kept apart deliberately. A channel that can be blocked may still be unable to decide a domain element, because a print job has no destination domain; a channel that cannot be blocked may decide every element of a classification perfectly well.
| Element | Where it cannot be decided |
|---|---|
detector / content | Nowhere - content is what every channel is inspected for. Where a body could not be extracted, the detector reports it and the element is indeterminate through the ordinary path. |
channel, coverage | Nowhere. |
application | Discovery and network-share scans: nobody acted, a scanner walked a share. |
path, file_property | Clipboard, print, Bluetooth: there is no file. |
domain | Anything but browser upload, e-mail attachment and cloud sync: there is no destination domain. |
label | Clipboard, print and Bluetooth (no file), and the server's own share and bucket scans, which read bytes and not NTFS alternate streams. |
An undecidable element is NotEvaluated, never a silent "no". A rule that needed three facts must never quietly become a rule about two - that is exactly how one definition comes to mean two things. An element that no supported channel can decide is refused at authoring instead, because the rule containing it can never match and a rule that can never match reads as coverage.
GET /api/v1/classifications/{id}/capabilities returns this for a specific definition, computed from the same function the runtimes use, so this table cannot drift from the code.
Visual markings, per format (DLP-409)
A label that says Confidential raises an expectation of a header, a footer or a watermark. The model for that ships in this build - versioned templates, a closed set of three interpolatable values, an idempotence marker and a rollback that can only remove MyDLP's own markings.
One case is marked: a Word document the Windows agent labels automatically. When the agent writes a governed label to a .docx and that label carries a marking, it writes the marking as the document's footer (DLP-1057). The template is the label's text, versioned by the governance revision, so a changed marking replaces the old footer rather than adding a second. A document is left alone, and the event says why, when it is:
- read-only, or over 50 MB;
- open in another program;
- not readable as a Word document;
- changed between being read and being replaced. The marked copy is checked for our signature, then swapped in with
ReplaceFileW, which keeps the original's ACL, attributes and named streams, includingMyDLP.LabelandZone.Identifier.
What has run: unit tests on real documents on Linux, and the same tests on the Windows build under Wine, including the ReplaceFileW swap and a document held open with no sharing. What has not: a Windows host, and so the claim that Zone.Identifier survives the swap, which Wine cannot show. Nothing else is marked. The label paths an administrator's plan or a deferred choice apply carry a label value, not its definition, and write no footer. The server never touches files. DLP-1054 corrected this section once already, when it said the footer was "Written" before anything called the renderer.
The table says what the renderer can write, which is the precondition for a format ever being applied:
| Format | What the renderer can write | Why |
|---|---|---|
docx footer | Written by the Windows agent, on automatic labelling | dlp-ooxml-marking, admitted when its golden documents passed (DLP-445); called by document_marking once a governed label is written (DLP-1057). Not yet run on a Windows host. |
docx header | Nothing | Its own part and its own golden documents; not written by this build. |
docx watermark | Nothing | A watermark is a drawing behind the text rather than a paragraph. |
xlsx, pptx | Nothing | Modelled but not written: a renderer that corrupts a customer's document is worse than an unmarked one, so each ships when its golden documents pass. |
pdf | Nothing | A PDF cannot be marked without re-rendering it, which changes bytes this product does not own. |
txt, csv | Nothing | Adding a line to a text or tabular file changes the data, not its presentation. |
| anything else | Nothing | Not a format MyDLP writes markings into. |
A format is added to that table in the change that makes its golden documents pass, never before - the same rule the classification channels follow, for the same reason. label::marking_support is the single place that decides, and dlp_ooxml_marking::writes delegates to it rather than keeping a second list, so this table and the renderer cannot drift apart.
What "golden documents" means here. Four fixtures in packages/ooxml-marking/tests/golden/, built deterministically by packages/ooxml-marking/tests/make_golden.py: a plain document, one already carrying another vendor's footer, one with two sections, and one that is not an OOXML package at all. They assert that a second application produces identical bytes, that a rollback leaves a foreign footer byte for byte and returns every other part to what it was, that both sections of a two-section document are marked, and that a file which is not a package comes back untouched with a stated reason rather than half-written.
Three values and no more. A marking template may interpolate {label}, {owner} and {date}. The set is closed because a template language that can reach arbitrary document facts can print somebody's file path, their user name or a matched value on every page of a document that is then e-mailed.
Encryption and rights management are a provider's job. ADR-0022 records why MyDLP will not implement document encryption in any form, which provider this integrates with when it is built, and what happens when that provider is unavailable: the endpoint reports that protection was not applied, with the reason. It never falls back to a visual marking and calls it protection.
Asking the person what a document is, per surface (DLP-408)
A detector says what it found; only the person writing the document knows what it is. A draft tender about to be published and a signed contract look identical to a pattern. The decision behind such a picker ships in this build - what it may show, what it must ask, when an answer is required and what travels back. No surface does.
| Surface | Offered today | Why |
|---|---|---|
| Explorer | no | a signed in-process COM shell extension; unsigned, it is a crash in every Explorer window on the machine |
| Word, Excel, PowerPoint | no | an Office add-in needs Office to build and test against, and a certificate to be loaded at all |
| Outlook (Win32, COM) | no | SPIKE-017 chose it for offline sends; the same signing requirement applies |
| Outlook (on-send web add-in) | no | needs a Microsoft tenant to sideload into |
A surface is added in the change that makes its end-to-end test pass, never before. DLP-446 carries that work and the three external resources it needs: a code-signing certificate, a licensed Office installation and a tenant.
What a prompt may carry. The file name, the current label, the recommendation, the category of the reason - "this document appears to contain material your organisation classifies" - and the tenant's own sentence about the consequence. Never a path, never a preview, never a matched value. This is the most tempting place in the product to show a snippet, because somebody deciding would genuinely find one useful.
A downgrade is a different act. Choosing something less restrictive than the recommendation, or than what the file already carries, requires a reason through the bounded justification path that already exists. Choosing something more restrictive does not: asking somebody for a reason to be careful teaches them that being careful is expensive.
An unranked label is applied, not guessed at. "Confidential" outranking "Internal" is a fact about a customer's scheme, not about the English language.