tree-sitter-analyzer 1.0.0__py3-none-any.whl → 1.1.1__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.
- tree_sitter_analyzer/__init__.py +132 -132
- tree_sitter_analyzer/api.py +542 -542
- tree_sitter_analyzer/cli/commands/base_command.py +181 -181
- tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -139
- tree_sitter_analyzer/cli/info_commands.py +124 -124
- tree_sitter_analyzer/cli_main.py +327 -327
- tree_sitter_analyzer/core/analysis_engine.py +584 -584
- tree_sitter_analyzer/core/query_service.py +162 -162
- tree_sitter_analyzer/file_handler.py +212 -212
- tree_sitter_analyzer/formatters/base_formatter.py +169 -169
- tree_sitter_analyzer/interfaces/cli.py +535 -535
- tree_sitter_analyzer/mcp/__init__.py +1 -1
- tree_sitter_analyzer/mcp/resources/__init__.py +0 -1
- tree_sitter_analyzer/mcp/resources/project_stats_resource.py +16 -5
- tree_sitter_analyzer/mcp/server.py +655 -655
- tree_sitter_analyzer/mcp/tools/__init__.py +28 -30
- tree_sitter_analyzer/mcp/utils/__init__.py +1 -2
- tree_sitter_analyzer/mcp/utils/error_handler.py +569 -569
- tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -414
- tree_sitter_analyzer/output_manager.py +257 -257
- tree_sitter_analyzer/project_detector.py +330 -330
- tree_sitter_analyzer/security/boundary_manager.py +260 -260
- tree_sitter_analyzer/security/validator.py +257 -257
- tree_sitter_analyzer/table_formatter.py +710 -710
- tree_sitter_analyzer/utils.py +335 -335
- {tree_sitter_analyzer-1.0.0.dist-info → tree_sitter_analyzer-1.1.1.dist-info}/METADATA +12 -12
- {tree_sitter_analyzer-1.0.0.dist-info → tree_sitter_analyzer-1.1.1.dist-info}/RECORD +29 -29
- {tree_sitter_analyzer-1.0.0.dist-info → tree_sitter_analyzer-1.1.1.dist-info}/WHEEL +0 -0
- {tree_sitter_analyzer-1.0.0.dist-info → tree_sitter_analyzer-1.1.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,260 +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
|
-
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
|
-
)
|
|
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
|
+
)
|