@@ -16,14 +16,23 @@ use crate::registry::BinaryRegistry;
1616#[ derive( Debug , Clone , Hash , PartialEq , Eq ) ]
1717pub struct EndpointId {
1818 pub policy_name : String ,
19+ /// Position within the policy rule's endpoint list.
20+ pub endpoint_index : usize ,
1921 pub host : String ,
2022 pub port : u16 ,
2123}
2224
2325impl EndpointId {
2426 /// Stable string key used for Z3 variable naming.
2527 pub fn key ( & self ) -> String {
26- format ! ( "{}:{}:{}" , self . policy_name, self . host, self . port)
28+ // Length-prefix the authored name so delimiters cannot alias another rule.
29+ format ! (
30+ "{}:{}:{}:{}" ,
31+ self . policy_name. len( ) ,
32+ self . policy_name,
33+ self . endpoint_index,
34+ self . port
35+ )
2736 }
2837}
2938
@@ -97,10 +106,11 @@ impl ReachabilityModel {
97106
98107 fn index_endpoints ( & mut self ) {
99108 for ( policy_name, rule) in & self . policy . network_policies {
100- for ep in & rule. endpoints {
109+ for ( endpoint_index , ep ) in rule. endpoints . iter ( ) . enumerate ( ) {
101110 for port in ep. effective_ports ( ) {
102111 self . endpoints . push ( EndpointId {
103112 policy_name : policy_name. clone ( ) ,
113+ endpoint_index,
104114 host : ep. host . clone ( ) ,
105115 port,
106116 } ) ;
@@ -122,15 +132,16 @@ impl ReachabilityModel {
122132
123133 fn encode_policy_allows ( & mut self ) {
124134 for ( policy_name, rule) in & self . policy . network_policies {
125- for ep in & rule. endpoints {
135+ for ( endpoint_index , ep ) in rule. endpoints . iter ( ) . enumerate ( ) {
126136 for port in ep. effective_ports ( ) {
127137 let eid = EndpointId {
128138 policy_name : policy_name. clone ( ) ,
139+ endpoint_index,
129140 host : ep. host . clone ( ) ,
130141 port,
131142 } ;
132143 for b in & rule. binaries {
133- let key = format ! ( "{}:{}" , b. path, eid. key( ) ) ;
144+ let key = format ! ( "{}:{}:{}" , b . path . len ( ) , b. path, eid. key( ) ) ;
134145 let var = Bool :: new_const ( format ! ( "policy_allows_{key}" ) ) ;
135146 self . solver . assert ( & var) ;
136147 self . policy_allows . insert ( key, var) ;
@@ -142,10 +153,11 @@ impl ReachabilityModel {
142153
143154 fn encode_l7_enforcement ( & mut self ) {
144155 for ( policy_name, rule) in & self . policy . network_policies {
145- for ep in & rule. endpoints {
156+ for ( endpoint_index , ep ) in rule. endpoints . iter ( ) . enumerate ( ) {
146157 for port in ep. effective_ports ( ) {
147158 let eid = EndpointId {
148159 policy_name : policy_name. clone ( ) ,
160+ endpoint_index,
149161 host : ep. host . clone ( ) ,
150162 port,
151163 } ;
@@ -284,7 +296,7 @@ impl ReachabilityModel {
284296 /// Build a Z3 expression for whether a binary can write to an endpoint.
285297 pub fn can_write_to_endpoint ( & self , bpath : & str , eid : & EndpointId ) -> Bool {
286298 let ek = eid. key ( ) ;
287- let access_key = format ! ( "{bpath}:{ek}" ) ;
299+ let access_key = format ! ( "{}:{ bpath}:{ek}" , bpath . len ( ) ) ;
288300
289301 let has_access = match self . policy_allows . get ( & access_key) {
290302 Some ( v) => v. clone ( ) ,
@@ -328,7 +340,7 @@ impl ReachabilityModel {
328340 /// Build a Z3 expression for whether data can be exfiltrated via this path.
329341 pub fn can_exfil_via_endpoint ( & self , bpath : & str , eid : & EndpointId ) -> Bool {
330342 let ek = eid. key ( ) ;
331- let access_key = format ! ( "{bpath}:{ek}" ) ;
343+ let access_key = format ! ( "{}:{ bpath}:{ek}" , bpath . len ( ) ) ;
332344
333345 let has_access = match self . policy_allows . get ( & access_key) {
334346 Some ( v) => v. clone ( ) ,
@@ -372,6 +384,15 @@ impl ReachabilityModel {
372384 ] )
373385 }
374386
387+ /// Reject inconsistent or inconclusive base constraints before querying them.
388+ pub fn validate ( & self ) -> miette:: Result < ( ) > {
389+ match self . solver . check ( ) {
390+ SatResult :: Sat => Ok ( ( ) ) ,
391+ SatResult :: Unsat => miette:: bail!( "inconsistent reachability model" ) ,
392+ SatResult :: Unknown => miette:: bail!( "reachability model validation inconclusive" ) ,
393+ }
394+ }
395+
375396 /// Check satisfiability of an expression against the base constraints.
376397 pub fn check_sat ( & self , expr : & Bool ) -> SatResult {
377398 self . solver . push ( ) ;
@@ -392,3 +413,52 @@ pub fn build_model(
392413 let _ctx = Context :: thread_local ( ) ;
393414 ReachabilityModel :: new ( policy, credentials, binary_registry)
394415}
416+
417+ #[ cfg( test) ]
418+ mod tests {
419+ use super :: * ;
420+ use crate :: queries:: { check_credential_safety, run_all_queries} ;
421+
422+ #[ test]
423+ fn inconsistent_base_model_is_an_error_even_without_query_paths ( ) {
424+ let model = build_model (
425+ PolicyModel :: default ( ) ,
426+ CredentialSet :: default ( ) ,
427+ crate :: registry:: load_embedded_binary_registry ( ) . unwrap ( ) ,
428+ ) ;
429+ model. solver . assert ( Bool :: from_bool ( false ) ) ;
430+ assert ! ( check_credential_safety( & model) . is_err( ) ) ;
431+ assert ! ( run_all_queries( & model) . is_err( ) ) ;
432+ }
433+
434+ #[ test]
435+ fn inconclusive_base_model_is_an_error ( ) {
436+ let model = build_model (
437+ PolicyModel :: default ( ) ,
438+ CredentialSet :: default ( ) ,
439+ crate :: registry:: load_embedded_binary_registry ( ) . unwrap ( ) ,
440+ ) ;
441+ model. solver . assert ( Bool :: new_const ( "resource_limited" ) ) ;
442+ let mut params = z3:: Params :: new ( ) ;
443+ params. set_u32 ( "rlimit" , 1 ) ;
444+ model. solver . set_params ( & params) ;
445+ assert_eq ! ( model. solver. check( ) , SatResult :: Unknown ) ;
446+ assert ! ( run_all_queries( & model) . is_err( ) ) ;
447+ }
448+
449+ #[ test]
450+ fn endpoint_keys_distinguish_rule_names_with_delimiters ( ) {
451+ let left = EndpointId {
452+ policy_name : "rule:2001" . to_string ( ) ,
453+ endpoint_index : 0 ,
454+ host : "db8::1" . to_string ( ) ,
455+ port : 443 ,
456+ } ;
457+ let right = EndpointId {
458+ policy_name : "rule" . to_string ( ) ,
459+ host : "2001:db8::1" . to_string ( ) ,
460+ ..left
461+ } ;
462+ assert_ne ! ( left. key( ) , right. key( ) ) ;
463+ }
464+ }
0 commit comments