tree-sitter-analyzer 0.9.9__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tree-sitter-analyzer might be problematic. Click here for more details.

@@ -1,251 +1,260 @@
1
- #!/usr/bin/env python3
2
- """
3
- Project Boundary Manager for Tree-sitter Analyzer
4
-
5
- Provides strict project boundary control to prevent access to files
6
- outside the designated project directory.
7
- """
8
-
9
- import os
10
- from pathlib import Path
11
-
12
- from ..exceptions import SecurityError
13
- from ..utils import log_debug, log_info, log_warning
14
-
15
-
16
- class ProjectBoundaryManager:
17
- """
18
- Project boundary manager for access control.
19
-
20
- This class enforces strict boundaries around project directories
21
- to prevent unauthorized file access outside the project scope.
22
-
23
- Features:
24
- - Real path resolution for symlink protection
25
- - Configurable allowed directories
26
- - Comprehensive boundary checking
27
- - Audit logging for security events
28
- """
29
-
30
- def __init__(self, project_root: str) -> None:
31
- """
32
- Initialize project boundary manager.
33
-
34
- Args:
35
- project_root: Root directory of the project
36
-
37
- Raises:
38
- SecurityError: If project root is invalid
39
- """
40
- if not project_root:
41
- raise SecurityError("Project root cannot be empty")
42
-
43
- if not os.path.exists(project_root):
44
- raise SecurityError(f"Project root does not exist: {project_root}")
45
-
46
- if not os.path.isdir(project_root):
47
- raise SecurityError(f"Project root is not a directory: {project_root}")
48
-
49
- # Store real path to prevent symlink attacks
50
- self.project_root = os.path.realpath(project_root)
51
- self.allowed_directories: set[str] = {self.project_root}
52
-
53
- log_debug(f"ProjectBoundaryManager initialized with root: {self.project_root}")
54
-
55
- def add_allowed_directory(self, directory: str) -> None:
56
- """
57
- Add an additional allowed directory.
58
-
59
- Args:
60
- directory: Directory path to allow access to
61
-
62
- Raises:
63
- SecurityError: If directory is invalid
64
- """
65
- if not directory:
66
- raise SecurityError("Directory cannot be empty")
67
-
68
- if not os.path.exists(directory):
69
- raise SecurityError(f"Directory does not exist: {directory}")
70
-
71
- if not os.path.isdir(directory):
72
- raise SecurityError(f"Path is not a directory: {directory}")
73
-
74
- real_dir = os.path.realpath(directory)
75
- self.allowed_directories.add(real_dir)
76
-
77
- log_info(f"Added allowed directory: {real_dir}")
78
-
79
- def is_within_project(self, file_path: str) -> bool:
80
- """
81
- Check if file path is within project boundaries.
82
-
83
- Args:
84
- file_path: File path to check
85
-
86
- Returns:
87
- True if path is within allowed boundaries
88
- """
89
- try:
90
- if not file_path:
91
- log_warning("Empty file path provided to boundary check")
92
- return False
93
-
94
- # Resolve real path to handle symlinks
95
- real_path = os.path.realpath(file_path)
96
-
97
- # Check against all allowed directories
98
- for allowed_dir in self.allowed_directories:
99
- if (
100
- real_path.startswith(allowed_dir + os.sep)
101
- or real_path == allowed_dir
102
- ):
103
- log_debug(f"File path within boundaries: {file_path}")
104
- return True
105
-
106
- log_warning(f"File path outside boundaries: {file_path} -> {real_path}")
107
- return False
108
-
109
- except Exception as e:
110
- log_warning(f"Boundary check error for {file_path}: {e}")
111
- return False
112
-
113
- def get_relative_path(self, file_path: str) -> str | None:
114
- """
115
- Get relative path from project root if within boundaries.
116
-
117
- Args:
118
- file_path: File path to convert
119
-
120
- Returns:
121
- Relative path from project root, or None if outside boundaries
122
- """
123
- if not self.is_within_project(file_path):
124
- return None
125
-
126
- try:
127
- real_path = os.path.realpath(file_path)
128
- rel_path = os.path.relpath(real_path, self.project_root)
129
-
130
- # Ensure relative path doesn't start with ..
131
- if rel_path.startswith(".."):
132
- log_warning(f"Relative path calculation failed: {rel_path}")
133
- return None
134
-
135
- return rel_path
136
-
137
- except Exception as e:
138
- log_warning(f"Relative path calculation error: {e}")
139
- return None
140
-
141
- def validate_and_resolve_path(self, file_path: str) -> str | None:
142
- """
143
- Validate path and return resolved absolute path if within boundaries.
144
-
145
- Args:
146
- file_path: File path to validate and resolve
147
-
148
- Returns:
149
- Resolved absolute path if valid, None otherwise
150
- """
151
- try:
152
- # Handle relative paths from project root
153
- if not os.path.isabs(file_path):
154
- full_path = os.path.join(self.project_root, file_path)
155
- else:
156
- full_path = file_path
157
-
158
- # Check boundaries
159
- if not self.is_within_project(full_path):
160
- return None
161
-
162
- # Return real path
163
- return os.path.realpath(full_path)
164
-
165
- except Exception as e:
166
- log_warning(f"Path validation error: {e}")
167
- return None
168
-
169
- def list_allowed_directories(self) -> set[str]:
170
- """
171
- Get list of all allowed directories.
172
-
173
- Returns:
174
- Set of allowed directory paths
175
- """
176
- return self.allowed_directories.copy()
177
-
178
- def is_symlink_safe(self, file_path: str) -> bool:
179
- """
180
- Check if file path is safe from symlink attacks.
181
-
182
- Args:
183
- file_path: File path to check
184
-
185
- Returns:
186
- True if path is safe from symlink attacks
187
- """
188
- try:
189
- if not os.path.exists(file_path):
190
- return True # Non-existent files are safe
191
-
192
- # If the fully resolved path is within project boundaries, we treat it as safe.
193
- # This makes the check tolerant to system-level symlinks like
194
- # /var -> /private/var on macOS runners.
195
- resolved = os.path.realpath(file_path)
196
- if self.is_within_project(resolved):
197
- return True
198
-
199
- # Otherwise, inspect each path component symlink to ensure no hop jumps outside
200
- # the allowed directories.
201
- path_parts = Path(file_path).parts
202
- current_path = ""
203
-
204
- for part in path_parts:
205
- current_path = (
206
- os.path.join(current_path, part) if current_path else part
207
- )
208
-
209
- if os.path.islink(current_path):
210
- target = os.path.realpath(current_path)
211
- if not self.is_within_project(target):
212
- log_warning(
213
- f"Unsafe symlink detected: {current_path} -> {target}"
214
- )
215
- return False
216
-
217
- # If no unsafe hop found, consider safe
218
- return True
219
-
220
- except Exception as e:
221
- log_warning(f"Symlink safety check error: {e}")
222
- return False
223
-
224
- def audit_access(self, file_path: str, operation: str) -> None:
225
- """
226
- Log file access for security auditing.
227
-
228
- Args:
229
- file_path: File path being accessed
230
- operation: Type of operation (read, write, analyze, etc.)
231
- """
232
- is_within = self.is_within_project(file_path)
233
- status = "ALLOWED" if is_within else "DENIED"
234
-
235
- log_info(f"AUDIT: {status} {operation} access to {file_path}")
236
-
237
- if not is_within:
238
- log_warning(f"SECURITY: Unauthorized access attempt to {file_path}")
239
-
240
- def __str__(self) -> str:
241
- """String representation of boundary manager."""
242
- return f"ProjectBoundaryManager(root={self.project_root}, allowed_dirs={len(self.allowed_directories)})"
243
-
244
- def __repr__(self) -> str:
245
- """Detailed representation of boundary manager."""
246
- return (
247
- f"ProjectBoundaryManager("
248
- f"project_root='{self.project_root}', "
249
- f"allowed_directories={self.allowed_directories}"
250
- f")"
251
- )
1
+ #!/usr/bin/env python3
2
+ """
3
+ Project Boundary Manager for Tree-sitter Analyzer
4
+
5
+ Provides strict project boundary control to prevent access to files
6
+ outside the designated project directory.
7
+ """
8
+
9
+ from pathlib import Path
10
+
11
+ from ..exceptions import SecurityError
12
+ from ..utils import log_debug, log_info, log_warning
13
+
14
+
15
+ class ProjectBoundaryManager:
16
+ """
17
+ Project boundary manager for access control.
18
+
19
+ This class enforces strict boundaries around project directories
20
+ to prevent unauthorized file access outside the project scope.
21
+
22
+ Features:
23
+ - Real path resolution for symlink protection
24
+ - Configurable allowed directories
25
+ - Comprehensive boundary checking
26
+ - Audit logging for security events
27
+ """
28
+
29
+ def __init__(self, project_root: str) -> None:
30
+ """
31
+ Initialize project boundary manager.
32
+
33
+ Args:
34
+ project_root: Root directory of the project
35
+
36
+ Raises:
37
+ SecurityError: If project root is invalid
38
+ """
39
+ if not project_root:
40
+ raise SecurityError("Project root cannot be empty")
41
+
42
+ project_path = Path(project_root)
43
+ if not project_path.exists():
44
+ raise SecurityError(f"Project root does not exist: {project_root}")
45
+
46
+ if not project_path.is_dir():
47
+ raise SecurityError(f"Project root is not a directory: {project_root}")
48
+
49
+ # Store real path to prevent symlink attacks
50
+ self.project_root = str(project_path.resolve())
51
+ self.allowed_directories: set[str] = {self.project_root}
52
+
53
+ log_debug(f"ProjectBoundaryManager initialized with root: {self.project_root}")
54
+
55
+ def add_allowed_directory(self, directory: str) -> None:
56
+ """
57
+ Add an additional allowed directory.
58
+
59
+ Args:
60
+ directory: Directory path to allow access to
61
+
62
+ Raises:
63
+ SecurityError: If directory is invalid
64
+ """
65
+ if not directory:
66
+ raise SecurityError("Directory cannot be empty")
67
+
68
+ dir_path = Path(directory)
69
+ if not dir_path.exists():
70
+ raise SecurityError(f"Directory does not exist: {directory}")
71
+
72
+ if not dir_path.is_dir():
73
+ raise SecurityError(f"Path is not a directory: {directory}")
74
+
75
+ real_dir = str(dir_path.resolve())
76
+ self.allowed_directories.add(real_dir)
77
+
78
+ log_info(f"Added allowed directory: {real_dir}")
79
+
80
+ def is_within_project(self, file_path: str) -> bool:
81
+ """
82
+ Check if file path is within project boundaries.
83
+
84
+ Args:
85
+ file_path: File path to check
86
+
87
+ Returns:
88
+ True if path is within allowed boundaries
89
+ """
90
+ try:
91
+ if not file_path:
92
+ log_warning("Empty file path provided to boundary check")
93
+ return False
94
+
95
+ # Resolve real path to handle symlinks
96
+ real_path = str(Path(file_path).resolve())
97
+
98
+ # Check against all allowed directories
99
+ for allowed_dir in self.allowed_directories:
100
+ # Use pathlib to check if path is within allowed directory
101
+ try:
102
+ Path(real_path).relative_to(Path(allowed_dir))
103
+ log_debug(f"File path within boundaries: {file_path}")
104
+ return True
105
+ except ValueError:
106
+ # Path is not within this allowed directory, continue checking
107
+ continue
108
+
109
+ log_warning(f"File path outside boundaries: {file_path} -> {real_path}")
110
+ return False
111
+
112
+ except Exception as e:
113
+ log_warning(f"Boundary check error for {file_path}: {e}")
114
+ return False
115
+
116
+ def get_relative_path(self, file_path: str) -> str | None:
117
+ """
118
+ Get relative path from project root if within boundaries.
119
+
120
+ Args:
121
+ file_path: File path to convert
122
+
123
+ Returns:
124
+ Relative path from project root, or None if outside boundaries
125
+ """
126
+ if not self.is_within_project(file_path):
127
+ return None
128
+
129
+ try:
130
+ real_path = Path(file_path).resolve()
131
+ try:
132
+ rel_path = real_path.relative_to(Path(self.project_root))
133
+ rel_path = str(rel_path)
134
+ except ValueError:
135
+ # Path is not relative to project root
136
+ log_warning(f"Path not relative to project root: {file_path}")
137
+ return None
138
+
139
+ # Ensure relative path doesn't start with ..
140
+ if rel_path.startswith(".."):
141
+ log_warning(f"Relative path calculation failed: {rel_path}")
142
+ return None
143
+
144
+ return rel_path
145
+
146
+ except Exception as e:
147
+ log_warning(f"Relative path calculation error: {e}")
148
+ return None
149
+
150
+ def validate_and_resolve_path(self, file_path: str) -> str | None:
151
+ """
152
+ Validate path and return resolved absolute path if within boundaries.
153
+
154
+ Args:
155
+ file_path: File path to validate and resolve
156
+
157
+ Returns:
158
+ Resolved absolute path if valid, None otherwise
159
+ """
160
+ try:
161
+ # Handle relative paths from project root
162
+ file_path_obj = Path(file_path)
163
+ if not file_path_obj.is_absolute():
164
+ full_path = Path(self.project_root) / file_path
165
+ else:
166
+ full_path = file_path_obj
167
+
168
+ # Check boundaries
169
+ if not self.is_within_project(str(full_path)):
170
+ return None
171
+
172
+ # Return real path
173
+ return str(full_path.resolve())
174
+
175
+ except Exception as e:
176
+ log_warning(f"Path validation error: {e}")
177
+ return None
178
+
179
+ def list_allowed_directories(self) -> set[str]:
180
+ """
181
+ Get list of all allowed directories.
182
+
183
+ Returns:
184
+ Set of allowed directory paths
185
+ """
186
+ return self.allowed_directories.copy()
187
+
188
+ def is_symlink_safe(self, file_path: str) -> bool:
189
+ """
190
+ Check if file path is safe from symlink attacks.
191
+
192
+ Args:
193
+ file_path: File path to check
194
+
195
+ Returns:
196
+ True if path is safe from symlink attacks
197
+ """
198
+ try:
199
+ file_path_obj = Path(file_path)
200
+ if not file_path_obj.exists():
201
+ return True # Non-existent files are safe
202
+
203
+ # If the fully resolved path is within project boundaries, we treat it as safe.
204
+ # This makes the check tolerant to system-level symlinks like
205
+ # /var -> /private/var on macOS runners.
206
+ resolved = str(file_path_obj.resolve())
207
+ if self.is_within_project(resolved):
208
+ return True
209
+
210
+ # Otherwise, inspect each path component symlink to ensure no hop jumps outside
211
+ # the allowed directories.
212
+ path_parts = file_path_obj.parts
213
+ current_path = Path()
214
+
215
+ for part in path_parts:
216
+ current_path = current_path / part if current_path.parts else Path(part)
217
+
218
+ if current_path.is_symlink():
219
+ target = str(current_path.resolve())
220
+ if not self.is_within_project(target):
221
+ log_warning(
222
+ f"Unsafe symlink detected: {current_path} -> {target}"
223
+ )
224
+ return False
225
+
226
+ # If no unsafe hop found, consider safe
227
+ return True
228
+
229
+ except Exception as e:
230
+ log_warning(f"Symlink safety check error: {e}")
231
+ return False
232
+
233
+ def audit_access(self, file_path: str, operation: str) -> None:
234
+ """
235
+ Log file access for security auditing.
236
+
237
+ Args:
238
+ file_path: File path being accessed
239
+ operation: Type of operation (read, write, analyze, etc.)
240
+ """
241
+ is_within = self.is_within_project(file_path)
242
+ status = "ALLOWED" if is_within else "DENIED"
243
+
244
+ log_info(f"AUDIT: {status} {operation} access to {file_path}")
245
+
246
+ if not is_within:
247
+ log_warning(f"SECURITY: Unauthorized access attempt to {file_path}")
248
+
249
+ def __str__(self) -> str:
250
+ """String representation of boundary manager."""
251
+ return f"ProjectBoundaryManager(root={self.project_root}, allowed_dirs={len(self.allowed_directories)})"
252
+
253
+ def __repr__(self) -> str:
254
+ """Detailed representation of boundary manager."""
255
+ return (
256
+ f"ProjectBoundaryManager("
257
+ f"project_root='{self.project_root}', "
258
+ f"allowed_directories={self.allowed_directories}"
259
+ f")"
260
+ )