@@ -537,6 +537,7 @@ fn merge_endpoint(
537537 existing. allow_encoded_slash |= incoming. allow_encoded_slash ;
538538 existing. websocket_credential_rewrite |= incoming. websocket_credential_rewrite ;
539539 existing. request_body_credential_rewrite |= incoming. request_body_credential_rewrite ;
540+ existing. advisor_proposed |= incoming. advisor_proposed ;
540541 normalize_endpoint ( existing) ;
541542 Ok ( ( ) )
542543}
@@ -723,6 +724,12 @@ fn expand_access_preset(protocol: &str, access: &str) -> Option<Vec<L7Rule>> {
723724fn append_unique_binaries ( existing : & mut Vec < NetworkBinary > , incoming : & [ NetworkBinary ] ) {
724725 let mut seen: HashSet < String > = existing. iter ( ) . map ( |binary| binary. path . clone ( ) ) . collect ( ) ;
725726 for binary in incoming {
727+ if let Some ( existing_binary) = existing. iter_mut ( ) . find ( |item| item. path == binary. path ) {
728+ if !is_advisor_proposed_binary ( binary) {
729+ mark_user_declared_binary ( existing_binary) ;
730+ }
731+ continue ;
732+ }
726733 if seen. insert ( binary. path . clone ( ) ) {
727734 existing. push ( binary. clone ( ) ) ;
728735 }
@@ -778,8 +785,30 @@ fn dedup_strings(values: &mut Vec<String>) {
778785}
779786
780787fn dedup_binaries ( values : & mut Vec < NetworkBinary > ) {
781- let mut seen = HashSet :: new ( ) ;
782- values. retain ( |binary| seen. insert ( binary. path . clone ( ) ) ) ;
788+ let mut deduped: Vec < NetworkBinary > = Vec :: with_capacity ( values. len ( ) ) ;
789+ for binary in std:: mem:: take ( values) {
790+ if let Some ( existing) = deduped. iter_mut ( ) . find ( |item| item. path == binary. path ) {
791+ if !is_advisor_proposed_binary ( & binary) {
792+ mark_user_declared_binary ( existing) ;
793+ }
794+ } else {
795+ deduped. push ( binary) ;
796+ }
797+ }
798+ * values = deduped;
799+ }
800+
801+ fn is_advisor_proposed_binary ( binary : & NetworkBinary ) -> bool {
802+ #[ allow( deprecated) ]
803+ let advisor_proposed = binary. harness ;
804+ advisor_proposed
805+ }
806+
807+ fn mark_user_declared_binary ( binary : & mut NetworkBinary ) {
808+ #[ allow( deprecated) ]
809+ {
810+ binary. harness = false ;
811+ }
783812}
784813
785814fn dedup_l7_rules ( values : & mut Vec < L7Rule > ) {
@@ -878,6 +907,18 @@ mod tests {
878907 }
879908 }
880909
910+ fn advisor_binary ( path : & str ) -> NetworkBinary {
911+ let mut binary = NetworkBinary {
912+ path : path. to_string ( ) ,
913+ ..Default :: default ( )
914+ } ;
915+ #[ allow( deprecated) ]
916+ {
917+ binary. harness = true ;
918+ }
919+ binary
920+ }
921+
881922 fn rest_rule ( method : & str , path : & str ) -> L7Rule {
882923 L7Rule {
883924 allow : Some ( L7Allow {
@@ -949,6 +990,131 @@ mod tests {
949990 assert_eq ! ( rule. binaries. len( ) , 2 ) ;
950991 }
951992
993+ #[ test]
994+ fn add_rule_user_binary_clears_advisor_marker_for_same_path ( ) {
995+ let mut policy = restrictive_default_policy ( ) ;
996+ policy. network_policies . insert (
997+ "existing" . to_string ( ) ,
998+ NetworkPolicyRule {
999+ name : "existing" . to_string ( ) ,
1000+ endpoints : vec ! [ endpoint( "api.github.com" , 443 ) ] ,
1001+ binaries : vec ! [ advisor_binary( "/usr/bin/curl" ) ] ,
1002+ } ,
1003+ ) ;
1004+
1005+ let incoming = NetworkPolicyRule {
1006+ name : "incoming" . to_string ( ) ,
1007+ endpoints : vec ! [ endpoint( "api.github.com" , 443 ) ] ,
1008+ binaries : vec ! [ NetworkBinary {
1009+ path: "/usr/bin/curl" . to_string( ) ,
1010+ ..Default :: default ( )
1011+ } ] ,
1012+ } ;
1013+
1014+ let result = merge_policy (
1015+ policy,
1016+ & [ PolicyMergeOp :: AddRule {
1017+ rule_name : "existing" . to_string ( ) ,
1018+ rule : incoming,
1019+ } ] ,
1020+ )
1021+ . expect ( "merge should succeed" ) ;
1022+
1023+ let rule = & result. policy . network_policies [ "existing" ] ;
1024+ assert_eq ! ( rule. binaries. len( ) , 1 ) ;
1025+ #[ allow( deprecated) ]
1026+ {
1027+ assert ! ( !rule. binaries[ 0 ] . harness) ;
1028+ }
1029+ }
1030+
1031+ #[ test]
1032+ fn add_rule_duplicate_binaries_prefer_user_declared_marker ( ) {
1033+ let incoming = NetworkPolicyRule {
1034+ name : "incoming" . to_string ( ) ,
1035+ endpoints : vec ! [ endpoint( "api.github.com" , 443 ) ] ,
1036+ binaries : vec ! [
1037+ advisor_binary( "/usr/bin/curl" ) ,
1038+ NetworkBinary {
1039+ path: "/usr/bin/curl" . to_string( ) ,
1040+ ..Default :: default ( )
1041+ } ,
1042+ ] ,
1043+ } ;
1044+
1045+ let result = merge_policy (
1046+ restrictive_default_policy ( ) ,
1047+ & [ PolicyMergeOp :: AddRule {
1048+ rule_name : "github" . to_string ( ) ,
1049+ rule : incoming,
1050+ } ] ,
1051+ )
1052+ . expect ( "merge should succeed" ) ;
1053+
1054+ let rule = & result. policy . network_policies [ "github" ] ;
1055+ assert_eq ! ( rule. binaries. len( ) , 1 ) ;
1056+ #[ allow( deprecated) ]
1057+ {
1058+ assert ! ( !rule. binaries[ 0 ] . harness) ;
1059+ }
1060+ }
1061+
1062+ #[ test]
1063+ fn add_rule_preserves_advisor_endpoint_marker_when_binary_is_deduped ( ) {
1064+ let mut policy = restrictive_default_policy ( ) ;
1065+ policy. network_policies . insert (
1066+ "app-api" . to_string ( ) ,
1067+ NetworkPolicyRule {
1068+ name : "app-api" . to_string ( ) ,
1069+ endpoints : vec ! [ endpoint( "api.example.com" , 443 ) ] ,
1070+ binaries : vec ! [ NetworkBinary {
1071+ path: "/usr/bin/python" . to_string( ) ,
1072+ ..Default :: default ( )
1073+ } ] ,
1074+ } ,
1075+ ) ;
1076+
1077+ let incoming = NetworkPolicyRule {
1078+ name : "app-api" . to_string ( ) ,
1079+ endpoints : vec ! [ NetworkEndpoint {
1080+ host: "internal-admin.local" . to_string( ) ,
1081+ port: 443 ,
1082+ ports: vec![ 443 ] ,
1083+ advisor_proposed: true ,
1084+ ..Default :: default ( )
1085+ } ] ,
1086+ binaries : vec ! [ advisor_binary( "/usr/bin/python" ) ] ,
1087+ } ;
1088+
1089+ let result = merge_policy (
1090+ policy,
1091+ & [ PolicyMergeOp :: AddRule {
1092+ rule_name : "app-api" . to_string ( ) ,
1093+ rule : incoming,
1094+ } ] ,
1095+ )
1096+ . expect ( "merge should succeed" ) ;
1097+
1098+ let rule = & result. policy . network_policies [ "app-api" ] ;
1099+ assert_eq ! ( rule. binaries. len( ) , 1 , "binary should still dedupe" ) ;
1100+ #[ allow( deprecated) ]
1101+ {
1102+ assert ! (
1103+ !rule. binaries[ 0 ] . harness,
1104+ "existing user binary provenance should be retained"
1105+ ) ;
1106+ }
1107+ let internal_endpoint = rule
1108+ . endpoints
1109+ . iter ( )
1110+ . find ( |endpoint| endpoint. host == "internal-admin.local" )
1111+ . expect ( "advisor endpoint should be appended" ) ;
1112+ assert ! (
1113+ internal_endpoint. advisor_proposed,
1114+ "endpoint provenance must survive merge even when binary provenance is deduped"
1115+ ) ;
1116+ }
1117+
9521118 #[ test]
9531119 fn add_rule_merges_websocket_credential_rewrite_flag ( ) {
9541120 let mut policy = restrictive_default_policy ( ) ;
0 commit comments