Appearance
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.
| Claim | Proved by |
|---|---|
| An exact classification id/version/revision drives a decision and retains rule/element lineage | classification_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 or | classification_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 text | classification_policy_binding::missing_detector_evidence_is_fail_safe_and_privacy_bounded |
| A matching detector triggers the policy; no hit means no action | evaluate::tests::matching_detector_triggers_the_policy, …::no_detector_hit_means_no_action |
| Boolean composition - and, or, not, at-least - works | evaluate::tests::boolean_composition_and_or_not_at_least |
| A bulk threshold catches what a per-detector one misses | evaluate::tests::total_matches_threshold_catches_bulk_exfiltration |
| Scope restricts by department and channel | evaluate::tests::scope_restricts_by_department_and_channel |
| File conditions filter by extension and size | evaluate::tests::file_condition_filters_by_extension_and_size |
| A file-owner condition matches an exact SID or resolved name without guessing one | evaluate::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 budget | file_owner::tests::a_slow_resolution_cannot_stall_the_scan_past_its_budget |
| Destination matching uses domain-suffix semantics, not substring | evaluate::tests::destination_matching_uses_domain_suffix_semantics |
An unvalidated match fails a require_validated condition | evaluate::tests::unvalidated_match_fails_a_require_validated_condition |
| Draft policies are not enforced | evaluate::tests::draft_policies_are_not_enforced |
| An effective period is honoured | evaluate::tests::effective_period_is_honoured |
| A policy from another tenant never applies | evaluate::tests::cross_tenant_policy_never_applies |
| A disconnected endpoint uses the policy's offline response | evaluate::tests::offline_response_is_used_when_disconnected |
| Evaluation is fast enough for the endpoint's hot path | evaluate::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.
| Claim | Proved by |
|---|---|
| Most restrictive wins by default | evaluate::tests::most_restrictive_wins_by_default |
| Highest priority overrides strength when asked | evaluate::tests::highest_priority_strategy_overrides_strength |
| An exception suppresses a policy, and it is recorded | evaluate::tests::exception_suppresses_the_policy_and_is_recorded |
| An exception can downgrade rather than suppress | evaluate::tests::exception_can_downgrade_instead_of_suppress |
| An expired exception does not apply | evaluate::tests::expired_exception_does_not_apply |
| An empty scoped exception is ignored rather than matching everything | evaluate::tests::empty_scoped_exception_is_ignored |
| An exception outranks the dynamic risk table | evaluate::dynamic_action_tests::an_exception_outranks_the_risk_table |
| Capability clamping has the last word | evaluate::tests::capability_clamping_never_overclaims_enforcement, evaluate::dynamic_action_tests::capability_clamping_still_has_the_last_word |
| The same event scores the same tomorrow | evaluate::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):
| Claim | Proved by |
|---|---|
| Every outcome carries an explanation | evaluate::tests::every_outcome_carries_an_explanation |
| The conflict explanation names the strategy, the winner and how many were weighed | evaluate::tests::the_conflict_explanation_names_the_strategy_the_winner_and_the_losers |
| Two different strategies produce two different sentences | evaluate::tests::the_explanation_distinguishes_one_strategy_from_another |
| A single matching policy still says it was the only one | evaluate::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 done | evaluate::tests::capability_clamping_never_overclaims_enforcement |
| Every risk factor is explained, and one that contributed nothing is not quoted as a reason | risk::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 silently | evaluate::dynamic_action_tests::an_unscored_event_falls_back_loudly_rather_than_silently |
| A malformed risk table is described in words an author can act on | evaluate::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.rsargues 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-107and the detector-authoring tests, not this document. - Schedules in a tenant's timezone are
dlp-policy-engine'sscheduleandbusiness_hoursmodules, 33 tests between them, and they run on the server.