diff --git a/crates/flowproof-adapters/src/egress.rs b/crates/flowproof-adapters/src/egress.rs index 4c33a0e..fb47b13 100644 --- a/crates/flowproof-adapters/src/egress.rs +++ b/crates/flowproof-adapters/src/egress.rs @@ -72,14 +72,21 @@ impl Containment { crate::egress_linux::probe_containment() } - /// Windows is being built. Until the WFP filters land this ALWAYS returns - /// "not contained" - the probe only makes the REASON specific, so an - /// adopter learns whether their host is ready rather than guessing. + /// Windows decides its tier from the RUN, not from this probe, so this is + /// the answer for a run that has not started - and it is never optimistic. /// - /// The `Enforced` arm is absent rather than conditional. `egress_windows` - /// reports facts about the host and never names this type, so there is no - /// path from a probe result to a containment claim until the step that - /// installs the filters adds one. + /// That is the whole difference from the Linux arm above. There the filter + /// installs in the child's `pre_exec`, so a probe-pass implies an + /// install-success. Here seven steps can still fail after the probe says + /// yes - the account, the logon, the privileges, the desktop grant, the + /// engine, the sublayer, the filters - plus collection, without which + /// there is no audit lane. See [`crate::egress_windows::run`]. + /// + /// So the `Enforced` arm is absent rather than conditional. A tier claimed + /// from a probe would be a PREDICTION reported as a RESULT, which is the + /// false green of #300 and #301 arriving by optimism instead of by + /// silence. The achieved tier travels back on + /// [`crate::agent_runner::AgentRun::containment`] and wins where present. #[cfg(windows)] pub fn command_flow() -> Self { use crate::egress_windows::HostReadiness; @@ -87,14 +94,13 @@ impl Containment { let blockers = host.blockers(); if blockers.is_empty() { Containment::NotContained(format!( - "egress containment is not implemented on Windows yet; this host could \ - support it ({})", + "this run has not been contained; on Windows the tier is decided by the \ + run, and this host can support it ({})", host.summary() )) } else { Containment::NotContained(format!( - "egress containment is not implemented on Windows yet, and this host could \ - not support it as configured: {}", + "this host cannot enforce egress containment as configured: {}", blockers.join("; ") )) } @@ -285,27 +291,33 @@ mod tests { ); } - /// Until the WFP filters exist, a Windows `command:` flow must report - /// "not contained" - whatever the host is capable of. + /// The PRE-RUN prediction is never optimistic on Windows. + /// + /// This test used to say the filters did not exist. They do now, so what + /// it holds has changed while staying the same claim: a tier taken before + /// anything starts must not say `enforced`, because seven steps can fail + /// after the probe passes. A tier that says `enforced` lets + /// `assert_no_egress` certify a run nothing was containing - the false + /// green of #300 and #301 arriving by prediction rather than by silence. /// - /// This is the same false green as #300 and #301 arriving by a third - /// route: a tier that says `enforced` lets `assert_no_egress` certify a - /// run nothing was containing. The probe makes the REASON specific; it - /// must never make the VERDICT optimistic. The step that installs filters - /// is the step that deletes this test. + /// The reason must also describe the RUN, not our roadmap. "Not + /// implemented on Windows yet" was true when this file was written and is + /// now prose describing code that no longer exists, which this repository + /// treats as a defect in its own right. #[cfg(windows)] #[test] - fn enforced_is_unreachable_on_windows() { + fn the_pre_run_prediction_is_never_optimistic_on_windows() { let tier = Containment::command_flow(); assert!( !tier.is_enforced(), - "no filter is installed yet, so nothing can be contained: {}", + "the run has not started, so nothing has been contained yet: {}", tier.report_line() ); let reason = tier.reason().expect("not contained carries a reason"); assert!( - reason.contains("not implemented on Windows yet"), - "the reason must read as our roadmap, not as the adopter's host: {reason}" + !reason.contains("not implemented"), + "Windows containment IS implemented; the reason must describe this run, \ + not a roadmap that has moved on: {reason}" ); } diff --git a/crates/flowproof-adapters/src/egress_windows.rs b/crates/flowproof-adapters/src/egress_windows.rs index 814c676..3c15a4f 100644 --- a/crates/flowproof-adapters/src/egress_windows.rs +++ b/crates/flowproof-adapters/src/egress_windows.rs @@ -1,14 +1,24 @@ -//! Windows egress containment. Still installs no WFP filter and launches -//! nothing; [`identity`] adds the per-run account the filters will be scoped -//! to. +//! Windows egress containment, end to end: a per-run local account +//! ([`identity`]) it is logged on as ([`logon`]) and started under +//! ([`spawn`]), WFP filters scoped to that account's SID ([`wfp`], +//! [`filters`]), and the audit lane that evidences what they refused +//! ([`netevents`], [`audit`]). [`run`] is the sequence; everything else is a +//! step in it. //! -//! **Nothing here can produce a `Containment::Enforced`.** That is structural -//! rather than a convention: this module does not name that type at all. It -//! reports facts; `Containment::command_flow` turns them into a "not -//! contained" reason, and `enforced_is_unreachable_on_windows` in `egress` -//! asserts the tier. Claiming enforcement before a filter exists would let -//! `assert_no_egress` certify a run nothing was containing - the same false -//! green as #300 and #301 by a third route. +//! **Nothing here names `Containment`.** That is structural rather than a +//! convention, and it survives the filters existing: this module is gated on +//! the `windows` DEPENDENCY while `Containment` is gated on the `agent` +//! FEATURE, so naming it would drag this code behind `agent` and cost it the +//! only typecheck it gets without a Windows runner (see the gate on the `pub +//! mod` in `lib.rs`). +//! +//! So [`run::Outcome`] reports what the run ACHIEVED as plain data, and the +//! caller converts. `Containment::command_flow` still cannot return +//! `Enforced` on Windows, for a reason that outlived the filters landing: it +//! is a PRE-RUN probe, and seven steps can fail after it passes. Predicting a +//! tier and reporting it as a result would let `assert_no_egress` certify a +//! run nothing was containing - the same false green as #300 and #301 by a +//! third route. //! //! Probing at all is about the REASON. Windows containment needs three things //! a host can be missing, each of which fails as something else (see diff --git a/crates/flowproof-adapters/src/lib.rs b/crates/flowproof-adapters/src/lib.rs index c8dd1e4..e54f8d0 100644 --- a/crates/flowproof-adapters/src/lib.rs +++ b/crates/flowproof-adapters/src/lib.rs @@ -16,9 +16,10 @@ pub mod egress; #[cfg(all(feature = "agent", target_os = "linux"))] pub mod egress_linux; -// Windows containment is being built (see `spike/windows-containment/LOG.md`). -// Today this module is the capability PROBE only: it installs no filter and -// cannot report "enforced", so a Windows run is still honestly not contained. +// Windows containment (see `spike/windows-containment/LOG.md` for how it was +// established). This module installs the filters and runs the agent behind +// them; the tier it achieved travels back on the run rather than being +// predicted by a probe, because several steps can fail after the probe passes. // // Gated on the `windows` DEPENDENCY (either feature that pulls it) rather than // on `agent`, and that is deliberate. `agent` pulls ureq, whose TLS stack