Skip to content

Device control, claim by claim ​

WIN-DLP-EPIC-012 asks to discover, classify and govern removable devices, because the USB stick remains the simplest exfiltration path in any organisation.

Two of the three verbs are done, and done unusually well: 47 tests across discovery and classification. The third is half done. A policy can name a device (DLP-269, §3), and the product still cannot block a write, which is known and ticketed. This paragraph used to say no policy could name a device. That stopped being true with DLP-269, and §3 below said so while this sentence did not.

Implementation: agent/core/src/removable.rs, with agent/core/src/portable_devices.rs for what is attached but not a filesystem.


1. Discover ​

ClaimProved by
A USB stick is recognisedremovable::tests::a_usb_stick_is_recognised
sysfs is read the way a stick actually presents itselfremovable::tests::sysfs_is_read_the_way_a_usb_stick_presents_it
/proc/mounts is parsed, including escaped spacesremovable::tests::proc_mounts_is_parsed_including_escaped_spaces
A truncated mount line is skipped, not panicked onremovable::tests::a_truncated_mount_line_is_skipped_not_panicked_on
The longest matching mount winsremovable::tests::the_longest_matching_mount_wins
A partition number is stripped to find its diskremovable::tests::a_partition_number_is_stripped_to_find_its_disk
…and an NVMe or MMC partition keeps its device nameremovable::tests::an_nvme_or_mmc_partition_keeps_its_device_name
Scanning a real host returns something saneremovable::tests::scanning_this_host_returns_something_sane

The NVMe row is the sort of thing that is only in a test because it once was not: nvme0n1p3 strips to nvme0n1, not to nvme0n, and the naive rule that works for sda3 gets it wrong.

2. Classify ​

Sixteen tests on nothing but telling one kind of device from another, and the list reads like a catalogue of things that go wrong:

ClaimProved by
A memory card is a memory card and not a USB stickremovable::device_class_tests::a_memory_card_is_a_memory_card_and_not_a_usb_stick
An optical drive is an optical disc, not a stickremovable::device_class_tests::a_windows_optical_drive_is_an_optical_disc_not_a_usb_stick
An optical filesystem wins over a drive type that claims removableremovable::device_class_tests::an_optical_filesystem_wins_over_a_drive_type_that_says_removable
A phone over MTP is seen at allremovable::device_class_tests::a_phone_over_mtp_is_seen_at_all
…and recognised by path when the filesystem is genericremovable::device_class_tests::a_phone_is_recognised_by_the_path_when_the_filesystem_is_generic
A camera over PTP counts tooremovable::device_class_tests::a_camera_over_ptp_counts_too
A colleague's SMB share under gvfs is not somebody's phoneremovable::device_class_tests::a_colleagues_smb_share_under_gvfs_is_not_somebodys_phone
A fixed disk at a media path is still not removableremovable::device_class_tests::a_fixed_disk_at_a_media_path_is_still_not_removable
A USB disk that enumerates as fixed is still removableremovable::tests::a_usb_disk_that_enumerates_as_fixed_is_still_removable
An internal fixed disk or a network drive is not reported at all, and a USB-bus fixed disk is (DLP-1128)removable::device_class_tests::an_internal_fixed_disk_or_a_network_drive_is_not_reported_at_all
A Windows volume is classed by its storage bus: USB, SD/MMC, or unclassifiedremovable::device_class_tests::a_windows_volume_is_classed_by_its_storage_bus
Virtual filesystems are never removableremovable::tests::virtual_filesystems_are_never_removable
An unclassifiable Windows volume is not guessed atremovable::device_class_tests::an_unclassifiable_windows_volume_is_not_guessed_at
Removable-with-nothing-more-to-say admits itremovable::device_class_tests::removable_with_nothing_more_to_say_admits_it
A Windows volume says why it was reportedremovable::device_class_tests::a_windows_volume_says_why_it_was_reported
The read-only flag is read rather than assumedremovable::device_class_tests::the_read_only_flag_is_read_rather_than_assumed
Every class names a channel and a wire nameremovable::device_class_tests::every_class_names_a_channel_and_a_wire_name

Two of those pull in opposite directions and both are right. A USB disk that enumerates as fixed is still removable - because a large external drive lies about itself. A fixed disk at a media path is still not removable - because an internal disk mounted under /media is still internal. Getting one of those right at the cost of the other is the usual failure, and neither is inferred from the other.

And a privacy one: a_phone_is_never_reported_as_identifiable. A personal phone attached to a work laptop is a person, and the product declines to build an identifier for it.

Mount caching ​

Resolving a path to its device reads /proc/mounts and sysfs; a burst of file events would do that per file.

ClaimProved by
A burst of local files costs one rescan, not one eachremovable::mount_cache_tests::a_burst_of_local_files_costs_one_rescan_not_one_each
A file on a known device resolves without rescanningremovable::mount_cache_tests::a_file_on_a_known_device_resolves_without_rescanning
A refresh is unconditional, because a removal produces no missremovable::mount_cache_tests::a_refresh_is_unconditional_because_a_removal_produces_no_miss
The longest match still wins through the cacheremovable::mount_cache_tests::the_longest_match_still_wins_through_the_cache

The third is the subtle one. A cache refreshed only on a miss never notices a device being unplugged, because an absent device produces no lookup to miss.

3. Govern - one gap, recorded ​

Enforcement is absent and recorded. The capability matrix says monitor-only for RemovableStorage on every platform, and Known limitations says a write to a stick is reported with the device named and not prevented. Until DLP-1117 that held only for a volume whose root was a watched path when the service started: the file watcher was built once and never rebuilt, so a stick inserted afterwards was not watched. Now the watch set is compared with what is asked for every few seconds and the watcher is rebuilt when it changes, and every writable removable volume present is watched whenever a policy can reach the removable-storage channel (DLP-1119). A stick inserted while the agent runs is watched within seconds. Read-only volumes, discs and phones are not watched: nothing the file watcher reports is a burn or an MTP transfer.

Windows can deny the write, and the console says where it does (DLP-1149). SPIKE-014 decided that blanket write control is the customer's Group Policy (Deny_All, Deny_Write on removable disks, or BitLocker's RDVDenyWriteAccess), enforced by Windows with none of MyDLP's code in the path, and that the agent never sets it. The agent reads it at check-in, and each endpoint and the Endpoints page's USB write control card show which endpoints it reaches, with the policy values as evidence. It is the configured machine policy: a per-user policy and the remount lag are not visible to the service, and the card says so. DLP-179 is the spike that decides how write control should work; DLP-6 is the capability.

A policy can name the device. Until DLP-269 it could not: the policy conditions offered detectors, categories, the boolean combinators, counts and file properties, and none of them was the identity of the removable device a file was going to, although the agent captured its serial. A rule could say "card numbers on the RemovableStorage channel" and could not say "...unless it is one of the encrypted sticks we issued".

ConditionNode::RemovableDevice (packages/contracts/src/policy.rs) now matches a device by serial and by class, as any_of or none_of the listed devices. Since DLP-903 a condition can also follow the approved-device list on the Device control page (include_approved_inventory): the server expands the list into serials when it compiles the signed bundle, so an endpoint evaluates plain serials and never needs the inventory. The policy editor authors the condition, including that choice (DLP-905).

So "a card number went to an unapproved device" can be the event, not "a card number went to a device". It is still an event and not a block: the enforcement gap above is unchanged.

4. Attached but not a filesystem ​

ClaimProved by
No portable devices are reported where they are not enumeratedportable_devices::tests::no_portable_devices_are_reported_where_they_are_not_enumerated
The reason this is not a channel says what is missingportable_devices::tests::the_reason_this_is_not_a_channel_says_what_is_missing
A device is named by the first thing it actually suppliedportable_devices::tests::a_device_is_named_by_the_first_thing_it_actually_supplied
A blank name is not a nameportable_devices::tests::a_blank_name_is_not_a_name
A device with nothing at all still describes itselfportable_devices::tests::a_device_with_nothing_at_all_still_describes_itself

The distinction this draws is the one the CI job also checks: presence detection is not channel coverage. A phone attached to an endpoint is a different fact from the mobile_device channel being monitored, and the capability matrix refuses to conflate them - .github/workflows/ci.yml fails the Windows job if a portable device is ever reported as monitored.


5. What is not covered ​

  • Nothing here has been run against real hardware. Every test uses synthesised sysfs and /proc/mounts content. DLP-1176 is the ticket for a real stick, a real memory card and a real phone.
  • No Windows run by this audit. The Windows classification paths are cfg(windows), and the windows-agent CI job written to exercise them has not yet passed on windows-latest (Windows CI status).
  • Bluetooth and printers are DLP-91, not this document.

Statements here describe what is built and tested; planned work is marked as planned.