Skip to content

The policy runtime, claim by claim ​

WIN-DLP-EPIC-005 asks for three things - evaluate offline against the full input set, resolve conflicts deterministically, and explain every decision - and gives the reason to hold them to: an unexplained decision cannot be defended to an employee, an auditor or a court.

All three are built, and the engine is the best-tested part of this product: 126 unit tests in dlp-policy-engine. What this audit adds is the assertion that the explanations say something, which is the part a test can pass without.

Implementation: packages/policy-engine/src/evaluate.rs.


1. Evaluate offline, against the full input set ​

Evaluation is a pure function of one verified bundle, the detector results and an evaluation context. The runtime evaluates the bundle's classification definitions first, then passes those results to policy evaluation without a second lookup or mutable catalogue. Nothing in it contacts a server, which is what makes offline evaluation a property of the design rather than a feature.

ClaimProved by
An exact classification id/version/revision drives a decision and retains rule/element lineageclassification_binding_tests::an_exact_classification_revision_drives_the_policy_and_keeps_bounded_lineage, classification_policy_binding::a_verified_bundle_drives_a_windows_decision_with_exact_lineage
A newer revision is not silently substituted; unavailable evidence remains fail-safe through not and auditable through orclassification_binding_tests::a_stale_revision_is_not_substituted, …::not_cannot_turn_an_indeterminate_classification_into_a_match, …::a_true_or_branch_can_match_while_missing_classification_evidence_stays_auditable
A Windows decision with missing detector coverage contains bounded reason codes and no detector error textclassification_policy_binding::missing_detector_evidence_is_fail_safe_and_privacy_bounded
A matching detector triggers the policy; no hit means no actionevaluate::tests::matching_detector_triggers_the_policy, …::no_detector_hit_means_no_action
Boolean composition - and, or, not, at-least - worksevaluate::tests::boolean_composition_and_or_not_at_least
A bulk threshold catches what a per-detector one missesevaluate::tests::total_matches_threshold_catches_bulk_exfiltration
Scope restricts by department and channelevaluate::tests::scope_restricts_by_department_and_channel
File conditions filter by extension and sizeevaluate::tests::file_condition_filters_by_extension_and_size
A file-owner condition matches an exact SID or resolved name without guessing oneevaluate::tests::a_file_owner_condition_matches_the_sid_or_the_resolved_name_without_guessing
A slow owner-name resolution cannot hold the scanner past its wait budgetfile_owner::tests::a_slow_resolution_cannot_stall_the_scan_past_its_budget
Destination matching uses domain-suffix semantics, not substringevaluate::tests::destination_matching_uses_domain_suffix_semantics
An unvalidated match fails a require_validated conditionevaluate::tests::unvalidated_match_fails_a_require_validated_condition
Draft policies are not enforcedevaluate::tests::draft_policies_are_not_enforced
An effective period is honouredevaluate::tests::effective_period_is_honoured
A policy from another tenant never appliesevaluate::tests::cross_tenant_policy_never_applies
A disconnected endpoint uses the policy's offline responseevaluate::tests::offline_response_is_used_when_disconnected
Evaluation is fast enough for the endpoint's hot pathevaluate::tests::evaluation_is_fast_enough_for_the_endpoint_hot_path

One input the endpoint does not have, stated because it is an abstention rather than an answer. off_hours is a risk factor, and the tenant's working day is a tenant setting that the policy bundle does not carry. The endpoint therefore passes off_hours: false - "a deliberate abstention, not a claim that the event is within hours", in runner.rs's own words. It is a 0.03 weight, the server re-scores with its own inputs when it correlates, and the incident carries both scores. The 15 business_hours tests are about the server's answer, not the endpoint's.

2. Resolve conflicts deterministically ​

Three strategies, and the winner is chosen by a total order - action strength then priority, or priority then strength - so there is no tie to break arbitrarily and no dependence on the order the policies arrived in.

ClaimProved by
Most restrictive wins by defaultevaluate::tests::most_restrictive_wins_by_default
Highest priority overrides strength when askedevaluate::tests::highest_priority_strategy_overrides_strength
An exception suppresses a policy, and it is recordedevaluate::tests::exception_suppresses_the_policy_and_is_recorded
An exception can downgrade rather than suppressevaluate::tests::exception_can_downgrade_instead_of_suppress
An expired exception does not applyevaluate::tests::expired_exception_does_not_apply
An empty scoped exception is ignored rather than matching everythingevaluate::tests::empty_scoped_exception_is_ignored
An exception outranks the dynamic risk tableevaluate::dynamic_action_tests::an_exception_outranks_the_risk_table
Capability clamping has the last wordevaluate::tests::capability_clamping_never_overclaims_enforcement, evaluate::dynamic_action_tests::capability_clamping_still_has_the_last_word
The same event scores the same tomorrowevaluate::dynamic_action_tests::the_same_event_scores_the_same_tomorrow, risk::properties::scoring_is_deterministic

The empty-scoped-exception row is the one worth reading twice. An exception whose scope is empty is the difference between "this applies to nobody" and "this applies to everybody", and getting it the wrong way round would silently disable a policy across the estate.

3. Explain every decision ​

This is the requirement a test can pass without meeting. every_outcome_carries_ an_explanation asserts the explanation is not empty - which an implementation whose explanation read "matched" would satisfy while failing the epic entirely.

So the explanation is now asserted to say things (DLP-119):

ClaimProved by
Every outcome carries an explanationevaluate::tests::every_outcome_carries_an_explanation
The conflict explanation names the strategy, the winner and how many were weighedevaluate::tests::the_conflict_explanation_names_the_strategy_the_winner_and_the_losers
Two different strategies produce two different sentencesevaluate::tests::the_explanation_distinguishes_one_strategy_from_another
A single matching policy still says it was the only oneevaluate::tests::a_single_matching_policy_still_says_it_was_the_only_one
A clamped action explains the clamp, naming what was asked for and what was doneevaluate::tests::capability_clamping_never_overclaims_enforcement
Every risk factor is explained, and one that contributed nothing is not quoted as a reasonrisk::tests::every_factor_is_explained, evaluate::dynamic_action_tests::a_factor_that_contributed_nothing_is_not_quoted_as_a_reason
An unscored event falls back loudly rather than silentlyevaluate::dynamic_action_tests::an_unscored_event_falls_back_loudly_rather_than_silently
A malformed risk table is described in words an author can act onevaluate::dynamic_action_tests::a_malformed_table_is_described_in_words_an_author_can_act_on

"How many were weighed" is there for a specific reason: "3 of 3 policies matched" is the sentence that rules out a fourth policy having been silently dropped. Without a count, an explanation cannot distinguish a decision from an incomplete one.


4. What is not covered ​

  • The endpoint cannot evaluate a business-hours condition, and abstains rather than guessing - see §1. Not a defect, and not silent: the factor breakdown on the event says what was and was not known.
  • No test reconstructs a decision from the event alone. runner.rs argues that a decision taken offline is reproducible from the factors the event carries; nothing asserts it end to end. That is a bigger test than this audit should invent, and it belongs with the event contract rather than the engine.
  • Policy authoring - what the console will let somebody write - is DLP-107 and the detector-authoring tests, not this document.
  • Schedules in a tenant's timezone are dlp-policy-engine's schedule and business_hours modules, 33 tests between them, and they run on the server.

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