Appearance
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
| Claim | Proved by |
|---|---|
| A USB stick is recognised | removable::tests::a_usb_stick_is_recognised |
| sysfs is read the way a stick actually presents itself | removable::tests::sysfs_is_read_the_way_a_usb_stick_presents_it |
/proc/mounts is parsed, including escaped spaces | removable::tests::proc_mounts_is_parsed_including_escaped_spaces |
| A truncated mount line is skipped, not panicked on | removable::tests::a_truncated_mount_line_is_skipped_not_panicked_on |
| The longest matching mount wins | removable::tests::the_longest_matching_mount_wins |
| A partition number is stripped to find its disk | removable::tests::a_partition_number_is_stripped_to_find_its_disk |
| …and an NVMe or MMC partition keeps its device name | removable::tests::an_nvme_or_mmc_partition_keeps_its_device_name |
| Scanning a real host returns something sane | removable::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:
| Claim | Proved by |
|---|---|
| A memory card is a memory card and not a USB stick | removable::device_class_tests::a_memory_card_is_a_memory_card_and_not_a_usb_stick |
| An optical drive is an optical disc, not a stick | removable::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 removable | removable::device_class_tests::an_optical_filesystem_wins_over_a_drive_type_that_says_removable |
| A phone over MTP is seen at all | removable::device_class_tests::a_phone_over_mtp_is_seen_at_all |
| …and recognised by path when the filesystem is generic | removable::device_class_tests::a_phone_is_recognised_by_the_path_when_the_filesystem_is_generic |
| A camera over PTP counts too | removable::device_class_tests::a_camera_over_ptp_counts_too |
| A colleague's SMB share under gvfs is not somebody's phone | removable::device_class_tests::a_colleagues_smb_share_under_gvfs_is_not_somebodys_phone |
| A fixed disk at a media path is still not removable | removable::device_class_tests::a_fixed_disk_at_a_media_path_is_still_not_removable |
| A USB disk that enumerates as fixed is still removable | removable::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 unclassified | removable::device_class_tests::a_windows_volume_is_classed_by_its_storage_bus |
| Virtual filesystems are never removable | removable::tests::virtual_filesystems_are_never_removable |
| An unclassifiable Windows volume is not guessed at | removable::device_class_tests::an_unclassifiable_windows_volume_is_not_guessed_at |
| Removable-with-nothing-more-to-say admits it | removable::device_class_tests::removable_with_nothing_more_to_say_admits_it |
| A Windows volume says why it was reported | removable::device_class_tests::a_windows_volume_says_why_it_was_reported |
| The read-only flag is read rather than assumed | removable::device_class_tests::the_read_only_flag_is_read_rather_than_assumed |
| Every class names a channel and a wire name | removable::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.
| Claim | Proved by |
|---|---|
| A burst of local files costs one rescan, not one each | removable::mount_cache_tests::a_burst_of_local_files_costs_one_rescan_not_one_each |
| A file on a known device resolves without rescanning | removable::mount_cache_tests::a_file_on_a_known_device_resolves_without_rescanning |
| A refresh is unconditional, because a removal produces no miss | removable::mount_cache_tests::a_refresh_is_unconditional_because_a_removal_produces_no_miss |
| The longest match still wins through the cache | removable::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
| Claim | Proved by |
|---|---|
| No portable devices are reported where they are not enumerated | portable_devices::tests::no_portable_devices_are_reported_where_they_are_not_enumerated |
| The reason this is not a channel says what is missing | portable_devices::tests::the_reason_this_is_not_a_channel_says_what_is_missing |
| A device is named by the first thing it actually supplied | portable_devices::tests::a_device_is_named_by_the_first_thing_it_actually_supplied |
| A blank name is not a name | portable_devices::tests::a_blank_name_is_not_a_name |
| A device with nothing at all still describes itself | portable_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/mountscontent.DLP-1176is 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 thewindows-agentCI job written to exercise them has not yet passed onwindows-latest(Windows CI status). - Bluetooth and printers are
DLP-91, not this document.