flowproof 0.12.1__tar.gz → 0.12.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flowproof-0.12.1 → flowproof-0.12.2}/Cargo.lock +7 -7
- {flowproof-0.12.1 → flowproof-0.12.2}/Cargo.toml +1 -1
- {flowproof-0.12.1 → flowproof-0.12.2}/PKG-INFO +1 -1
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/env_block.rs +17 -2
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/web.rs +228 -15
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/tests/windows_containment_e2e.rs +90 -31
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/author.rs +367 -9
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/src/lib.rs +9 -10
- flowproof-0.12.2/crates/flowproof-cli/tests/llm_author_e2e.rs +556 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/flowproof/__init__.py +1 -1
- {flowproof-0.12.1 → flowproof-0.12.2}/pyproject.toml +1 -1
- flowproof-0.12.1/crates/flowproof-cli/tests/llm_author_e2e.rs +0 -177
- {flowproof-0.12.1 → flowproof-0.12.2}/README.md +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/Cargo.toml +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/agent_proxy.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/agent_runner.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_linux.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/audit.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/capture.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/filters.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/identity.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/logon.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/netevents.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/run.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/spawn.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/wfp.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/fs_observe.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/lib.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/mcp_core.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/mcp_http.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/mcp_stdio.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/sap_com.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/vision.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/tests/drag_spike.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/tests/fs_observation_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/Cargo.toml +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/agent_steps.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/clarify.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/heal.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/lib.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/llm.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/recorder.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/rules.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/src/spec.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-agent/tests/model_client_http.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/Cargo.toml +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/src/agent_flow.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/src/capture.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/src/main.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/agent_flow_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/agent_url_record_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/api_pipeline.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/audit_record_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/calc_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/clock_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/cookie_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/doctor_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/egress_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/examples_resolve.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_arg_matchers.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_audit_since.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_call_order.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_capability_regression.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_control_verdict.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_mcp_kill.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_no_tool_call.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/falsifiability_reply_contains.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/grid_cell_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/iframe_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/mcp_http_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/mcp_stdio_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/notepad_author_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/notepad_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/page_title_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/pinned_random_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/sap_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/sap_pipeline.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/sap_sim_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/scoped_container_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/select_missing_option_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/skip_unless_env.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/suite_env_from.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/suite_flow_isolation.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/suite_missing_trace.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/support/sap_simulator.py +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/visibility_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/vision_pipeline.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-cli/tests/web_e2e.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/Cargo.toml +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/app.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/backend.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/gdi.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/lib.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/mock.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/oob.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/recording.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/redact.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/visual.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-driver/src/window.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-python/Cargo.toml +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-python/src/lib.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-replay/Cargo.toml +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-replay/src/lib.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-replay/src/report.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-replay/src/runrecord.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-replay/tests/replay_calc.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/Cargo.toml +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/schema/trace-v1.schema.json +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/captures.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/cassette.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/cassette_diff.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/egress.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/format.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/lib.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/secret.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/secret_scan.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/substitution.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/src/toolcalls.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/tests/fixtures/sample.trace.jsonl +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-trace/tests/schema_conformance.rs +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/flowproof/cli.py +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/flowproof/flow.py +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/flowproof/mcp_server.py +0 -0
- {flowproof-0.12.1 → flowproof-0.12.2}/flowproof/py.typed +0 -0
|
@@ -760,7 +760,7 @@ dependencies = [
|
|
|
760
760
|
|
|
761
761
|
[[package]]
|
|
762
762
|
name = "flowproof-adapters"
|
|
763
|
-
version = "0.12.
|
|
763
|
+
version = "0.12.2"
|
|
764
764
|
dependencies = [
|
|
765
765
|
"anyhow",
|
|
766
766
|
"flowproof-driver",
|
|
@@ -779,7 +779,7 @@ dependencies = [
|
|
|
779
779
|
|
|
780
780
|
[[package]]
|
|
781
781
|
name = "flowproof-agent"
|
|
782
|
-
version = "0.12.
|
|
782
|
+
version = "0.12.2"
|
|
783
783
|
dependencies = [
|
|
784
784
|
"chrono",
|
|
785
785
|
"flowproof-driver",
|
|
@@ -796,7 +796,7 @@ dependencies = [
|
|
|
796
796
|
|
|
797
797
|
[[package]]
|
|
798
798
|
name = "flowproof-cli"
|
|
799
|
-
version = "0.12.
|
|
799
|
+
version = "0.12.2"
|
|
800
800
|
dependencies = [
|
|
801
801
|
"ab_glyph",
|
|
802
802
|
"chrono",
|
|
@@ -817,7 +817,7 @@ dependencies = [
|
|
|
817
817
|
|
|
818
818
|
[[package]]
|
|
819
819
|
name = "flowproof-driver"
|
|
820
|
-
version = "0.12.
|
|
820
|
+
version = "0.12.2"
|
|
821
821
|
dependencies = [
|
|
822
822
|
"image",
|
|
823
823
|
"postgres",
|
|
@@ -831,7 +831,7 @@ dependencies = [
|
|
|
831
831
|
|
|
832
832
|
[[package]]
|
|
833
833
|
name = "flowproof-python"
|
|
834
|
-
version = "0.12.
|
|
834
|
+
version = "0.12.2"
|
|
835
835
|
dependencies = [
|
|
836
836
|
"flowproof-agent",
|
|
837
837
|
"flowproof-cli",
|
|
@@ -843,7 +843,7 @@ dependencies = [
|
|
|
843
843
|
|
|
844
844
|
[[package]]
|
|
845
845
|
name = "flowproof-replay"
|
|
846
|
-
version = "0.12.
|
|
846
|
+
version = "0.12.2"
|
|
847
847
|
dependencies = [
|
|
848
848
|
"chrono",
|
|
849
849
|
"flowproof-agent",
|
|
@@ -857,7 +857,7 @@ dependencies = [
|
|
|
857
857
|
|
|
858
858
|
[[package]]
|
|
859
859
|
name = "flowproof-trace"
|
|
860
|
-
version = "0.12.
|
|
860
|
+
version = "0.12.2"
|
|
861
861
|
dependencies = [
|
|
862
862
|
"jsonschema",
|
|
863
863
|
"regex",
|
|
@@ -22,7 +22,7 @@ members = ["crates/flowproof-driver", "crates/flowproof-trace", "crates/flowproo
|
|
|
22
22
|
exclude = ["spike/windows-containment"]
|
|
23
23
|
|
|
24
24
|
[workspace.package]
|
|
25
|
-
version = "0.12.
|
|
25
|
+
version = "0.12.2"
|
|
26
26
|
edition = "2021"
|
|
27
27
|
license = "Apache-2.0"
|
|
28
28
|
repository = "https://github.com/automators-com/flowproof"
|
{flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/src/egress_windows/env_block.rs
RENAMED
|
@@ -198,11 +198,26 @@ mod tests {
|
|
|
198
198
|
let mut overrides = BTreeMap::new();
|
|
199
199
|
overrides.insert("ZZ_FLOWPROOF_SORT".to_string(), "z".into());
|
|
200
200
|
overrides.insert("AA_FLOWPROOF_SORT".to_string(), "a".into());
|
|
201
|
+
// Windows runner images carry this same prefix shape as
|
|
202
|
+
// COMMONPROGRAMFILES and COMMONPROGRAMFILES(X86). Comparing whole
|
|
203
|
+
// NAME=VALUE entries would put '(' before '=' and falsely call the
|
|
204
|
+
// correctly name-sorted block unsorted.
|
|
205
|
+
overrides.insert("MM_FLOWPROOF_SORT".to_string(), "plain".into());
|
|
206
|
+
overrides.insert("MM_FLOWPROOF_SORT(X86)".to_string(), "qualified".into());
|
|
201
207
|
|
|
202
208
|
let entries = parse(&environment_block(&overrides, &[]));
|
|
203
|
-
let upper: Vec<String> = entries
|
|
209
|
+
let upper: Vec<String> = entries
|
|
210
|
+
.iter()
|
|
211
|
+
.map(|entry| {
|
|
212
|
+
entry
|
|
213
|
+
.split_once('=')
|
|
214
|
+
.expect("an environment entry has a name and value")
|
|
215
|
+
.0
|
|
216
|
+
.to_uppercase()
|
|
217
|
+
})
|
|
218
|
+
.collect();
|
|
204
219
|
let mut sorted = upper.clone();
|
|
205
220
|
sorted.sort();
|
|
206
|
-
assert_eq!(upper, sorted, "the block must be sorted");
|
|
221
|
+
assert_eq!(upper, sorted, "the block must be sorted by name");
|
|
207
222
|
}
|
|
208
223
|
}
|
|
@@ -15,6 +15,7 @@ use flowproof_driver::{
|
|
|
15
15
|
use headless_chrome::browser::tab::{ModifierKey, Tab};
|
|
16
16
|
use headless_chrome::protocol::cdp::Target::CreateTarget;
|
|
17
17
|
use headless_chrome::protocol::cdp::{Emulation, Input, Network, Page};
|
|
18
|
+
use headless_chrome::types::Bounds;
|
|
18
19
|
use headless_chrome::{Browser, LaunchOptions};
|
|
19
20
|
|
|
20
21
|
use crate::AdapterError;
|
|
@@ -526,6 +527,16 @@ fn headed_requested() -> bool {
|
|
|
526
527
|
std::env::var_os("FLOWPROOF_HEADED").is_some()
|
|
527
528
|
}
|
|
528
529
|
|
|
530
|
+
/// Headed runs are deliberately private. A shared browser isolates each flow
|
|
531
|
+
/// in an incognito context, which Chromium presents as a second window. When
|
|
532
|
+
/// the flow tab closes, the original window (including the keep-alive tab)
|
|
533
|
+
/// becomes visible again and looks like a leaked browser. A visible run is for
|
|
534
|
+
/// a person to watch, so cold-start reuse is worth less than owning one window
|
|
535
|
+
/// whose lifetime exactly matches the flow.
|
|
536
|
+
fn should_share_browser(headed: bool, no_shared: bool) -> bool {
|
|
537
|
+
!headed && !no_shared
|
|
538
|
+
}
|
|
539
|
+
|
|
529
540
|
/// Build the launch options, split out from [`launch_browser`] so the headless
|
|
530
541
|
/// decision is testable without starting a browser.
|
|
531
542
|
fn launch_options_for(
|
|
@@ -813,9 +824,14 @@ impl WebAppDriver {
|
|
|
813
824
|
}
|
|
814
825
|
|
|
815
826
|
/// A driver on the shared browser (isolated context per flow), or a
|
|
816
|
-
/// private browser when `FLOWPROOF_NO_SHARED_BROWSER=1
|
|
827
|
+
/// private browser when `FLOWPROOF_NO_SHARED_BROWSER=1` or the browser is
|
|
828
|
+
/// headed. Headed ownership prevents the keep-alive window from surviving
|
|
829
|
+
/// after the visible flow window closes.
|
|
817
830
|
pub fn new() -> Result<Self, AdapterError> {
|
|
818
|
-
if
|
|
831
|
+
if !should_share_browser(
|
|
832
|
+
headed_requested(),
|
|
833
|
+
std::env::var_os("FLOWPROOF_NO_SHARED_BROWSER").is_some(),
|
|
834
|
+
) {
|
|
819
835
|
return Ok(Self {
|
|
820
836
|
browser: launch_browser(&[])?,
|
|
821
837
|
context_id: None,
|
|
@@ -1427,9 +1443,9 @@ fn xpath_literal(text: &str) -> String {
|
|
|
1427
1443
|
|
|
1428
1444
|
impl Drop for WebAppDriver {
|
|
1429
1445
|
fn drop(&mut self) {
|
|
1430
|
-
// The shared browser outlives this driver: close the tab so pages
|
|
1431
|
-
// don't accumulate across a suite. A private browser (opt-out
|
|
1432
|
-
// torn down with its
|
|
1446
|
+
// The shared browser outlives this driver: close the flow tab so pages
|
|
1447
|
+
// don't accumulate across a suite. A private browser (opt-out or
|
|
1448
|
+
// headed) is torn down with its process.
|
|
1433
1449
|
if self.context_id.is_some() {
|
|
1434
1450
|
if let Some(tab) = self.tab.take() {
|
|
1435
1451
|
let _ = tab.close(false);
|
|
@@ -1633,6 +1649,16 @@ impl AppDriver for WebAppDriver {
|
|
|
1633
1649
|
None => self.browser.new_tab(),
|
|
1634
1650
|
}
|
|
1635
1651
|
.map_err(|e| web_err("opening tab", e))?;
|
|
1652
|
+
// A visible flow is the only Chrome window the user should have to
|
|
1653
|
+
// manage. Chromium's default launch bounds can be a narrow utility
|
|
1654
|
+
// window (and an incognito context used to create another one), so
|
|
1655
|
+
// present the privately-owned headed window at the desktop's normal
|
|
1656
|
+
// maximized size. Device emulation below still pins the page viewport
|
|
1657
|
+
// when the spec requests one.
|
|
1658
|
+
if headed_requested() {
|
|
1659
|
+
tab.set_bounds(Bounds::Maximized)
|
|
1660
|
+
.map_err(|e| web_err("maximizing headed browser window", e))?;
|
|
1661
|
+
}
|
|
1636
1662
|
// Viewport/UA emulation BEFORE navigation, so the app boots into
|
|
1637
1663
|
// the emulated device (responsive breakpoints, UA sniffing). NOT
|
|
1638
1664
|
// best-effort: a flow recorded mobile must never run desktop.
|
|
@@ -3134,16 +3160,85 @@ impl AppDriver for WebAppDriver {
|
|
|
3134
3160
|
}
|
|
3135
3161
|
return 'body > ' + parts.join(' > ');
|
|
3136
3162
|
}
|
|
3163
|
+
function compoundCss(el) {
|
|
3164
|
+
const classes = Array.from(el.classList);
|
|
3165
|
+
if (!classes.length) return null;
|
|
3166
|
+
return el.tagName.toLowerCase() + classes.map(cls => '.' + CSS.escape(cls)).join('');
|
|
3167
|
+
}
|
|
3168
|
+
function relativeCss(el, container) {
|
|
3169
|
+
const semantic = semanticCss(el);
|
|
3170
|
+
if (semantic && container.querySelectorAll(semantic).length === 1) return semantic;
|
|
3171
|
+
const compound = compoundCss(el);
|
|
3172
|
+
if (!compound) return null;
|
|
3173
|
+
if (container.querySelectorAll(compound).length === 1) return compound;
|
|
3174
|
+
|
|
3175
|
+
// A value cell often shares its base classes with the label
|
|
3176
|
+
// beside it, while the label has one extra presentation class
|
|
3177
|
+
// (`bg-info`, for example). Excluding that distinguishing
|
|
3178
|
+
// class gives the scoped target a stable, non-positional inner
|
|
3179
|
+
// selector without smuggling visible text into CSS.
|
|
3180
|
+
const extras = new Set();
|
|
3181
|
+
for (const other of container.querySelectorAll(compound)) {
|
|
3182
|
+
if (other === el) continue;
|
|
3183
|
+
for (const cls of other.classList) {
|
|
3184
|
+
if (!el.classList.contains(cls)) extras.add(cls);
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
for (const extra of extras) {
|
|
3188
|
+
const candidate = compound + ':not(.' + CSS.escape(extra) + ')';
|
|
3189
|
+
if (container.querySelectorAll(candidate).length === 1) return candidate;
|
|
3190
|
+
}
|
|
3191
|
+
return null;
|
|
3192
|
+
}
|
|
3193
|
+
function scopedReadable(el) {
|
|
3194
|
+
const container = el.parentElement;
|
|
3195
|
+
if (!container) return null;
|
|
3196
|
+
const containerCss = semanticCss(container) || compoundCss(container);
|
|
3197
|
+
const innerCss = relativeCss(el, container);
|
|
3198
|
+
if (!containerCss || !innerCss) return null;
|
|
3199
|
+
|
|
3200
|
+
// Use a neighbouring leaf as the row/card identity. Require
|
|
3201
|
+
// that the anchor identifies one matching container on the
|
|
3202
|
+
// current screen; runtime scoped resolution will enforce the
|
|
3203
|
+
// same relationship again on every replay.
|
|
3204
|
+
const siblings = Array.from(container.children);
|
|
3205
|
+
const before = siblings.slice(0, siblings.indexOf(el)).reverse();
|
|
3206
|
+
const after = siblings.slice(siblings.indexOf(el) + 1);
|
|
3207
|
+
for (const anchorEl of before.concat(after)) {
|
|
3208
|
+
const anchor = (anchorEl.textContent || '').trim();
|
|
3209
|
+
if (!anchor || anchor.length > 120) continue;
|
|
3210
|
+
const matching = Array.from(document.querySelectorAll(containerCss)).filter(candidate =>
|
|
3211
|
+
Array.from(candidate.children).some(child =>
|
|
3212
|
+
child !== el && (child.textContent || '').trim() === anchor
|
|
3213
|
+
)
|
|
3214
|
+
);
|
|
3215
|
+
if (matching.length !== 1 || matching[0] !== container) continue;
|
|
3216
|
+
const containerTarget = 'css:' + containerCss;
|
|
3217
|
+
const innerTarget = 'css:' + innerCss;
|
|
3218
|
+
return {
|
|
3219
|
+
token: 'scoped:' + containerTarget + ' containing ' + JSON.stringify(anchor) +
|
|
3220
|
+
' > ' + innerTarget,
|
|
3221
|
+
container: containerTarget,
|
|
3222
|
+
anchor,
|
|
3223
|
+
inner: innerTarget,
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
return null;
|
|
3227
|
+
}
|
|
3137
3228
|
const all = Array.from(document.querySelectorAll('body *'));
|
|
3229
|
+
const styledLeaf = el => el.children.length === 0 &&
|
|
3230
|
+
['DIV', 'SPAN'].includes(el.tagName) &&
|
|
3231
|
+
/(?:background|color)\s*:/i.test(el.getAttribute('style') || '');
|
|
3138
3232
|
const interactive = el => el.matches(
|
|
3139
|
-
'input, button, a, select, textarea, [role=button], [role=checkbox], [role=radio], [role=menuitem]'
|
|
3140
|
-
)
|
|
3233
|
+
'input, button, a, select, textarea, [role=button], [role=checkbox], [role=radio], [role=menuitem], [draggable], [ondrop], .draggable-row, .droparea'
|
|
3234
|
+
) || (!!el.id && el.children.length === 0 && ['DIV', 'SPAN'].includes(el.tagName)) ||
|
|
3235
|
+
styledLeaf(el);
|
|
3141
3236
|
const readableLeaf = el => {
|
|
3142
3237
|
if (['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(el.tagName)) return false;
|
|
3143
3238
|
const text = (el.textContent || '').trim();
|
|
3144
|
-
return text &&
|
|
3239
|
+
return text && !Array.from(el.children).some(child =>
|
|
3145
3240
|
(child.textContent || '').trim()
|
|
3146
|
-
);
|
|
3241
|
+
) && (semanticCss(el) || scopedReadable(el));
|
|
3147
3242
|
};
|
|
3148
3243
|
const ordered = all.filter(interactive).concat(
|
|
3149
3244
|
all.filter(el => !interactive(el) && readableLeaf(el))
|
|
@@ -3152,27 +3247,134 @@ impl AppDriver for WebAppDriver {
|
|
|
3152
3247
|
const chosen = ordered.filter(el => {
|
|
3153
3248
|
const r = el.getBoundingClientRect();
|
|
3154
3249
|
const style = getComputedStyle(el);
|
|
3250
|
+
// Authoring inventories the rendered PAGE, not only the
|
|
3251
|
+
// current viewport. A user naturally says "enter the date"
|
|
3252
|
+
// even when the field starts below the fold, and every web
|
|
3253
|
+
// action already scrolls its target into view before acting.
|
|
3155
3254
|
const rendered = style.display !== 'none' && style.visibility !== 'hidden' &&
|
|
3156
|
-
Number(style.opacity) > 0 && r.width > 0 && r.height > 0
|
|
3157
|
-
r.bottom > 0 && r.right > 0 && r.top < innerHeight && r.left < innerWidth;
|
|
3255
|
+
Number(style.opacity) > 0 && r.width > 0 && r.height > 0;
|
|
3158
3256
|
if (!rendered || seen.has(el)) return false;
|
|
3159
3257
|
seen.add(el);
|
|
3160
3258
|
return true;
|
|
3161
3259
|
}).slice(0, 100);
|
|
3162
|
-
|
|
3260
|
+
const entries = chosen.map(el => {
|
|
3163
3261
|
const css = cssPath(el);
|
|
3262
|
+
const scoped = !interactive(el) && !semanticCss(el) ? scopedReadable(el) : null;
|
|
3164
3263
|
const label = el.labels && el.labels[0] ? el.labels[0].textContent.trim()
|
|
3165
3264
|
: (el.getAttribute('aria-label') || el.getAttribute('placeholder') || '');
|
|
3166
|
-
|
|
3167
|
-
target: 'css:' + css,
|
|
3265
|
+
const entry = {
|
|
3266
|
+
target: scoped ? scoped.token : 'css:' + css,
|
|
3168
3267
|
css,
|
|
3169
3268
|
tag: el.tagName.toLowerCase(),
|
|
3170
3269
|
actionable: interactive(el),
|
|
3171
3270
|
type: el.getAttribute('type') || undefined,
|
|
3172
3271
|
text: (el.textContent || '').trim().slice(0, 80) || undefined,
|
|
3173
3272
|
label: label || undefined,
|
|
3273
|
+
background_color: styledLeaf(el) ? getComputedStyle(el).backgroundColor : undefined,
|
|
3274
|
+
};
|
|
3275
|
+
if (scoped) entry.scope = {
|
|
3276
|
+
container: scoped.container,
|
|
3277
|
+
anchor: scoped.anchor,
|
|
3278
|
+
inner: scoped.inner,
|
|
3174
3279
|
};
|
|
3175
|
-
|
|
3280
|
+
return entry;
|
|
3281
|
+
});
|
|
3282
|
+
|
|
3283
|
+
// A human can ask for the number of displayed rows or the
|
|
3284
|
+
// final cell without naming one current row. Represent those
|
|
3285
|
+
// collection identities directly so the model can ground the
|
|
3286
|
+
// intent without inventing a selector from today's DOM.
|
|
3287
|
+
for (const table of document.querySelectorAll('table')) {
|
|
3288
|
+
const tableCss = semanticCss(table);
|
|
3289
|
+
if (!tableCss) continue;
|
|
3290
|
+
const rows = Array.from(table.querySelectorAll('tr'));
|
|
3291
|
+
if (rows.length) {
|
|
3292
|
+
const css = tableCss + ' tr';
|
|
3293
|
+
entries.push({
|
|
3294
|
+
target: 'css:' + css,
|
|
3295
|
+
css,
|
|
3296
|
+
tag: 'collection',
|
|
3297
|
+
actionable: false,
|
|
3298
|
+
count: rows.length,
|
|
3299
|
+
label: 'rows in ' + tableCss,
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
const lastCell = table.querySelector('tr:last-child td:last-child');
|
|
3303
|
+
if (lastCell) {
|
|
3304
|
+
const css = tableCss + ' tr:last-child td:last-child';
|
|
3305
|
+
entries.push({
|
|
3306
|
+
target: 'css:' + css,
|
|
3307
|
+
css,
|
|
3308
|
+
tag: 'td',
|
|
3309
|
+
actionable: false,
|
|
3310
|
+
text: (lastCell.textContent || '').trim().slice(0, 80) || undefined,
|
|
3311
|
+
label: 'last cell in the final row of ' + tableCss,
|
|
3312
|
+
});
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
// Same-origin iframe values are a normal part of the live
|
|
3317
|
+
// scene. Their synthetic token is translated to Target::Framed
|
|
3318
|
+
// before tracing, just like a scoped row token; no synthetic
|
|
3319
|
+
// authoring syntax is persisted or shown to the human.
|
|
3320
|
+
for (const frameEl of document.querySelectorAll('iframe, frame')) {
|
|
3321
|
+
const frameRect = frameEl.getBoundingClientRect();
|
|
3322
|
+
const frameStyle = getComputedStyle(frameEl);
|
|
3323
|
+
if (frameStyle.display === 'none' || frameStyle.visibility === 'hidden' ||
|
|
3324
|
+
Number(frameStyle.opacity) <= 0 || frameRect.width <= 0 || frameRect.height <= 0) {
|
|
3325
|
+
continue;
|
|
3326
|
+
}
|
|
3327
|
+
const frame = frameEl.getAttribute('title') || frameEl.getAttribute('name') ||
|
|
3328
|
+
frameEl.id || frameEl.getAttribute('aria-label');
|
|
3329
|
+
if (!frame) continue;
|
|
3330
|
+
let frameDoc;
|
|
3331
|
+
try { frameDoc = frameEl.contentDocument; } catch (_) { continue; }
|
|
3332
|
+
if (!frameDoc || !frameDoc.body) continue;
|
|
3333
|
+
const inner = [frameDoc.body].concat(Array.from(frameDoc.querySelectorAll(
|
|
3334
|
+
'input, button, select, textarea, a, [role=button], [role=checkbox], [role=radio], [role=menuitem], [id]'
|
|
3335
|
+
)));
|
|
3336
|
+
for (const el of inner) {
|
|
3337
|
+
const style = frameDoc.defaultView.getComputedStyle(el);
|
|
3338
|
+
const rect = el.getBoundingClientRect();
|
|
3339
|
+
if (style.display === 'none' || style.visibility === 'hidden' ||
|
|
3340
|
+
Number(style.opacity) <= 0 || rect.width <= 0 || rect.height <= 0) {
|
|
3341
|
+
continue;
|
|
3342
|
+
}
|
|
3343
|
+
let css = el === frameDoc.body ? 'body' : null;
|
|
3344
|
+
if (!css && el.id) css = '#' + CSS.escape(el.id);
|
|
3345
|
+
if (!css) {
|
|
3346
|
+
for (const attr of ['data-testid', 'data-test', 'data-qa', 'aria-label', 'name']) {
|
|
3347
|
+
const value = el.getAttribute(attr);
|
|
3348
|
+
if (!value) continue;
|
|
3349
|
+
const candidate = el.tagName.toLowerCase() + '[' + attr + '="' +
|
|
3350
|
+
CSS.escape(value) + '"]';
|
|
3351
|
+
if (frameDoc.querySelectorAll(candidate).length === 1) {
|
|
3352
|
+
css = candidate;
|
|
3353
|
+
break;
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
if (!css) continue;
|
|
3358
|
+
const innerTarget = 'css:' + css;
|
|
3359
|
+
const token = 'framed:' + JSON.stringify(frame) + ' > ' + innerTarget;
|
|
3360
|
+
const actionable = el.matches(
|
|
3361
|
+
'input, button, select, textarea, a, [role=button], [role=checkbox], [role=radio], [role=menuitem]'
|
|
3362
|
+
);
|
|
3363
|
+
const label = el.labels && el.labels[0] ? el.labels[0].textContent.trim()
|
|
3364
|
+
: (el.getAttribute('aria-label') || el.getAttribute('placeholder') || '');
|
|
3365
|
+
entries.push({
|
|
3366
|
+
target: token,
|
|
3367
|
+
css,
|
|
3368
|
+
tag: el.tagName.toLowerCase(),
|
|
3369
|
+
actionable,
|
|
3370
|
+
text: el === frameDoc.body ? undefined
|
|
3371
|
+
: (el.value || el.textContent || '').trim().slice(0, 80) || undefined,
|
|
3372
|
+
label: label || (el === frameDoc.body ? 'scroll surface' : undefined),
|
|
3373
|
+
scope: { frame, inner: innerTarget },
|
|
3374
|
+
});
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
return JSON.stringify(entries);
|
|
3176
3378
|
})()
|
|
3177
3379
|
"#;
|
|
3178
3380
|
let value = self
|
|
@@ -3226,6 +3428,17 @@ mod tests {
|
|
|
3226
3428
|
assert!(!headed.headless, "FLOWPROOF_HEADED must show the window");
|
|
3227
3429
|
}
|
|
3228
3430
|
|
|
3431
|
+
/// A visible shared browser leaks its keep-alive window into the user's
|
|
3432
|
+
/// desktop after the flow tab closes. Headless suites retain reuse; either
|
|
3433
|
+
/// explicit opt-out or headed mode must select one privately-owned process.
|
|
3434
|
+
#[test]
|
|
3435
|
+
fn headed_runs_never_use_the_shared_keep_alive_browser() {
|
|
3436
|
+
assert!(super::should_share_browser(false, false));
|
|
3437
|
+
assert!(!super::should_share_browser(true, false));
|
|
3438
|
+
assert!(!super::should_share_browser(false, true));
|
|
3439
|
+
assert!(!super::should_share_browser(true, true));
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3229
3442
|
/// The headed launch failure must name the display, and must not invent one
|
|
3230
3443
|
/// when headless fails for an unrelated reason.
|
|
3231
3444
|
///
|
{flowproof-0.12.1 → flowproof-0.12.2}/crates/flowproof-adapters/tests/windows_containment_e2e.rs
RENAMED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
#![cfg(all(windows, any(feature = "agent", feature = "sap-com")))]
|
|
41
41
|
|
|
42
42
|
use std::io::Read;
|
|
43
|
-
use std::net::TcpListener;
|
|
43
|
+
use std::net::{IpAddr, Ipv4Addr, TcpListener, UdpSocket};
|
|
44
44
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
45
45
|
use std::sync::Arc;
|
|
46
46
|
use std::time::Duration;
|
|
@@ -59,20 +59,20 @@ const RUN_TIMEOUT: Duration = Duration::from_secs(180);
|
|
|
59
59
|
/// Bound in the TEST process, not the child's - that is what makes it an
|
|
60
60
|
/// independent witness rather than a restatement of the child's own opinion.
|
|
61
61
|
struct Oracle {
|
|
62
|
+
host: Ipv4Addr,
|
|
62
63
|
port: u16,
|
|
63
64
|
seen: Arc<AtomicUsize>,
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
impl Oracle {
|
|
67
|
-
/// Bind on
|
|
68
|
+
/// Bind on the runner's primary NIC and accept in the background.
|
|
68
69
|
///
|
|
69
|
-
///
|
|
70
|
-
///
|
|
71
|
-
///
|
|
72
|
-
///
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
let listener = TcpListener::bind("127.0.0.1:0")?;
|
|
70
|
+
/// Flowproof deliberately permits 127.0.0.0/8 so a replay can reach its
|
|
71
|
+
/// local model proxy. Using that address here would test the exception,
|
|
72
|
+
/// not the default-deny policy. The primary NIC still stays entirely on
|
|
73
|
+
/// this host while traversing ALE_AUTH_CONNECT like ordinary traffic.
|
|
74
|
+
fn bind(host: Ipv4Addr) -> std::io::Result<Self> {
|
|
75
|
+
let listener = TcpListener::bind((host, 0))?;
|
|
76
76
|
let port = listener.local_addr()?.port();
|
|
77
77
|
let seen = Arc::new(AtomicUsize::new(0));
|
|
78
78
|
let counter = seen.clone();
|
|
@@ -87,7 +87,7 @@ impl Oracle {
|
|
|
87
87
|
counter.fetch_add(1, Ordering::SeqCst);
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
|
-
Ok(Self { port, seen })
|
|
90
|
+
Ok(Self { host, port, seen })
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
fn sightings(&self) -> usize {
|
|
@@ -96,12 +96,40 @@ impl Oracle {
|
|
|
96
96
|
|
|
97
97
|
fn allow_entry(&self) -> AllowEntry {
|
|
98
98
|
AllowEntry {
|
|
99
|
-
host: HostMatch::Ip(
|
|
99
|
+
host: HostMatch::Ip(IpAddr::V4(self.host)),
|
|
100
100
|
port: Some(self.port),
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/// The address selected by the routing table without sending a packet.
|
|
106
|
+
fn primary_ipv4() -> Option<Ipv4Addr> {
|
|
107
|
+
UdpSocket::bind("0.0.0.0:0")
|
|
108
|
+
.and_then(|socket| {
|
|
109
|
+
socket.connect("192.0.2.1:9")?;
|
|
110
|
+
socket.local_addr()
|
|
111
|
+
})
|
|
112
|
+
.ok()
|
|
113
|
+
.and_then(|addr| match addr.ip() {
|
|
114
|
+
IpAddr::V4(ip) if !ip.is_loopback() && !ip.is_unspecified() => Some(ip),
|
|
115
|
+
_ => None,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// A machine with no non-loopback route cannot exercise the policy honestly.
|
|
120
|
+
fn test_host() -> Option<Ipv4Addr> {
|
|
121
|
+
let host = primary_ipv4();
|
|
122
|
+
if host.is_some() {
|
|
123
|
+
return host;
|
|
124
|
+
}
|
|
125
|
+
assert!(
|
|
126
|
+
std::env::var_os("CI").is_none(),
|
|
127
|
+
"CI must expose a non-loopback IPv4 address for the containment oracle"
|
|
128
|
+
);
|
|
129
|
+
eprintln!("SKIP windows containment E2E: no non-loopback IPv4 address");
|
|
130
|
+
None
|
|
131
|
+
}
|
|
132
|
+
|
|
105
133
|
/// The probe the contained child runs: connect to each port, write a byte,
|
|
106
134
|
/// and say what happened.
|
|
107
135
|
///
|
|
@@ -114,7 +142,7 @@ impl Oracle {
|
|
|
114
142
|
/// Only the outer quotes are double quotes. The command line crosses
|
|
115
143
|
/// `CreateProcessW` as ONE string, and nesting doubles inside it is how this
|
|
116
144
|
/// kind of probe usually breaks.
|
|
117
|
-
fn probe_command(ports: &[u16]) -> String {
|
|
145
|
+
fn probe_command(host: Ipv4Addr, ports: &[u16]) -> String {
|
|
118
146
|
let list = ports
|
|
119
147
|
.iter()
|
|
120
148
|
.map(|p| p.to_string())
|
|
@@ -123,7 +151,8 @@ fn probe_command(ports: &[u16]) -> String {
|
|
|
123
151
|
format!(
|
|
124
152
|
"powershell.exe -NoProfile -NonInteractive -Command \
|
|
125
153
|
\"foreach($p in @({list})){{try{{$c=New-Object Net.Sockets.TcpClient;\
|
|
126
|
-
$c.
|
|
154
|
+
$t=$c.ConnectAsync('{host}',$p);\
|
|
155
|
+
if(-not $t.Wait(10000)){{throw 'connect timeout'}};$s=$c.GetStream();\
|
|
127
156
|
$s.Write([Text.Encoding]::ASCII.GetBytes('fp'),0,2);$s.Flush();$c.Close();\
|
|
128
157
|
Write-Output ('PROBE|'+$p+'|CONNECTED')}}catch{{\
|
|
129
158
|
Write-Output ('PROBE|'+$p+'|REFUSED')}}}}\""
|
|
@@ -140,7 +169,7 @@ struct Observed {
|
|
|
140
169
|
/// Run the probe against both oracles, declaring `allow`.
|
|
141
170
|
fn probe_run(declared: &Oracle, undeclared: &Oracle, allow: &[AllowEntry]) -> Observed {
|
|
142
171
|
let outcome = run::run_contained(
|
|
143
|
-
&probe_command(&[declared.port, undeclared.port]),
|
|
172
|
+
&probe_command(declared.host, &[declared.port, undeclared.port]),
|
|
144
173
|
&Default::default(),
|
|
145
174
|
&[],
|
|
146
175
|
allow,
|
|
@@ -157,6 +186,27 @@ fn probe_run(declared: &Oracle, undeclared: &Oracle, allow: &[AllowEntry]) -> Ob
|
|
|
157
186
|
}
|
|
158
187
|
}
|
|
159
188
|
|
|
189
|
+
/// Output capture and the privileged launcher are not witnesses in this test:
|
|
190
|
+
/// the two destination oracles and the WFP audit lane are. GitHub's hosted
|
|
191
|
+
/// Windows runner currently takes the documented CreateProcessWithLogonW
|
|
192
|
+
/// fallback, so accept only those launcher diagnostics while continuing to
|
|
193
|
+
/// reject every audit or collection fault.
|
|
194
|
+
fn assert_evidence_lane_intact(o: &Observed, ev: &str) {
|
|
195
|
+
let unexpected = o
|
|
196
|
+
.outcome
|
|
197
|
+
.faults
|
|
198
|
+
.iter()
|
|
199
|
+
.filter(|fault| {
|
|
200
|
+
!fault.starts_with("the agent was started through CreateProcessWithLogonW")
|
|
201
|
+
&& !fault.starts_with("launch privileges were not all enabled")
|
|
202
|
+
})
|
|
203
|
+
.collect::<Vec<_>>();
|
|
204
|
+
assert!(
|
|
205
|
+
unexpected.is_empty(),
|
|
206
|
+
"the containment evidence lane reported faults: {unexpected:?}. {ev}"
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
160
210
|
/// Everything a failure needs, in one block, because reproducing this locally
|
|
161
211
|
/// is not an option for most people who will read it.
|
|
162
212
|
fn evidence(label: &str, o: &Observed) -> String {
|
|
@@ -204,14 +254,29 @@ fn host_can_enforce() -> bool {
|
|
|
204
254
|
false
|
|
205
255
|
}
|
|
206
256
|
|
|
207
|
-
///
|
|
257
|
+
/// Prove the positive control first, then invert exactly one declaration.
|
|
258
|
+
///
|
|
259
|
+
/// These phases mutate process-global Windows state (net-event collection and
|
|
260
|
+
/// the interactive desktop ACL), and the positive control also establishes
|
|
261
|
+
/// that this runner's primary-NIC route works before a refusal is treated as
|
|
262
|
+
/// containment evidence. Keeping both phases in one test makes their order
|
|
263
|
+
/// explicit instead of leaving it to Rust's parallel test scheduler.
|
|
208
264
|
#[test]
|
|
209
|
-
fn
|
|
265
|
+
fn declared_and_undeclared_destinations_invert_with_the_policy() {
|
|
210
266
|
if !host_can_enforce() {
|
|
211
267
|
return;
|
|
212
268
|
}
|
|
213
|
-
let
|
|
214
|
-
|
|
269
|
+
let Some(host) = test_host() else {
|
|
270
|
+
return;
|
|
271
|
+
};
|
|
272
|
+
assert_both_declared_destinations_connect(host);
|
|
273
|
+
assert_undeclared_destination_is_refused(host);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// The claim, with all three witnesses agreeing.
|
|
277
|
+
fn assert_undeclared_destination_is_refused(host: Ipv4Addr) {
|
|
278
|
+
let declared = Oracle::bind(host).expect("bind the declared oracle");
|
|
279
|
+
let undeclared = Oracle::bind(host).expect("bind the undeclared oracle");
|
|
215
280
|
|
|
216
281
|
let o = probe_run(&declared, &undeclared, &[declared.allow_entry()]);
|
|
217
282
|
let ev = evidence("enforced", &o);
|
|
@@ -220,10 +285,7 @@ fn an_undeclared_destination_is_refused_and_a_declared_one_is_not() {
|
|
|
220
285
|
// anything. A run that never installed its filters and then saw no traffic
|
|
221
286
|
// is not evidence of containment.
|
|
222
287
|
assert!(o.outcome.is_contained(), "the run was not contained. {ev}");
|
|
223
|
-
|
|
224
|
-
o.outcome.faults.is_empty(),
|
|
225
|
-
"a fault is the absence of evidence, so nothing below can be certified over it. {ev}"
|
|
226
|
-
);
|
|
288
|
+
assert_evidence_lane_intact(&o, &ev);
|
|
227
289
|
|
|
228
290
|
// Witness 3 first: without it, everything after is consistent with a child
|
|
229
291
|
// that never ran.
|
|
@@ -256,18 +318,14 @@ fn an_undeclared_destination_is_refused_and_a_declared_one_is_not() {
|
|
|
256
318
|
/// The negative control: with the same probe and the undeclared port DECLARED,
|
|
257
319
|
/// the connection must go through.
|
|
258
320
|
///
|
|
259
|
-
/// This is what makes the
|
|
260
|
-
///
|
|
321
|
+
/// This is what makes the refusal phase non-vacuous. If this one ever fails,
|
|
322
|
+
/// the refusal stops meaning anything - not because containment broke, but
|
|
261
323
|
/// because the probe stopped reaching the network at all, and a test that
|
|
262
324
|
/// cannot connect when it is allowed to cannot prove anything by failing to
|
|
263
325
|
/// connect when it is not.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
let declared = Oracle::bind().expect("bind the declared oracle");
|
|
270
|
-
let undeclared = Oracle::bind().expect("bind the second oracle");
|
|
326
|
+
fn assert_both_declared_destinations_connect(host: Ipv4Addr) {
|
|
327
|
+
let declared = Oracle::bind(host).expect("bind the declared oracle");
|
|
328
|
+
let undeclared = Oracle::bind(host).expect("bind the second oracle");
|
|
271
329
|
|
|
272
330
|
let o = probe_run(
|
|
273
331
|
&declared,
|
|
@@ -277,6 +335,7 @@ fn the_same_probe_connects_when_the_destination_is_declared() {
|
|
|
277
335
|
let ev = evidence("negative-control", &o);
|
|
278
336
|
|
|
279
337
|
assert!(o.outcome.is_contained(), "the run was not contained. {ev}");
|
|
338
|
+
assert_evidence_lane_intact(&o, &ev);
|
|
280
339
|
assert!(
|
|
281
340
|
o.declared_sightings >= 1 && o.undeclared_sightings >= 1,
|
|
282
341
|
"both destinations were declared, so both must have been reached; a probe that \
|