//! Credentials as references, never values (`graph::secret`): a canary credential put in the //! store and referred to by a Particle node must occur nowhere a value leaves a run or a //! document: the steps a run returns, its log, its console, the files it wrote, the //! `.flowrs` and `.fdsl` texts, and the generated program. Each test follows one channel a //! value can take from the node that holds the credential, as `tests/taint.rs` does for //! neural data. use flowrs_core::graph::runtime::Runtime; use flowrs_core::graph::secret::{self, MARKER, Secret, SecretTaint}; use flowrs_core::graph::widget::{ExecContext, WidgetRegistry, builtin_registry}; use flowrs_core::graph::{Graph, GraphExecutor, NodeId}; use flowrs_core::{Value, WireMode}; use std::collections::HashMap; use std::sync::Arc; /// The canary. Built at run time so the source carries no credential-shaped literal. fn canary() -> String { format!("{}-{}-{}", "flowrs", "canary", "7f3a9c41d2e8b6aa") } /// The name the canary is stored under. const NAME: &str = "PARTICLE_CANARY"; fn runtime() -> Arc { let rt = Runtime::new(); rt.put_secret(NAME, Secret::new(canary())).unwrap(); rt } struct G { g: Graph, } impl G { fn new() -> Self { G { g: Graph::new() } } fn node(&mut self, ty: &str, props: &[(&str, &str)]) -> NodeId { let n = self.g.add_node(ty, 0.0, 0.0); for (k, v) in props { self.g .nodes .get_mut(&n) .unwrap() .properties .insert((*k).into(), (*v).into()); } n } fn wire(&mut self, a: NodeId, ap: &str, b: NodeId, bp: &str) { self.g.add_wire(a, ap, b, bp, WireMode::Clone).unwrap(); } /// A Particle function call holding the canary by reference. fn source(&mut self) -> NodeId { let reference = secret::make_reference(NAME); self.node( "cloud.particle.function", &[ ("token", reference.as_str()), ("device_id", "dev"), ("function", "led"), ], ) } } struct Run { outputs: HashMap<(NodeId, String), Value>, taint: SecretTaint, console: String, log: String, written: String, unexecuted: String, } fn run(g: &Graph, rt: &Arc, withhold: bool) -> Run { let mut ex = GraphExecutor::with_runtime(WidgetRegistry::with_builtins(), rt.clone()).withhold(withhold); let before = rt.log().events.len(); let steps = ex.run(g).expect("runs"); let mut outputs = HashMap::new(); let mut inputs = String::new(); for s in steps { inputs.push_str(&format!("{:?}", s.inputs)); for (p, v) in s.outputs { outputs.insert((s.node_id, p), v); } } let log = format!("{:?}{inputs}", &rt.log().events[before..]); Run { outputs, taint: ex.secret_taint().clone(), console: ex.console().to_string(), log, // As text: a `Vec`'s `Debug` would hide a credential from the check. written: ex .written() .iter() .map(|(k, v)| format!("{k}={}", String::from_utf8_lossy(v))) .collect::>() .join("\n"), unexecuted: format!("{:?}", ex.unexecuted()), } } /// No form of the canary (itself, JSON-escaped, percent-encoded, base64) in `text`. fn assert_clean(text: &str, what: &str) { assert!( !secret::holds(text, &[canary()]), "{what} carries the credential: {text}" ); } fn assert_run_clean(r: &Run) { assert_clean(&format!("{:?}", r.outputs), "the steps"); assert_clean(&r.log, "the log"); assert_clean(&r.console, "the console"); assert_clean(&r.written, "the files written"); assert_clean(&r.unexecuted, "the reasons a node did not run"); } /// Whether `v` is the marker. fn is_marker(v: &Value) -> bool { matches!(v, Value::String(s) if s == MARKER) } #[test] fn a_reference_resolves_from_the_store_at_t0_and_never_in_an_agents_run() { let rt = runtime(); let params = HashMap::from([("token".to_string(), secret::make_reference(NAME))]); let ctx = ExecContext::new(params.clone()).with_runtime(rt.clone()); assert_eq!(ctx.secret("token"), canary()); let mut agent = ExecContext::new(params).with_runtime(rt.clone()); agent.secrets_withheld = true; assert_eq!(agent.secret("token"), "", "an agent's run resolves nothing"); let missing = ExecContext::new(HashMap::from([( "token".to_string(), "secret:NOT_SET".to_string(), )])) .with_runtime(rt.clone()); assert_eq!( missing.secret("token"), "", "a name not in the store is empty" ); let typed = ExecContext::new(HashMap::from([("token".to_string(), "typed".to_string())])); assert_eq!( typed.secret("token"), "typed", "an old document's value still runs" ); assert!(rt.put_secret("not a name", Secret::new("x")).is_err()); assert_eq!(rt.secret_names(), [NAME]); } #[test] fn a_secret_is_withheld_from_the_replay_log() { let rt = runtime(); let mut g = G::new(); let src = g.source(); let p = run(&g.g, &rt, false); assert!(p.taint.sources.contains(&src)); assert!(p.taint.port(src, "auth"), "auth carries the token"); assert!(!p.taint.port(src, "url"), "the url does not"); assert!(is_marker(&p.outputs[&(src, "auth".into())])); assert!( matches!(&p.outputs[&(src, "url".into())], Value::String(u) if u.contains("/v1/devices/dev/led")) ); assert!( p.log.contains(MARKER), "the log records the marker where the value was" ); assert!( p.log.contains(&secret::make_reference(NAME)), "the log keeps the reference, which is a name" ); assert_run_clean(&p); // The timeline (probes over the log) reads the same marker. let tl = flowrs_core::graph::timeline::Timeline::new(&rt.log()); assert_clean(&format!("{tl:?}"), "the timeline"); // The whole log, setting the credential included, holds no value. assert_clean(&format!("{:?}", rt.log()), "the log from the start"); // And it replays: the log names which credentials were set, never their values, and // every value derived from one is a marker in both runs. flowrs_core::graph::runtime::replay(&rt.log(), &g.g, WidgetRegistry::with_builtins()) .expect("a log with a credential replays"); } #[test] fn taint_follows_a_secret_through_io_format() { let rt = runtime(); let mut g = G::new(); let src = g.source(); let fmt = g.node("io.format", &[("format", "header: {}")]); g.wire(src, "auth", fmt, "value"); let upper = g.node("text.case", &[]); g.wire(fmt, "string", upper, "text"); let len = g.node("text.length", &[]); g.wire(upper, "out", len, "text"); let print = g.node("io.print", &[]); g.wire(fmt, "string", print, "value"); let print2 = g.node("PrintLn", &[]); g.wire(upper, "out", print2, "input"); // A neighbour free of the credential is untouched. let hello = g.node("StringLiteral", &[("value", "hello")]); let print3 = g.node("PrintLn", &[]); g.wire(hello, "value", print3, "input"); let p = run(&g.g, &rt, false); for (n, port) in [(fmt, "string"), (upper, "out"), (len, "length")] { assert!(p.taint.port(n, port), "node {} {port}", n.0); if let Some(v) = p.outputs.get(&(n, port.to_string())) { assert!(is_marker(v), "node {} {port} shows the marker", n.0); } } assert!(p.taint.node(print) && p.taint.node(print2)); assert!(!p.taint.node(print3) && !p.taint.node(hello)); assert!(p.console.contains("hello"), "{}", p.console); assert!( p.console.contains(secret::CONSOLE_LINE.trim()), "{}", p.console ); assert_run_clean(&p); } #[test] fn a_secret_never_appears_in_generated_code() { let mut g = G::new(); let src = g.source(); let fmt = g.node("io.format", &[]); g.wire(src, "auth", fmt, "value"); let detect = g.node( "cloud.roboflow.detect", &[("api_key", "secret:ROBOFLOW_KEY"), ("model", "cars")], ); // A credential typed into the document: compiled as a read of a derived name. let typed = format!("typed-{}", canary()); let old = g.node("cloud.particle.variable", &[("token", typed.as_str())]); let program = g.g.to_rust().expect("generates"); assert_clean(&program, "the program"); assert!( !program.contains(&typed), "the typed value is not compiled in" ); assert!(program.contains("std::env::var(\"FLOWRS_SECRET_PARTICLE_CANARY\")")); assert!(program.contains("std::env::var(\"FLOWRS_SECRET_ROBOFLOW_KEY\")")); let derived = secret::env_var(&secret::derived_name(old, "token")); assert!( program.contains(&format!("std::env::var({derived:?})")), "{derived} read: {program}" ); // The other assembler, the profile-driven one, the same way. let profiled = flowrs_core::codegen::rust::RustCodegen::generate( &g.g, &WidgetRegistry::with_builtins(), &flowrs_core::graph::profile::Profile::learning(), ) .expect("generates"); assert!( !profiled.contains(&typed), "the typed value is not compiled in" ); assert!( profiled.contains(&format!("std::env::var({derived:?})")), "{derived} read: {profiled}" ); let _ = detect; } #[test] fn export_writes_a_reference_only() { let rt = runtime(); let mut g = G::new(); let src = g.source(); let fmt = g.node("io.format", &[]); g.wire(src, "auth", fmt, "value"); // Run it, so the store's value has been used, then write every text a graph leaves as. let p = run(&g.g, &rt, false); assert_run_clean(&p); let flowrs = flowrs_core::graph::document::save(&g.g); let fdsl = flowrs_core::graph::dsl::print::print(&g.g, builtin_registry()); let program = g.g.to_rust().unwrap(); for (what, text) in [ (".flowrs", &flowrs), (".fdsl", &fdsl), ("the program", &program), ] { assert_clean(text, what); } assert!(flowrs.contains("secret:PARTICLE_CANARY"), "{flowrs}"); assert!(fdsl.contains("secret:PARTICLE_CANARY"), "{fdsl}"); // Both read back to the same reference. let back = flowrs_core::graph::document::load(&flowrs).unwrap(); assert_eq!( back.nodes[&src].properties["token"], secret::make_reference(NAME) ); let parsed = flowrs_core::graph::dsl::parse_graph(&fdsl).unwrap().graph; assert!( parsed .nodes .values() .any(|n| n.properties.get("token") == Some(&secret::make_reference(NAME))) ); } #[test] fn an_old_document_with_a_literal_token_loads_and_gets_a_finding() { let typed = format!("old-{}", canary()); let mut g = G::new(); let src = g.node( "cloud.particle.function", &[("token", typed.as_str()), ("device_id", "dev")], ); let fmt = g.node("io.format", &[]); g.wire(src, "auth", fmt, "value"); let text = flowrs_core::graph::document::save(&g.g); // It loads, and saves back byte for byte: the document is the person's. let loaded = flowrs_core::graph::document::load(&text).expect("an old document loads"); assert_eq!(flowrs_core::graph::document::save(&loaded), text); // The diagnostics say so on the node, without repeating the value. let diags = flowrs_core::graph::diagnostics::check(&loaded); let finding = diags .iter() .find(|d| d.node == src && d.message.contains("credential's value")) .expect("a finding on the node"); assert_eq!( finding.severity, flowrs_core::graph::diagnostics::Severity::Warning, "a warning: the node still runs" ); assert!(!finding.message.contains(&typed)); assert!(finding.message.contains("secret:NAME")); // It still runs on the value, and no run shows it. let rt = Runtime::new(); let p = run(&loaded, &rt, false); assert!(p.taint.sources.contains(&src)); assert!(!format!("{:?}{}{}", p.outputs, p.log, p.console).contains(&typed)); assert!( p.log.contains(MARKER), "the parameter is logged as the marker" ); // An agent sees it withheld, and the program never compiles it in. let shown = secret::redacted(&loaded, builtin_registry()); assert!(!flowrs_core::graph::document::save(&shown).contains(&typed)); assert!(!loaded.to_rust().unwrap().contains(&typed)); assert_eq!(secret::literal_values(&loaded, builtin_registry()), [typed]); } #[test] fn a_held_wire_carries_a_secret_to_the_next_step() { // The token's length, summed on a held wire: what the next step reads came from it. let rt = runtime(); let mut g = G::new(); let src = g.source(); let len = g.node("text.length", &[]); g.wire(src, "auth", len, "text"); let add = g.node("Add", &[]); g.wire(len, "length", add, "a"); let fb = g.g.add_wire(add, "result", add, "b", WireMode::Copy) .unwrap(); g.g.wires.get_mut(&fb).unwrap().feedback = true; let mut ex = GraphExecutor::with_runtime(WidgetRegistry::with_builtins(), rt.clone()); let before = rt.log().events.len(); let first = ex.step(&g.g).unwrap(); let key = flowrs_core::graph::held::key(&g.g.wires[&fb]); // The real value is held, as the program holds it: the run computed on the credential. let want = ("Bearer ".len() + canary().len()) as i64; assert!( matches!(rt.held.get(&key), Some(Value::Int(n)) if n == want), "the held value is computed on the real token: {:?}", rt.held.get(&key) ); let second = ex.step(&g.g).unwrap(); assert!( ex.secret_taint().port(add, "result"), "the reader is tainted" ); assert!(ex.secret_taint().input(add, "b"), "through the held wire"); let shown = format!("{:?}{:?}", step_outputs(&first), step_outputs(&second)); assert_clean(&shown, "the steps"); assert!( !shown.contains(&format!("Int({want})")), "the steps show no sum: {shown}" ); let log = format!("{:?}", &rt.log().events[before..]); assert!( !log.contains(&format!("Int({want})")), "the log holds no sum: {log}" ); rt.clear("held"); } fn step_outputs(steps: &[flowrs_core::graph::executor::ExecutionStep]) -> Vec { steps .iter() .map(|s| format!("{:?} {:?}", s.inputs, s.outputs)) .collect() } #[test] fn a_loop_steered_by_a_secret_taints_its_body() { let rt = runtime(); let mut g = G::new(); let src = g.source(); let split = g.node("text.split", &[("separator", " ")]); g.wire(src, "auth", split, "text"); let lp = g.node("ForLoop", &[]); g.wire(split, "parts", lp, "iterator"); let v = g.node("VecNew", &[]); let push = g.node("VecPush", &[]); g.wire(v, "vec", push, "vec"); let one = g.node("IntLiteral", &[("value", "1")]); g.wire(one, "value", push, "item"); g.wire(push, "vec", lp, "body"); let p = run(&g.g, &rt, false); assert!(p.taint.node(lp), "the loop ran on the credential"); assert!(p.taint.port(push, "vec"), "its body did"); assert_run_clean(&p); } #[test] fn the_program_scope_and_a_file_the_run_wrote() { let rt = runtime(); let mut g = G::new(); let src = g.source(); let block = g.node("CodeBlock", &[("src", "format!(\"t,{}\\n1,2\\n\", input)")]); g.wire(src, "auth", block, "inputs"); let name = g.node("StringLiteral", &[("value", "leak.csv")]); let write = g.node("WriteFile", &[]); g.wire(name, "value", write, "path"); g.wire(block, "outputs", write, "contents"); // A code block wired to nothing that carries the credential reads the scope. let later = g.node("CodeBlockNode", &[("src", "println!(\"{}\", input);")]); // A widget that loads the file the run wrote: no wire ties it to the credential. let load = g.node("data.csv.load", &[("path", "leak.csv")]); let name2 = g.node("StringLiteral", &[("value", "leak.csv")]); let read = g.node("ReadFile", &[]); g.wire(name2, "value", read, "path"); let p = run(&g.g, &rt, false); assert!(p.taint.node(block) && p.taint.node(write)); assert!(p.taint.node(later), "the scope carried it"); assert!(p.taint.node(read), "the file carried it to ReadFile"); assert!( p.outputs.contains_key(&(load, "data".to_string())), "the loader ran after the write: {:?}", p.unexecuted ); assert!( p.taint.port(load, "data"), "the file carried it to the loader" ); assert!( p.written.contains(MARKER), "the written file is withheld: {}", p.written ); assert_run_clean(&p); } #[test] fn an_in_place_write_back_reaches_the_bindings_other_readers() { // `v.push(auth)` then `v.len()` through the wire `v` came on. let rt = runtime(); let mut g = G::new(); let src = g.source(); let v = g.node("VecNew", &[]); let push = g.node("VecPush", &[]); g.wire(v, "vec", push, "vec"); g.wire(src, "auth", push, "item"); let len = g.node("VecLen", &[]); g.wire(v, "vec", len, "vec"); let p = run(&g.g, &rt, false); assert!(p.taint.port(len, "length"), "the binding's other reader"); assert_run_clean(&p); } #[test] fn an_agent_run_resolves_no_credential() { let rt = runtime(); let mut g = G::new(); let src = g.source(); let len = g.node("text.length", &[]); g.wire(src, "auth", len, "text"); let a = run(&g.g, &rt, true); assert!(a.taint.port(len, "length"), "still tainted"); assert!(is_marker(&a.outputs[&(src, "auth".into())])); assert_run_clean(&a); // What an agent's step holds was computed on nothing: the token's length, summed on a // held wire, is the length of the scheme alone. The person's step holds the real one. let mut h = G::new(); let src = h.source(); let len = h.node("text.length", &[]); h.wire(src, "auth", len, "text"); let add = h.node("Add", &[]); h.wire(len, "length", add, "a"); let fb = h.g.add_wire(add, "result", add, "b", WireMode::Copy) .unwrap(); h.g.wires.get_mut(&fb).unwrap().feedback = true; let key = flowrs_core::graph::held::key(&h.g.wires[&fb]); for (withhold, want) in [ (true, "Bearer ".len() as i64), (false, ("Bearer ".len() + canary().len()) as i64), ] { rt.clear("held"); let mut ex = GraphExecutor::with_runtime(WidgetRegistry::with_builtins(), rt.clone()) .withhold(withhold); ex.step(&h.g).unwrap(); assert!( matches!(rt.held.get(&key), Some(Value::Int(n)) if n == want), "withhold={withhold}: {:?}", rt.held.get(&key) ); } rt.clear("held"); } #[test] fn a_graph_without_credentials_is_untouched() { let rt = runtime(); let mut g = G::new(); let hello = g.node("StringLiteral", &[("value", "hello")]); let print = g.node("PrintLn", &[]); g.wire(hello, "value", print, "input"); // A Particle node with no token set brings nothing in. let unset = g.node("cloud.particle.devices", &[]); let p = run(&g.g, &rt, false); assert!(!p.taint.any()); assert!(matches!(&p.outputs[&(unset, "auth".into())], Value::String(a) if a == "Bearer ")); assert!(!p.log.contains(MARKER)); assert_eq!(p.console, "hello\n"); } #[test] fn an_agent_may_not_write_a_credentials_value() { let reg = WidgetRegistry::with_builtins(); let mut g = Graph::new(); let n = g.add_node("cloud.particle.function", 0.0, 0.0); let typed = format!("agent-{}", canary()); let err = flowrs_core::graph::agent::set_declared_properties( &mut g, ®, n, &[("token".into(), typed.clone())], ) .unwrap_err(); assert!(err.contains("secret:NAME"), "{err}"); assert!( !err.contains(&typed), "the refusal does not repeat the value" ); assert!(g.nodes[&n].properties.is_empty(), "unchanged"); flowrs_core::graph::agent::set_declared_properties( &mut g, ®, n, &[("token".into(), "secret:PARTICLE".into())], ) .expect("a reference is written"); } /// Every parameter whose name says it holds a credential is declared one, with an empty /// default; every declared one is a real parameter. #[test] fn every_credential_parameter_is_declared() { let shaped = regex_lite::Regex::new( r"(?i)(^|_)(token|api_?key|apikey|password|passwd|secret|credential|bearer|private_?key|access_?key|client_?secret)($|_)", ) .unwrap(); let mut declared = 0usize; for w in builtin_registry().all() { let id = w.id(); let params = w.parameters(); for p in ¶ms { // A tokenizer is not a token. if shaped.is_match(&p.name) { assert!( w.secret_parameters().contains(&p.name.as_str()), "{id}.{} reads like a credential and is not declared one", p.name ); } } for s in w.secret_parameters() { declared += 1; let p = params .iter() .find(|p| p.name == *s) .unwrap_or_else(|| panic!("{id} declares credential {s} but no such parameter")); assert!(p.default.is_empty(), "{id}.{s} has a default credential"); assert_eq!(p.ty, "String", "{id}.{s}"); } if let Some(outs) = w.secret_outputs() { assert!(!w.secret_parameters().is_empty(), "{id}"); let names: Vec = w.outputs().into_iter().map(|p| p.name).collect(); for o in outs { assert!(names.iter().any(|n| n == o), "{id} names no output {o}"); } } } assert_eq!(declared, 6, "five Particle tokens and Roboflow's key"); } /// Every widget with a credential parameter, run with the canary set: every output it does /// not name as carrying the credential is free of it in every encoding, and the ones it /// names do carry it (or there is no point naming them). #[test] fn every_credential_bearing_widget_keeps_it_to_the_outputs_it_names() { let rt = runtime(); let mut checked = 0usize; for w in builtin_registry().all() { if w.secret_parameters().is_empty() { continue; } let id = w.id(); let mut props: HashMap = w .parameters() .into_iter() .map(|p| (p.name, p.default)) .collect(); for s in w.secret_parameters() { props.insert((*s).to_string(), secret::make_reference(NAME)); } let ctx = ExecContext::for_widget(w, &props).with_runtime(rt.clone()); // Every text input wired, so a widget that echoes an input cannot hide behind an // empty one. let inputs: HashMap = w .inputs() .into_iter() .map(|p| (p.name, Value::String("x".into()))) .collect(); let out = w.execute(&ctx, inputs); let named = w.secret_outputs().unwrap_or(&[]); let mut carried = false; for (port, v) in &out { let text = format!("{v:?}"); if named.contains(&port.as_str()) || w.secret_outputs().is_none() { carried |= secret::holds(&text, &[canary()]); continue; } assert!( !secret::holds(&text, &[canary()]), "{id}.{port} carries the credential and is not named in secret_outputs" ); } assert!( carried, "{id}: the credential reached none of its named outputs" ); checked += 1; } assert_eq!(checked, 6); } /// The edit checker refuses an agent's edit that writes a credential's value, and lets one /// through that writes a reference; a value the person's document already held is theirs, /// said by the diagnostics, and not the edit's to fix. #[test] fn an_agents_edit_may_not_write_a_credentials_value() { use flowrs_core::graph::verify::{self, Expect, InBrowser, Stage, Status}; let mut g = Graph::new(); let n = g.add_node("cloud.particle.function", 0.0, 0.0); g.nodes .get_mut(&n) .unwrap() .properties .insert("token".into(), "secret:PARTICLE".into()); let typed = format!("edit-{}", canary()); let refused = verify::check_patch( &g, &format!("set n{} {{ token: {typed:?} }};", n.0), builtin_registry(), &InBrowser, &Expect::default(), ); let v = &refused.verification; // Refused where the patch is applied (`set_declared_properties`). assert!( matches!(v.status(Stage::Apply), Some(Status::Failed(_))), "{}", v.feedback() ); let f = v .errors() .find(|f| f.message.contains("credential parameter")) .expect("the refusal names the parameter"); assert!(!f.message.contains(&typed)); // Nothing the checker reports (its findings, its evidence) repeats the value. let reported = serde_json::to_string(&v.to_json()).unwrap(); assert!(!reported.contains(&typed), "{reported}"); let fine = verify::check_patch( &g, &format!("set n{} {{ token: \"secret:OTHER\" }};", n.0), builtin_registry(), &InBrowser, &Expect::default(), ); assert!( fine.verification .errors() .all(|f| !f.message.contains("credential parameter")), "{}", fine.verification.summary() ); // A value the document already held: the edit touches something else and passes it by. let mut old = g.clone(); old.nodes .get_mut(&n) .unwrap() .properties .insert("token".into(), typed.clone()); let other = verify::check_patch( &old, &format!("set n{} {{ device_id: \"lab\" }};", n.0), builtin_registry(), &InBrowser, &Expect::default(), ); assert!( other .verification .errors() .all(|f| !f.message.contains("credential parameter")), "{}", other.verification.summary() ); let reported = serde_json::to_string(&other.verification.to_json()).unwrap(); assert!( !reported.contains(&typed), "the old value is not echoed: {reported}" ); // A whole graph an agent hands back is held to it at the structure stage: a value it // wrote fails, one the base already held does not. let origin = flowrs_core::graph::dsl::Origin::default(); let whole = verify::check_graph( Some(&g), &old, &origin, builtin_registry(), &InBrowser, &Expect::default(), ); assert!( matches!(whole.status(Stage::Structure), Some(Status::Failed(_))), "{}", whole.feedback() ); assert!( whole .errors() .any(|f| f.message.contains("credential parameter")) ); let kept = verify::check_graph( Some(&old), &old, &origin, builtin_registry(), &InBrowser, &Expect::default(), ); assert!( kept.errors() .all(|f| !f.message.contains("credential parameter")), "{}", kept.feedback() ); } /// A password typed into a front-panel control is saved as the literal's value: the /// diagnostics say so, and no run returns it, whatever it flows into. #[test] fn a_password_control_is_said_and_removed_by_value() { let typed = format!("pw-{}", canary()); let mut g = G::new(); let quoted = format!("{typed:?}"); let ctl = g.node( "StringLiteral", &[("value", quoted.as_str()), ("widget", "password")], ); let fmt = g.node("io.format", &[("format", "login {}")]); g.wire(ctl, "value", fmt, "value"); let print = g.node("PrintLn", &[]); g.wire(fmt, "string", print, "input"); assert_eq!(secret::password_controls(&g.g), [(ctl, typed.clone())]); let d = flowrs_core::graph::diagnostics::check(&g.g); assert!( d.iter() .any(|d| d.node == ctl && d.message.contains("password control")) ); let p = run(&g.g, &Runtime::new(), false); let all = format!("{:?}{}{}", p.outputs, p.log, p.console); assert!(!all.contains(&typed), "{all}"); assert!( p.console.contains("login"), "the run still ran: {}", p.console ); } /// An evidence bundle names a credential parameter by its reference, never a value: one an /// edit wrote is refused before evidence is gathered, and this holds the bundle to it even if /// one were gathered. #[test] fn an_evidence_bundle_carries_no_credential() { use flowrs_core::graph::evidence::{self, Bench, Budget, Context, Edit}; use flowrs_core::graph::verify::{self, Expect, InBrowser}; let typed = format!("ev-{}", canary()); let mut base = Graph::new(); let n = base.add_node("cloud.particle.function", 0.0, 0.0); let mut edited = base.clone(); for (g, token) in [ (&mut base, "secret:PARTICLE"), (&mut edited, typed.as_str()), ] { g.nodes .get_mut(&n) .unwrap() .properties .insert("token".into(), token.to_string()); } let origin = flowrs_core::graph::dsl::Origin::default(); let v = verify::check_graph( Some(&base), &edited, &origin, builtin_registry(), &InBrowser, &Expect::default(), ); let changes = flowrs_core::graph::diff::diff(&base, &edited); let bundle = evidence::gather( &Edit { base: &base, graph: &edited, changes: &changes, origin: &origin, registry: builtin_registry(), verification: &v, }, &InBrowser, &Context { bench: Bench::default(), budget: Budget::default(), }, ); let text = serde_json::to_string(&bundle).unwrap(); assert!( text.contains("token"), "the bundle speaks of the parameter: {text}" ); assert!(!text.contains(&typed), "{text}"); }