host/audit: name the owner key the bridge resolved on
Impl plan section 5.1 row 1 asserts "reason = owner bridge, naming the key",
and the record could not express that: Reason::code collapses
TaintedOwnerBridge { key } to one string, so an exclusion that arrived across a
named owner key was indistinguishable from one that arrived by an incidental
link walk reaching the same verdict. Telling those apart is the entire point of
the row.
OwnerKey::code already documented itself as ending up in the phase 5 audit
output; it was simply never wired to it. Adds owner_key to AuditRow and
TaintRow, omitted when absent, and absent is meaningful: a bridge whose tainted
member shared no key directly reached the node transitively, so there is no
single key to name and naming one would be a false diagnosis.
Read-only and diagnostic-only. New test mutation-verified (stubbing the key to
None fails it); 221 tests green, clippy clean.
This commit is contained in:
@@ -529,6 +529,53 @@ fn row_1_owner_bridge_forwarder_with_an_untainted_control() {
|
||||
);
|
||||
}
|
||||
|
||||
/// §5.1 row 1's other half: the record must **name the key** the bridge
|
||||
/// resolved on, not merely say "owner bridge".
|
||||
///
|
||||
/// Without this the row is unassertable from the record: `Reason::code`
|
||||
/// collapses `TaintedOwnerBridge { key }` to one string, so an exclusion that
|
||||
/// arrived by an incidental link walk and one that arrived across a named owner
|
||||
/// key are indistinguishable — and the row exists precisely to tell them apart.
|
||||
/// The fixture's forwarder legs are joined by `pulse.module.id`, so that is the
|
||||
/// key that must be reported.
|
||||
#[test]
|
||||
fn row_1_names_the_owner_key_the_bridge_resolved_on() {
|
||||
let mut graph = Graph::new();
|
||||
let call = graph.peerspeak_node("peerspeak-call", 200);
|
||||
let sink = graph.module_node("tainted-null-sink", MediaRole::Sink, 30);
|
||||
graph.link(call, sink);
|
||||
let capture = graph.module_node("tainted-loopback-capture", MediaRole::StreamInput, 30);
|
||||
let _playback = graph.module_node("tainted-loopback-playback", MediaRole::StreamOutput, 30);
|
||||
graph.link(sink, capture);
|
||||
|
||||
let body = observe(&mut auditor_off(), &ready(graph.build()), 0)
|
||||
.record
|
||||
.body;
|
||||
|
||||
let playback = body
|
||||
.candidates
|
||||
.iter()
|
||||
.find(|row| row.name.as_deref() == Some("tainted-loopback-playback"))
|
||||
.expect("the forwarder's playback leg is a candidate");
|
||||
assert_eq!(playback.reason, Some("tainted-owner-bridge"));
|
||||
assert_eq!(
|
||||
playback.owner_key,
|
||||
Some("pulse.module.id"),
|
||||
"the bridge key must be named in the record, not collapsed into the reason code"
|
||||
);
|
||||
|
||||
// And it stays absent everywhere it would be a false diagnosis: the tag
|
||||
// exclusion is not a bridge at all.
|
||||
let call_name = owned_name("peerspeak-call", 200);
|
||||
let tagged = body
|
||||
.candidates
|
||||
.iter()
|
||||
.find(|row| row.name.as_deref() == Some(call_name.as_str()))
|
||||
.expect("the tagged call playback is a candidate");
|
||||
assert_eq!(tagged.reason, Some("peerspeak-owned"));
|
||||
assert_eq!(tagged.owner_key, None);
|
||||
}
|
||||
|
||||
/// §5.1 row 3: two Pulse modules, one tainted input. **The other module's output
|
||||
/// must be eligible** — this is the row that makes a wrong pipewire-pulse-PID
|
||||
/// fusion observable, because fusing all Pulse-created nodes into one owner
|
||||
|
||||
Reference in New Issue
Block a user