@@ -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,172 @@ 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
188+ . atomic_write ( "not_policy.txt" , "not a policy file" )
189+ . unwrap ( ) ;
190+ tempdir. atomic_write ( "other.txt" , "invalid format" ) . unwrap ( ) ;
191+
192+ let result = find_latest_policy_file ( & tempdir) ;
193+ assert ! ( result. is_err( ) ) ;
194+ assert ! (
195+ result
196+ . unwrap_err( )
197+ . to_string( )
198+ . contains( "Failed to get latest SELinux policy" )
199+ ) ;
200+ }
201+
202+ #[ test]
203+ fn test_find_latest_policy_file_invalid_version ( ) {
204+ let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( ambient_authority ( ) ) . unwrap ( ) ;
205+
206+ tempdir
207+ . atomic_write ( "policy.abc" , "invalid version" )
208+ . unwrap ( ) ;
209+
210+ let result = find_latest_policy_file ( & tempdir) ;
211+ assert ! ( result. is_err( ) ) ;
212+ assert ! (
213+ result
214+ . unwrap_err( )
215+ . to_string( )
216+ . contains( "Parsing abc as int" )
217+ ) ;
218+ }
219+
220+ #[ test]
221+ fn test_find_latest_policy_file_negative_version ( ) -> Result < ( ) > {
222+ let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( ambient_authority ( ) ) ?;
223+
224+ tempdir. atomic_write ( "policy.5" , "positive version" ) ?;
225+ tempdir. atomic_write ( "policy.-1" , "negative version" ) ?;
226+
227+ let result = find_latest_policy_file ( & tempdir) ?;
228+ assert_eq ! ( result, "policy.5" ) ;
229+
230+ Ok ( ( ) )
231+ }
232+
233+ #[ test]
234+ fn test_find_latest_policy_file_skips_directories ( ) -> Result < ( ) > {
235+ let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( ambient_authority ( ) ) ?;
236+
237+ tempdir. create_dir ( "policy.99" ) ?; // This should be skipped
238+ tempdir. atomic_write ( "policy.5" , "actual policy file" ) ?;
239+
240+ let result = find_latest_policy_file ( & tempdir) ?;
241+ assert_eq ! ( result, "policy.5" ) ;
242+
243+ Ok ( ( ) )
244+ }
245+
246+ #[ test]
247+ fn test_compute_policy_file_hash ( ) -> Result < ( ) > {
248+ let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( ambient_authority ( ) ) ?;
249+
250+ let test_content = "test policy content for hashing" ;
251+ tempdir. atomic_write ( "test_policy.30" , test_content) ?;
252+
253+ let hash = compute_policy_file_hash ( & tempdir, "test_policy.30" ) ?;
254+
255+ // Verify the hash is a valid SHA256 hash (64 hex characters)
256+ assert_eq ! ( hash. len( ) , 64 ) ;
257+ assert ! ( hash. chars( ) . all( |c| c. is_ascii_hexdigit( ) ) ) ;
258+
259+ // Verify consistent hashing
260+ let hash2 = compute_policy_file_hash ( & tempdir, "test_policy.30" ) ?;
261+ assert_eq ! ( hash, hash2) ;
262+
263+ Ok ( ( ) )
264+ }
265+
266+ #[ test]
267+ fn test_compute_policy_file_hash_different_content ( ) -> Result < ( ) > {
268+ let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( ambient_authority ( ) ) ?;
269+
270+ tempdir. atomic_write ( "policy1.30" , "content 1" ) ?;
271+ tempdir. atomic_write ( "policy2.30" , "content 2" ) ?;
272+
273+ let hash1 = compute_policy_file_hash ( & tempdir, "policy1.30" ) ?;
274+ let hash2 = compute_policy_file_hash ( & tempdir, "policy2.30" ) ?;
275+
276+ assert_ne ! ( hash1, hash2) ;
277+
278+ Ok ( ( ) )
279+ }
280+
281+ #[ test]
282+ fn test_compute_policy_file_hash_nonexistent_file ( ) {
283+ let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( ambient_authority ( ) ) . unwrap ( ) ;
284+
285+ let result = compute_policy_file_hash ( & tempdir, "nonexistent.30" ) ;
286+ assert ! ( result. is_err( ) ) ;
287+ assert ! (
288+ result
289+ . unwrap_err( )
290+ . to_string( )
291+ . contains( "Opening policy file" )
292+ ) ;
293+ }
294+
295+ #[ test]
296+ fn test_compute_policy_file_hash_empty_file ( ) -> Result < ( ) > {
297+ let tempdir = cap_std_ext:: cap_tempfile:: tempdir ( ambient_authority ( ) ) ?;
298+
299+ tempdir. atomic_write ( "empty_policy.30" , "" ) ?;
300+
301+ let hash = compute_policy_file_hash ( & tempdir, "empty_policy.30" ) ?;
302+
303+ // Should produce a valid hash even for empty file
304+ assert_eq ! ( hash. len( ) , 64 ) ;
305+ assert ! ( hash. chars( ) . all( |c| c. is_ascii_hexdigit( ) ) ) ;
306+
307+ // SHA256 of empty string
308+ assert_eq ! (
309+ hash,
310+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
311+ ) ;
312+
313+ Ok ( ( ) )
314+ }
315+ }
0 commit comments