Skip to content

Observability, claim by claim ​

WIN-DLP-EPIC-025 asks for structured logs, event log integration, health, and a support bundle that leaks nothing - because an agent that cannot explain itself will not be trusted with enforcement.

All four exist. The support bundle was the last. For a while it did not exist, and two documents said it did, including the threat model, which credited it as a prevention. It was then built as dlp-agent support-bundle (§4). This sentence kept saying it did not exist after that.


1. Event log integration ​

Seven ids in three ranges - 1000 lifecycle, 2000 normal operation, 3000 something to look at - published, pinned by a test, and documented in . A reader filtering on >= 3000 gets everything worth an alert without knowing any individual id.

Covered in The Windows service, claim by claim §5 rather than repeated here. The row worth restating is that a test reads the published document and fails if an id is missing from it, so the code and the catalogue cannot drift apart.

2. Health ​

ClaimProved by
No verified bundle is Impaired, with the reasonrunner.rs health()
An expired bundle is Degradedas above
Detectors that would not compile are Degradeddetector_runtime::tests::a_detector_that_will_not_compile_is_dropped_and_reported supplies the input
A weakened state-directory ACL is Degradedstate_acl - 14 tests
Weak key protection is Degradedkeystore::tests::a_keystore_failure_is_not_the_expected_state_anywhere
A nearly-full queue is Degradedhealth(), against queue_capacity

And the local record an operator can read without a server:

ClaimProved by
History survives a restartreport::tests::history_survives_a_restart
It is bounded and newest-firstreport::tests::history_is_bounded_and_newest_first
A corrupt history file starts empty rather than failing the agentreport::tests::a_corrupt_history_file_starts_empty_rather_than_failing
Counts separate the outcomes that matterreport::tests::counts_separate_the_outcomes_that_matter
Notable outcomes exclude the boring onesreport::tests::notable_outcomes_exclude_the_boring_ones

That last one is the difference between a diagnostic and a log: a list that includes everything is one nobody reads.

What the agent costs the machine ​

AgentHealth carries cpu_percent and memory_mb. Both used to be f32 filled with a literal 0.0: nothing measured either, and every endpoint in every fleet reported that the agent used no CPU and no memory, on every check-in.

Zero is not "unknown"; it is a claim, and it was wrong in the flattering direction. It also broke a rule this codebase states as a test elsewhere - runner::tests::unknown_elevation_is_absent_from_the_wire_not_false asserts that an elevation the agent could not determine is absent from the JSON rather than serialised as false.

DLP-274 did both halves. The fields are now Option<f32> and skipped when absent, so "not measured" is expressible; and agent/core/src/resources.rs measures them, through sysinfo, which was already a declared dependency and already in the SBOM. Only this process is refreshed: enumerating every process on the machine to answer what the agent costs would make the measurement one of the more expensive things the agent does.

CPU is absent on the first check-in. It is a rate, and a rate from one observation is not a measurement, so the first sample reports memory and leaves CPU out rather than sending the 0.0 that sysinfo returns for it. That is the shape of the thing being measured, not a limitation.

This is what makes Performance, claim by claim's two idle budgets - under 0.5 % CPU and under 80 MB working set - checkable against a real fleet at all. Neither is checked automatically yet; DLP-204 is health and diagnostics as a feature.

3. Structured logs ​

tracing throughout, --log-json on the server, and two destinations on the endpoint with two filters (DLP-259).

Stderr belongs to whoever typed the command: quiet for one-shot commands unless --verbose was asked for, detailed for run. Under the Service Control Manager there is no console attached to it, which is why everything the agent said about itself used to be discarded as it was written - the seven Event Log ids were the whole of its account, enough to know the service started and not enough to answer "why did this file not raise an event".

The file - logs\agent.log under the state directory - belongs to whoever is asked that question afterwards, so it is always detailed: it is read days later or not at all, and one that recorded only warnings would answer nothing. It is bounded at five megabytes a file, four files and fourteen days, held under the state directory's ACL, and dlp-agent doctor prints the path and the bounds. RUST_LOG changes the console, not the file. A diagnostic command (scan <file> --explain, status, doctor, history, capabilities, verify-package, support-bundle) answers on the console and does not write the file or take part in its rotation (DLP-1748): an explain sweep on YUSIF put four lines per file into it and left a 441-byte rotated file between two full ones. A command that does something (a discovery scan, enroll, update) is still recorded in it, and can still rotate it at the moment the service does; that race is narrower, not gone.

The session agent's file - %LOCALAPPDATA%\MyDLP\session-logs\agent.log in the signed-in person's own profile, with the same bounds (DLP-1261) - is the other side of the session channel. The session agent runs as the person and cannot write the state directory. It records each connection's ending and what happens next ("nothing is listening on the session channel; connecting again in 8 s"), the welcome with both versions and the clipboard and activity switches, and a refusal with its reason. It records nothing a prompt, a toast or the clipboard carried.

What it may contain is PRIVACY_MODEL.md §6, and the short version is the event's own rule: paths, detector ids, decisions, counts and event ids, never a matched value. tests/agent_logging.sh asserts that by scanning a real card number and reading the log back.

Shipping these logs to the server is a different feature with a different privacy answer, and is not part of this.

4. The support bundle ​

dlp-agent support-bundle writes one JSON file - by default into the current directory, 0600, named for the device and the instant it was taken (DLP-204).

One readable file, not an archive, and that is the decision the rest follows from. An archive is opaque: nobody reads what they are about to attach to a ticket, and "it is probably fine" is how the interesting thing gets attached. A JSON document can be read before it is sent and grepped by the person sending it - and it can be asserted over in one test, which agent/core/tests/nothing_sensitive_leaks.rs does, searching it for a planted card number beside the log, the queue and the history.

What is in it: who asked and on what machine; the agent version; device id, tenant, group, key protection, enrolment and certificate dates; the policy summary; queue depth, capacity and both drop counters; the full health record; the capability matrix; the state directory and its ACL finding; the binary integrity verdict (DLP-199); the configuration; and the log files, described.

What is not, and the bundle says so itself - omitted is a field inside it, so a reader does not go looking for something that was never there and the next person adding a field has to decide against the list rather than around it:

  • the device private key, and every other secret. Never collected.
  • local history. Every entry names a file, and a list of the sensitive files on somebody's machine is the most damaging thing this could carry. An investigator who needs it reads it in the console, under the tenant's access control.
  • log file contents. The log is 0600 and names scanned paths; a log tail is precisely what somebody would paste into a ticket without reading it. The bundle gives each file's path, size and modification time, so an operator who wants one sends it deliberately and owns that.
  • configuration values that could identify anything.
  • anything a detector matched, in any form, redacted or otherwise.

Configuration: hashed, and where hashing stops being useful ​

A support engineer's real question is are these two endpoints configured the same, and a hash answers it without disclosing either. So server_url, every path and every file name is carried as the first sixteen hex characters of its hash.

Numbers and flags are carried in the clear, deliberately. A hash of 60 is a hash of a value from a set of a few hundred: anybody who wants it back has it in a second, so hashing protects nothing and destroys the only use the field had. insecure_skip_tls_verify is in the clear for a stronger reason - a true there is an endpoint that trusts any certificate, and that is the first thing an auditor should be able to read.

The field list is written out by hand rather than serialised from the config struct. That is the point: a new configuration field does not silently appear in a support bundle. Somebody has to decide which side of the line it is on.

5. What is not covered ​

  • No agent-side metrics endpoint, and none is wanted: the server's meta.rs makes the same argument for itself - "that would mean adding a metrics dependency for a single counter" - and an endpoint agent exposing a scrape target is a listening socket nobody asked for.
  • No test of the health mapping. The inputs to each rule are tested individually; the mapping from them to Healthy/Degraded/Impaired is read from runner.rs by inspection, because asserting it needs a constructed Agent. Same limitation recorded in Policy synchronisation, claim by claim §5.
  • Nothing has been read from a real Windows Event Log, beyond CI dumping it when a job fails. DLP-208.

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