@@ -13,6 +13,57 @@ const SELINUX_CONFIG_PATH: &str = "etc/selinux/config";
1313const SELINUX_TYPE : & str = "SELINUXTYPE=" ;
1414const POLICY_FILE_PREFIX : & str = "policy." ;
1515
16+ /// Find the highest versioned policy file in the given directory
17+ fn find_latest_policy_file ( policy_dir : & Dir ) -> Result < String > {
18+ let mut highest_policy_version = -1 ;
19+ let mut latest_policy_name = None ;
20+
21+ for entry in policy_dir
22+ . entries_utf8 ( )
23+ . context ( "Getting policy dir entries" ) ?
24+ {
25+ let entry = entry?;
26+
27+ if !entry. file_type ( ) ?. is_file ( ) {
28+ // We don't want symlinks, another directory etc
29+ continue ;
30+ }
31+
32+ let filename = entry. file_name ( ) ?;
33+
34+ match filename. strip_prefix ( POLICY_FILE_PREFIX ) {
35+ Some ( version) => {
36+ let v_int = version
37+ . parse :: < i32 > ( )
38+ . with_context ( || anyhow:: anyhow!( "Parsing {version} as int" ) ) ?;
39+
40+ if v_int < highest_policy_version {
41+ continue ;
42+ }
43+
44+ highest_policy_version = v_int;
45+ latest_policy_name = Some ( filename. to_string ( ) ) ;
46+ }
47+
48+ None => continue ,
49+ } ;
50+ }
51+
52+ latest_policy_name. ok_or_else ( || anyhow:: anyhow!( "Failed to get latest SELinux policy" ) )
53+ }
54+
55+ /// Compute SHA256 hash of a policy file
56+ fn compute_policy_file_hash ( deployment_root : & Dir , full_path : & str ) -> Result < String > {
57+ let mut file = deployment_root
58+ . open ( full_path)
59+ . context ( "Opening policy file" ) ?;
60+ let mut hasher = openssl:: hash:: Hasher :: new ( openssl:: hash:: MessageDigest :: sha256 ( ) ) ?;
61+ std:: io:: copy ( & mut file, & mut hasher) ?;
62+
63+ let hash = hex:: encode ( hasher. finish ( ) . context ( "Computing hash" ) ?) ;
64+ Ok ( hash)
65+ }
66+
1667#[ context( "Getting SELinux policy for deployment {depl_id}" ) ]
1768fn get_selinux_policy_for_deployment (
1869 storage : & Storage ,
@@ -50,56 +101,15 @@ fn get_selinux_policy_for_deployment(
50101
51102 let policy_dir_path = format ! ( "etc/selinux/{type_}/policy" ) ;
52103
53- let mut highest_policy_version = -1 ;
54- let mut latest_policy_name = None ;
55-
56104 let policy_dir = deployment_root
57105 . open_dir ( & policy_dir_path)
58106 . context ( "Opening selinux policy dir" ) ?;
59107
60- for entry in policy_dir
61- . entries_utf8 ( )
62- . context ( "Getting policy dir entries" ) ?
63- {
64- let entry = entry?;
65-
66- if !entry. file_type ( ) ?. is_file ( ) {
67- // We don't want symlinks, another directory etc
68- continue ;
69- }
70-
71- let filename = entry. file_name ( ) ?;
72-
73- match filename. strip_prefix ( POLICY_FILE_PREFIX ) {
74- Some ( version) => {
75- let v_int = version
76- . parse :: < i32 > ( )
77- . with_context ( || anyhow:: anyhow!( "Parsing {version} as int" ) ) ?;
78-
79- if v_int < highest_policy_version {
80- continue ;
81- }
82-
83- highest_policy_version = v_int;
84- latest_policy_name = Some ( filename. to_string ( ) ) ;
85- }
86-
87- None => continue ,
88- } ;
89- }
90-
91- let policy_name =
92- latest_policy_name. ok_or_else ( || anyhow:: anyhow!( "Failed to get latest SELinux policy" ) ) ?;
108+ let policy_name = find_latest_policy_file ( & policy_dir) ?;
93109
94110 let full_path = format ! ( "{policy_dir_path}/{policy_name}" ) ;
95111
96- let mut file = deployment_root
97- . open ( full_path)
98- . context ( "Opening policy file" ) ?;
99- let mut hasher = openssl:: hash:: Hasher :: new ( openssl:: hash:: MessageDigest :: sha256 ( ) ) ?;
100- std:: io:: copy ( & mut file, & mut hasher) ?;
101-
102- let hash = hex:: encode ( hasher. finish ( ) . context ( "Computing hash" ) ?) ;
112+ let hash = compute_policy_file_hash ( & deployment_root, & full_path) ?;
103113
104114 Ok ( Some ( hash) )
105115}
@@ -134,3 +144,150 @@ pub(crate) fn are_selinux_policies_compatible(
134144
135145 Ok ( sl_policy_match)
136146}
147+
148+ #[ cfg( test) ]
149+ mod tests {
150+ use super :: * ;
151+ use cap_std_ext:: cap_std:: ambient_authority;
152+ use cap_std_ext:: dirext:: CapStdExtDirExt ;
153+
154+ #[ test]
155+ fn test_find_latest_policy_file ( ) -> Result < ( ) > {
156+ let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( ambient_authority ( ) ) ?;
157+
158+ // Create policy files with different versions
159+ tempdir. atomic_write ( "policy.30" , "policy content 30" ) ?;
160+ tempdir. atomic_write ( "policy.31" , "policy content 31" ) ?;
161+ tempdir. atomic_write ( "policy.29" , "policy content 29" ) ?;
162+ tempdir. atomic_write ( "not_policy.32" , "not a policy file" ) ?;
163+ tempdir. atomic_write ( "other_policy.txt" , "invalid policy file" ) ?;
164+
165+ let result = find_latest_policy_file ( & tempdir) ?;
166+ assert_eq ! ( result, "policy.31" ) ;
167+
168+ Ok ( ( ) )
169+ }
170+
171+ #[ test]
172+ fn test_find_latest_policy_file_with_single_file ( ) -> Result < ( ) > {
173+ let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( ambient_authority ( ) ) ?;
174+
175+ tempdir. atomic_write ( "policy.25" , "single policy file" ) ?;
176+
177+ let result = find_latest_policy_file ( & tempdir) ?;
178+ assert_eq ! ( result, "policy.25" ) ;
179+
180+ Ok ( ( ) )
181+ }
182+
183+ #[ test]
184+ fn test_find_latest_policy_file_no_policy_files ( ) {
185+ let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( ambient_authority ( ) ) . unwrap ( ) ;
186+
187+ tempdir. atomic_write ( "not_policy.txt" , "not a policy file" ) . unwrap ( ) ;
188+ tempdir. atomic_write ( "other.txt" , "invalid format" ) . unwrap ( ) ;
189+
190+ let result = find_latest_policy_file ( & tempdir) ;
191+ assert ! ( result. is_err( ) ) ;
192+ assert ! ( result. unwrap_err( ) . to_string( ) . contains( "Failed to get latest SELinux policy" ) ) ;
193+ }
194+
195+ #[ test]
196+ fn test_find_latest_policy_file_invalid_version ( ) {
197+ let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( ambient_authority ( ) ) . unwrap ( ) ;
198+
199+ tempdir. atomic_write ( "policy.abc" , "invalid version" ) . unwrap ( ) ;
200+
201+ let result = find_latest_policy_file ( & tempdir) ;
202+ assert ! ( result. is_err( ) ) ;
203+ assert ! ( result. unwrap_err( ) . to_string( ) . contains( "Parsing abc as int" ) ) ;
204+ }
205+
206+ #[ test]
207+ fn test_find_latest_policy_file_negative_version ( ) -> Result < ( ) > {
208+ let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( ambient_authority ( ) ) ?;
209+
210+ tempdir. atomic_write ( "policy.5" , "positive version" ) ?;
211+ tempdir. atomic_write ( "policy.-1" , "negative version" ) ?;
212+
213+ let result = find_latest_policy_file ( & tempdir) ?;
214+ assert_eq ! ( result, "policy.5" ) ;
215+
216+ Ok ( ( ) )
217+ }
218+
219+ #[ test]
220+ fn test_find_latest_policy_file_skips_directories ( ) -> Result < ( ) > {
221+ let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( ambient_authority ( ) ) ?;
222+
223+ tempdir. create_dir ( "policy.99" ) ?; // This should be skipped
224+ tempdir. atomic_write ( "policy.5" , "actual policy file" ) ?;
225+
226+ let result = find_latest_policy_file ( & tempdir) ?;
227+ assert_eq ! ( result, "policy.5" ) ;
228+
229+ Ok ( ( ) )
230+ }
231+
232+ #[ test]
233+ fn test_compute_policy_file_hash ( ) -> Result < ( ) > {
234+ let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( ambient_authority ( ) ) ?;
235+
236+ let test_content = "test policy content for hashing" ;
237+ tempdir. atomic_write ( "test_policy.30" , test_content) ?;
238+
239+ let hash = compute_policy_file_hash ( & tempdir, "test_policy.30" ) ?;
240+
241+ // Verify the hash is a valid SHA256 hash (64 hex characters)
242+ assert_eq ! ( hash. len( ) , 64 ) ;
243+ assert ! ( hash. chars( ) . all( |c| c. is_ascii_hexdigit( ) ) ) ;
244+
245+ // Verify consistent hashing
246+ let hash2 = compute_policy_file_hash ( & tempdir, "test_policy.30" ) ?;
247+ assert_eq ! ( hash, hash2) ;
248+
249+ Ok ( ( ) )
250+ }
251+
252+ #[ test]
253+ fn test_compute_policy_file_hash_different_content ( ) -> Result < ( ) > {
254+ let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( ambient_authority ( ) ) ?;
255+
256+ tempdir. atomic_write ( "policy1.30" , "content 1" ) ?;
257+ tempdir. atomic_write ( "policy2.30" , "content 2" ) ?;
258+
259+ let hash1 = compute_policy_file_hash ( & tempdir, "policy1.30" ) ?;
260+ let hash2 = compute_policy_file_hash ( & tempdir, "policy2.30" ) ?;
261+
262+ assert_ne ! ( hash1, hash2) ;
263+
264+ Ok ( ( ) )
265+ }
266+
267+ #[ test]
268+ fn test_compute_policy_file_hash_nonexistent_file ( ) {
269+ let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( ambient_authority ( ) ) . unwrap ( ) ;
270+
271+ let result = compute_policy_file_hash ( & tempdir, "nonexistent.30" ) ;
272+ assert ! ( result. is_err( ) ) ;
273+ assert ! ( result. unwrap_err( ) . to_string( ) . contains( "Opening policy file" ) ) ;
274+ }
275+
276+ #[ test]
277+ fn test_compute_policy_file_hash_empty_file ( ) -> Result < ( ) > {
278+ let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( ambient_authority ( ) ) ?;
279+
280+ tempdir. atomic_write ( "empty_policy.30" , "" ) ?;
281+
282+ let hash = compute_policy_file_hash ( & tempdir, "empty_policy.30" ) ?;
283+
284+ // Should produce a valid hash even for empty file
285+ assert_eq ! ( hash. len( ) , 64 ) ;
286+ assert ! ( hash. chars( ) . all( |c| c. is_ascii_hexdigit( ) ) ) ;
287+
288+ // SHA256 of empty string
289+ assert_eq ! ( hash, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" ) ;
290+
291+ Ok ( ( ) )
292+ }
293+ }
0 commit comments