Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 34 additions & 22 deletions crates/flowproof-adapters/src/egress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,29 +72,35 @@ 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;
let host = HostReadiness::probe();
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("; ")
))
}
Expand Down Expand Up @@ -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}"
);
}

Expand Down
30 changes: 20 additions & 10 deletions crates/flowproof-adapters/src/egress_windows.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
7 changes: 4 additions & 3 deletions crates/flowproof-adapters/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading