@@ -102,6 +102,265 @@ def mock_relative_to(self, other, *args, **kwargs):
102102 PathSecurityValidator .validate_cache_path (valid_path , cache_root )
103103
104104
105+ class TestTrestleURIPathValidation :
106+ """Test trestle:// URI path validation."""
107+
108+ def test_validate_trestle_uri_path_normal (self ) -> None :
109+ """Test that normal trestle:// URI paths pass validation."""
110+ PathSecurityValidator .validate_trestle_uri_path ('catalogs/nist/catalog.json' )
111+ PathSecurityValidator .validate_trestle_uri_path ('profiles/fedramp/profile.json' )
112+ PathSecurityValidator .validate_trestle_uri_path ('components/mycomp/component.json' )
113+
114+ def test_validate_trestle_uri_path_blocks_traversal (self ) -> None :
115+ """Test that trestle:// URI paths with .. are blocked."""
116+ with pytest .raises (TrestleError , match = 'Security violation:.*[Pp]ath traversal blocked.*trestle://' ):
117+ PathSecurityValidator .validate_trestle_uri_path ('../../etc/passwd' )
118+
119+ with pytest .raises (TrestleError , match = 'Security violation:.*[Pp]ath traversal blocked.*trestle://' ):
120+ PathSecurityValidator .validate_trestle_uri_path ('catalogs/../../../etc/shadow' )
121+
122+ with pytest .raises (TrestleError , match = 'Security violation:.*[Pp]ath traversal blocked.*trestle://' ):
123+ PathSecurityValidator .validate_trestle_uri_path ('../sensitive/file.json' )
124+
125+
126+ class TestLocalPathValidation :
127+ """Test local path validation."""
128+
129+ def test_validate_local_path_within_workspace (self , tmp_path : pathlib .Path ) -> None :
130+ """Test that valid paths within trestle workspace are accepted."""
131+ trestle_root = tmp_path / 'trestle-workspace'
132+ trestle_root .mkdir (parents = True )
133+
134+ # Valid path within workspace
135+ valid_path = trestle_root / 'catalogs' / 'nist' / 'catalog.json'
136+ PathSecurityValidator .validate_local_path (valid_path , trestle_root ) # Should not raise
137+
138+ def test_validate_local_path_traversal_blocked (self , tmp_path : pathlib .Path ) -> None :
139+ """Test that path traversal outside workspace is blocked."""
140+ trestle_root = tmp_path / 'trestle-workspace'
141+ trestle_root .mkdir (parents = True )
142+
143+ # Attempt to traverse outside workspace
144+ evil_path = trestle_root / '..' / '..' / 'etc' / 'passwd'
145+
146+ with pytest .raises (TrestleError , match = 'Security violation.*[Pp]ath traversal blocked' ):
147+ PathSecurityValidator .validate_local_path (evil_path , trestle_root )
148+
149+ def test_validate_local_path_absolute_outside_blocked (self , tmp_path : pathlib .Path ) -> None :
150+ """Test that absolute paths outside workspace are blocked."""
151+ trestle_root = tmp_path / 'trestle-workspace'
152+ trestle_root .mkdir (parents = True )
153+
154+ # Absolute path outside workspace
155+ evil_path = pathlib .Path ('/tmp/pwned.json' )
156+
157+ with pytest .raises (TrestleError , match = 'Security violation.*[Pp]ath traversal blocked' ):
158+ PathSecurityValidator .validate_local_path (evil_path , trestle_root )
159+
160+ def test_validate_local_path_unexpected_error (self , tmp_path : pathlib .Path , monkeypatch ) -> None :
161+ """Test that unexpected errors during validation are caught and wrapped."""
162+ trestle_root = tmp_path / 'trestle-workspace'
163+ trestle_root .mkdir (parents = True )
164+
165+ valid_path = trestle_root / 'catalogs' / 'file.json'
166+
167+ # Mock relative_to() to raise an unexpected exception (not ValueError)
168+ def mock_relative_to (self , other , * args , ** kwargs ):
169+ raise RuntimeError ('Unexpected filesystem error' )
170+
171+ monkeypatch .setattr (pathlib .Path , 'relative_to' , mock_relative_to )
172+
173+ with pytest .raises (TrestleError , match = 'Error validating local path' ):
174+ PathSecurityValidator .validate_local_path (valid_path , trestle_root )
175+
176+
177+ class TestLocalFilePathValidation :
178+ """Test local file path validation with workspace boundaries and sensitive file checks."""
179+
180+ def test_validate_local_file_path_within_workspace (self , tmp_path : pathlib .Path ) -> None :
181+ """Test that files within workspace are allowed."""
182+ workspace = tmp_path / 'workspace'
183+ workspace .mkdir (parents = True )
184+
185+ file_path = workspace / 'catalogs' / 'catalog.json'
186+ PathSecurityValidator .validate_local_file_path (workspace , file_path , allow_outside_workspace = False )
187+
188+ def test_validate_local_file_path_outside_workspace_blocked (self , tmp_path : pathlib .Path ) -> None :
189+ """Test that files outside workspace are blocked when allow_outside_workspace=False."""
190+ workspace = tmp_path / 'workspace'
191+ workspace .mkdir (parents = True )
192+
193+ outside_file = tmp_path / 'outside.json'
194+
195+ with pytest .raises (TrestleError , match = 'Access to files outside the trestle workspace is not allowed' ):
196+ PathSecurityValidator .validate_local_file_path (workspace , outside_file , allow_outside_workspace = False )
197+
198+ def test_validate_local_file_path_outside_workspace_allowed (self , tmp_path : pathlib .Path ) -> None :
199+ """Test that non-sensitive files outside workspace are allowed when allow_outside_workspace=True."""
200+ workspace = tmp_path / 'workspace'
201+ workspace .mkdir (parents = True )
202+
203+ # Create a safe file outside workspace
204+ outside_file = tmp_path / 'safe_file.json'
205+ outside_file .touch ()
206+
207+ # Should not raise
208+ PathSecurityValidator .validate_local_file_path (workspace , outside_file , allow_outside_workspace = True )
209+
210+ def test_validate_local_file_path_blocks_etc_passwd (self , tmp_path : pathlib .Path ) -> None :
211+ """Test that /etc/passwd is blocked even with allow_outside_workspace=True."""
212+ workspace = tmp_path / 'workspace'
213+ workspace .mkdir (parents = True )
214+
215+ passwd_path = pathlib .Path ('/etc/passwd' )
216+
217+ with pytest .raises (TrestleError , match = 'Attempt to access potentially sensitive system file' ):
218+ PathSecurityValidator .validate_local_file_path (workspace , passwd_path , allow_outside_workspace = True )
219+
220+ def test_validate_local_file_path_blocks_etc_shadow (self , tmp_path : pathlib .Path ) -> None :
221+ """Test that /etc/shadow is blocked."""
222+ workspace = tmp_path / 'workspace'
223+ workspace .mkdir (parents = True )
224+
225+ shadow_path = pathlib .Path ('/etc/shadow' )
226+
227+ with pytest .raises (TrestleError , match = 'Attempt to access potentially sensitive system file' ):
228+ PathSecurityValidator .validate_local_file_path (workspace , shadow_path , allow_outside_workspace = True )
229+
230+ def test_validate_local_file_path_blocks_etc_group (self , tmp_path : pathlib .Path ) -> None :
231+ """Test that /etc/group is blocked."""
232+ workspace = tmp_path / 'workspace'
233+ workspace .mkdir (parents = True )
234+
235+ group_path = pathlib .Path ('/etc/group' )
236+
237+ with pytest .raises (TrestleError , match = 'Attempt to access potentially sensitive system file' ):
238+ PathSecurityValidator .validate_local_file_path (workspace , group_path , allow_outside_workspace = True )
239+
240+ def test_validate_local_file_path_blocks_etc_sudoers (self , tmp_path : pathlib .Path ) -> None :
241+ """Test that /etc/sudoers is blocked."""
242+ workspace = tmp_path / 'workspace'
243+ workspace .mkdir (parents = True )
244+
245+ sudoers_path = pathlib .Path ('/etc/sudoers' )
246+
247+ with pytest .raises (TrestleError , match = 'Attempt to access potentially sensitive system file' ):
248+ PathSecurityValidator .validate_local_file_path (workspace , sudoers_path , allow_outside_workspace = True )
249+
250+ def test_validate_local_file_path_blocks_ssh_directory (self , tmp_path : pathlib .Path ) -> None :
251+ """Test that .ssh directory is blocked."""
252+ workspace = tmp_path / 'workspace'
253+ workspace .mkdir (parents = True )
254+
255+ ssh_path = pathlib .Path ('/home/user/.ssh/id_rsa' )
256+
257+ with pytest .raises (TrestleError , match = 'Attempt to access potentially sensitive system file' ):
258+ PathSecurityValidator .validate_local_file_path (workspace , ssh_path , allow_outside_workspace = True )
259+
260+ def test_validate_local_file_path_blocks_aws_credentials (self , tmp_path : pathlib .Path ) -> None :
261+ """Test that .aws credentials are blocked."""
262+ workspace = tmp_path / 'workspace'
263+ workspace .mkdir (parents = True )
264+
265+ aws_path = pathlib .Path ('/home/user/.aws/credentials' )
266+
267+ with pytest .raises (TrestleError , match = 'Attempt to access potentially sensitive system file' ):
268+ PathSecurityValidator .validate_local_file_path (workspace , aws_path , allow_outside_workspace = True )
269+
270+ def test_validate_local_file_path_blocks_docker_config (self , tmp_path : pathlib .Path ) -> None :
271+ """Test that .docker config is blocked."""
272+ workspace = tmp_path / 'workspace'
273+ workspace .mkdir (parents = True )
274+
275+ docker_path = pathlib .Path ('/home/user/.docker/config.json' )
276+
277+ with pytest .raises (TrestleError , match = 'Attempt to access potentially sensitive system file' ):
278+ PathSecurityValidator .validate_local_file_path (workspace , docker_path , allow_outside_workspace = True )
279+
280+ def test_validate_local_file_path_blocks_kube_config (self , tmp_path : pathlib .Path ) -> None :
281+ """Test that .kube config is blocked."""
282+ workspace = tmp_path / 'workspace'
283+ workspace .mkdir (parents = True )
284+
285+ kube_path = pathlib .Path ('/home/user/.kube/config' )
286+
287+ with pytest .raises (TrestleError , match = 'Attempt to access potentially sensitive system file' ):
288+ PathSecurityValidator .validate_local_file_path (workspace , kube_path , allow_outside_workspace = True )
289+
290+ def test_validate_local_file_path_blocks_proc_environ (self , tmp_path : pathlib .Path ) -> None :
291+ """Test that /proc/self/environ is blocked."""
292+ workspace = tmp_path / 'workspace'
293+ workspace .mkdir (parents = True )
294+
295+ proc_path = pathlib .Path ('/proc/self/environ' )
296+
297+ with pytest .raises (TrestleError , match = 'Attempt to access potentially sensitive system file' ):
298+ PathSecurityValidator .validate_local_file_path (workspace , proc_path , allow_outside_workspace = True )
299+
300+ def test_validate_local_file_path_blocks_windows_system32 (self , tmp_path : pathlib .Path ) -> None :
301+ """Test that Windows System32 is blocked."""
302+ workspace = tmp_path / 'workspace'
303+ workspace .mkdir (parents = True )
304+
305+ win_path = pathlib .Path ('C:\\ Windows\\ System32\\ config\\ SAM' )
306+
307+ with pytest .raises (TrestleError , match = 'Attempt to access potentially sensitive system file' ):
308+ PathSecurityValidator .validate_local_file_path (workspace , win_path , allow_outside_workspace = True )
309+
310+ def test_validate_local_file_path_blocks_windows_credentials (self , tmp_path : pathlib .Path ) -> None :
311+ """Test that Windows credentials are blocked."""
312+ workspace = tmp_path / 'workspace'
313+ workspace .mkdir (parents = True )
314+
315+ cred_path = pathlib .Path ('C:\\ Users\\ user\\ AppData\\ Local\\ Microsoft\\ Credentials\\ secret' )
316+
317+ with pytest .raises (TrestleError , match = 'Attempt to access potentially sensitive system file' ):
318+ PathSecurityValidator .validate_local_file_path (workspace , cred_path , allow_outside_workspace = True )
319+
320+ def test_validate_local_file_path_blocks_var_log (self , tmp_path : pathlib .Path ) -> None :
321+ """Test that /var/log is blocked."""
322+ workspace = tmp_path / 'workspace'
323+ workspace .mkdir (parents = True )
324+
325+ log_path = pathlib .Path ('/var/log/auth.log' )
326+
327+ with pytest .raises (TrestleError , match = 'Attempt to access potentially sensitive system file' ):
328+ PathSecurityValidator .validate_local_file_path (workspace , log_path , allow_outside_workspace = True )
329+
330+ def test_validate_local_file_path_blocks_mysql_data (self , tmp_path : pathlib .Path ) -> None :
331+ """Test that MySQL data directory is blocked."""
332+ workspace = tmp_path / 'workspace'
333+ workspace .mkdir (parents = True )
334+
335+ mysql_path = pathlib .Path ('/var/lib/mysql/users.MYD' )
336+
337+ with pytest .raises (TrestleError , match = 'Attempt to access potentially sensitive system file' ):
338+ PathSecurityValidator .validate_local_file_path (workspace , mysql_path , allow_outside_workspace = True )
339+
340+ def test_validate_local_file_path_case_insensitive (self , tmp_path : pathlib .Path ) -> None :
341+ """Test that sensitive path checking is case-insensitive."""
342+ workspace = tmp_path / 'workspace'
343+ workspace .mkdir (parents = True )
344+
345+ # Test uppercase variations
346+ passwd_upper = pathlib .Path ('/ETC/PASSWD' )
347+
348+ with pytest .raises (TrestleError , match = 'Attempt to access potentially sensitive system file' ):
349+ PathSecurityValidator .validate_local_file_path (workspace , passwd_upper , allow_outside_workspace = True )
350+
351+ def test_validate_local_file_path_checks_original_and_resolved (self , tmp_path : pathlib .Path ) -> None :
352+ """Test that both original and resolved paths are checked for sensitive patterns."""
353+ workspace = tmp_path / 'workspace'
354+ workspace .mkdir (parents = True )
355+
356+ # Create a path that might resolve differently
357+ # The validator checks both the original string and resolved path
358+ sensitive_path = pathlib .Path ('/home/user/.ssh/authorized_keys' )
359+
360+ with pytest .raises (TrestleError , match = 'Attempt to access potentially sensitive system file' ):
361+ PathSecurityValidator .validate_local_file_path (workspace , sensitive_path , allow_outside_workspace = True )
362+
363+
105364class TestHTTPSFetcherPathTraversal :
106365 """Test HTTPSFetcher protection against path traversal attacks."""
107366
0 commit comments