fast-clean-architecture 1.0.0__py3-none-any.whl → 1.1.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.
- fast_clean_architecture/__init__.py +3 -4
- fast_clean_architecture/analytics.py +260 -0
- fast_clean_architecture/cli.py +555 -43
- fast_clean_architecture/config.py +47 -23
- fast_clean_architecture/error_tracking.py +201 -0
- fast_clean_architecture/exceptions.py +432 -12
- fast_clean_architecture/generators/__init__.py +11 -1
- fast_clean_architecture/generators/component_generator.py +407 -103
- fast_clean_architecture/generators/config_updater.py +186 -38
- fast_clean_architecture/generators/generator_factory.py +223 -0
- fast_clean_architecture/generators/package_generator.py +9 -7
- fast_clean_architecture/generators/template_validator.py +109 -9
- fast_clean_architecture/generators/validation_config.py +5 -3
- fast_clean_architecture/generators/validation_metrics.py +10 -6
- fast_clean_architecture/health.py +169 -0
- fast_clean_architecture/logging_config.py +52 -0
- fast_clean_architecture/metrics.py +108 -0
- fast_clean_architecture/protocols.py +406 -0
- fast_clean_architecture/templates/external.py.j2 +109 -32
- fast_clean_architecture/utils.py +50 -31
- fast_clean_architecture/validation.py +302 -0
- {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.0.dist-info}/METADATA +31 -21
- fast_clean_architecture-1.1.0.dist-info/RECORD +38 -0
- fast_clean_architecture-1.0.0.dist-info/RECORD +0 -30
- {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.0.dist-info}/WHEEL +0 -0
- {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.0.dist-info}/entry_points.txt +0 -0
- {fast_clean_architecture-1.0.0.dist-info → fast_clean_architecture-1.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,46 +1,71 @@
|
|
1
1
|
"""Component generator for creating individual component files."""
|
2
2
|
|
3
|
+
import functools
|
3
4
|
import os
|
4
5
|
import re
|
5
|
-
import
|
6
|
-
import logging
|
6
|
+
from datetime import datetime
|
7
7
|
from pathlib import Path
|
8
|
-
from typing import
|
8
|
+
from typing import (
|
9
|
+
Any,
|
10
|
+
Callable,
|
11
|
+
Dict,
|
12
|
+
Generic,
|
13
|
+
List,
|
14
|
+
Optional,
|
15
|
+
Set,
|
16
|
+
Tuple,
|
17
|
+
TypeVar,
|
18
|
+
Union,
|
19
|
+
)
|
9
20
|
|
10
21
|
import jinja2
|
11
22
|
from jinja2 import TemplateSyntaxError
|
12
|
-
from jinja2.exceptions import SecurityError
|
23
|
+
from jinja2.exceptions import SecurityError as JinjaSecurityError
|
13
24
|
from jinja2.sandbox import SandboxedEnvironment
|
14
25
|
from rich.console import Console
|
15
26
|
|
27
|
+
from ..config import Config
|
28
|
+
from ..error_tracking import track_error, track_exception
|
29
|
+
from ..exceptions import (
|
30
|
+
ComponentError,
|
31
|
+
FileConflictError,
|
32
|
+
SecurityError,
|
33
|
+
TemplateError,
|
34
|
+
ValidationError,
|
35
|
+
create_secure_error,
|
36
|
+
)
|
37
|
+
from ..logging_config import get_logger
|
38
|
+
from ..metrics import PerformanceTracker, measure_execution_time
|
39
|
+
from ..protocols import (
|
40
|
+
ComponentGeneratorProtocol,
|
41
|
+
ComponentValidationStrategy,
|
42
|
+
SecurePathHandler,
|
43
|
+
TemplateValidatorProtocol,
|
44
|
+
)
|
16
45
|
from ..templates import TEMPLATES_DIR
|
17
46
|
from ..utils import (
|
47
|
+
ensure_directory,
|
18
48
|
get_template_variables,
|
49
|
+
sanitize_error_message,
|
50
|
+
sanitize_name,
|
51
|
+
secure_file_operation,
|
19
52
|
to_pascal_case,
|
20
53
|
to_snake_case,
|
21
|
-
ensure_directory,
|
22
|
-
sanitize_name,
|
23
|
-
validate_python_identifier,
|
24
54
|
validate_name,
|
25
|
-
|
26
|
-
create_secure_error,
|
27
|
-
sanitize_error_message,
|
28
|
-
)
|
29
|
-
from ..exceptions import (
|
30
|
-
TemplateError,
|
31
|
-
ValidationError,
|
32
|
-
FileConflictError,
|
33
|
-
ComponentError,
|
55
|
+
validate_python_identifier,
|
34
56
|
)
|
35
|
-
from ..config import Config
|
36
57
|
from .template_validator import TemplateValidatorFactory
|
37
58
|
|
38
|
-
# Set up logger
|
39
|
-
logger =
|
59
|
+
# Set up structured logger
|
60
|
+
logger = get_logger(__name__)
|
40
61
|
|
41
62
|
|
42
|
-
class ComponentGenerator:
|
43
|
-
"""Generator for creating individual component files from templates.
|
63
|
+
class ComponentGenerator(ComponentGeneratorProtocol):
|
64
|
+
"""Generator for creating individual component files from templates.
|
65
|
+
|
66
|
+
This class implements the ComponentGeneratorProtocol to ensure type safety
|
67
|
+
and provides enhanced security through protocol-based design patterns.
|
68
|
+
"""
|
44
69
|
|
45
70
|
# Mapping of component types to their template files
|
46
71
|
TEMPLATE_MAPPING = {
|
@@ -59,9 +84,24 @@ class ComponentGenerator:
|
|
59
84
|
# Infrastructure repositories use a different template
|
60
85
|
INFRASTRUCTURE_REPOSITORY_TEMPLATE = "infrastructure_repository.py.j2"
|
61
86
|
|
62
|
-
def __init__(
|
87
|
+
def __init__(
|
88
|
+
self,
|
89
|
+
config: Config,
|
90
|
+
template_validator: Optional[TemplateValidatorProtocol] = None,
|
91
|
+
path_handler: Optional[SecurePathHandler[Union[str, Path]]] = None,
|
92
|
+
console: Optional[Console] = None,
|
93
|
+
):
|
94
|
+
"""Initialize ComponentGenerator with dependency injection.
|
95
|
+
|
96
|
+
Args:
|
97
|
+
config: Configuration object
|
98
|
+
template_validator: Template validator (injected dependency)
|
99
|
+
path_handler: Secure path handler (injected dependency)
|
100
|
+
console: Console for output
|
101
|
+
"""
|
63
102
|
self.config = config
|
64
103
|
self.console = console or Console()
|
104
|
+
|
65
105
|
# Use SandboxedEnvironment for security
|
66
106
|
self.template_env = SandboxedEnvironment(
|
67
107
|
loader=jinja2.FileSystemLoader(TEMPLATES_DIR),
|
@@ -73,19 +113,38 @@ class ComponentGenerator:
|
|
73
113
|
# Define allowed filters and functions
|
74
114
|
self._setup_sandbox_security()
|
75
115
|
|
76
|
-
#
|
77
|
-
|
116
|
+
# Use injected template validator or create default
|
117
|
+
if template_validator is not None:
|
118
|
+
self.template_validator = template_validator
|
119
|
+
else:
|
120
|
+
# Fallback to factory creation for backward compatibility
|
121
|
+
from .validation_config import ValidationConfig
|
122
|
+
|
123
|
+
validation_config = ValidationConfig(
|
124
|
+
sandbox_mode=True,
|
125
|
+
max_template_size_bytes=64 * 1024, # 64KB limit
|
126
|
+
render_timeout_seconds=10,
|
127
|
+
max_variable_nesting_depth=10,
|
128
|
+
)
|
129
|
+
self.template_validator = TemplateValidatorFactory().create(
|
130
|
+
self.template_env, validation_config
|
131
|
+
)
|
78
132
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
133
|
+
# Use injected path handler or create default
|
134
|
+
if path_handler is not None:
|
135
|
+
self.path_handler: SecurePathHandler[Union[str, Path]] = path_handler
|
136
|
+
else:
|
137
|
+
# Fallback to default creation for backward compatibility
|
138
|
+
self.path_handler = SecurePathHandler[Union[str, Path]](
|
139
|
+
max_path_length=4096,
|
140
|
+
allowed_extensions=[".py", ".j2", ".yaml", ".yml", ".json"],
|
141
|
+
)
|
88
142
|
|
143
|
+
# Initialize component validation strategies
|
144
|
+
self._validation_strategies = self._setup_validation_strategies()
|
145
|
+
|
146
|
+
@measure_execution_time("create_component")
|
147
|
+
@track_exception(operation="create_component") # type: ignore[misc]
|
89
148
|
def create_component(
|
90
149
|
self,
|
91
150
|
base_path: Path,
|
@@ -97,7 +156,27 @@ class ComponentGenerator:
|
|
97
156
|
dry_run: bool = False,
|
98
157
|
force: bool = False,
|
99
158
|
) -> Path:
|
100
|
-
"""Create a single component file.
|
159
|
+
"""Create a single component file with enhanced type safety.
|
160
|
+
|
161
|
+
This method implements the ComponentGeneratorProtocol interface
|
162
|
+
and uses secure path handling for enhanced security.
|
163
|
+
"""
|
164
|
+
# Log component creation start
|
165
|
+
logger.info(
|
166
|
+
"Starting component creation",
|
167
|
+
operation="create_component",
|
168
|
+
system_name=system_name,
|
169
|
+
module_name=module_name,
|
170
|
+
layer=layer,
|
171
|
+
component_type=component_type,
|
172
|
+
component_name=component_name,
|
173
|
+
dry_run=dry_run,
|
174
|
+
force=force,
|
175
|
+
)
|
176
|
+
|
177
|
+
# Validate and secure the base path using type-safe path handler
|
178
|
+
secure_base_path = Path(self.path_handler.process(base_path))
|
179
|
+
|
101
180
|
# Validate inputs
|
102
181
|
self._validate_component_inputs(
|
103
182
|
system_name, module_name, layer, component_type, component_name
|
@@ -108,26 +187,36 @@ class ComponentGenerator:
|
|
108
187
|
if not validate_python_identifier(sanitized_name):
|
109
188
|
raise ValidationError(f"Invalid component name: {component_name}")
|
110
189
|
|
111
|
-
# Determine file path and name
|
190
|
+
# Determine file path and name with secure base path
|
112
191
|
file_path, file_name = self._get_component_file_path(
|
113
|
-
|
192
|
+
secure_base_path,
|
193
|
+
system_name,
|
194
|
+
module_name,
|
195
|
+
layer,
|
196
|
+
component_type,
|
197
|
+
sanitized_name,
|
114
198
|
)
|
115
199
|
|
116
|
-
#
|
117
|
-
|
200
|
+
# Additional security validation for the final file path
|
201
|
+
secure_file_path = Path(self.path_handler.process(file_path))
|
202
|
+
|
203
|
+
# Check for conflicts using secure file path
|
204
|
+
if secure_file_path.exists() and not force:
|
118
205
|
raise FileConflictError(
|
119
|
-
f"Component file already exists: {
|
206
|
+
f"Component file already exists: {secure_file_path}. Use --force to overwrite."
|
120
207
|
)
|
121
208
|
|
122
209
|
if dry_run:
|
123
|
-
self.console.print(
|
124
|
-
|
210
|
+
self.console.print(
|
211
|
+
f"[yellow]DRY RUN:[/yellow] Would create {secure_file_path}"
|
212
|
+
)
|
213
|
+
return secure_file_path
|
125
214
|
|
126
215
|
# Validate file system permissions and space
|
127
|
-
self._validate_file_system(
|
216
|
+
self._validate_file_system(secure_file_path)
|
128
217
|
|
129
218
|
# Ensure directory exists
|
130
|
-
ensure_directory(
|
219
|
+
ensure_directory(secure_file_path.parent)
|
131
220
|
|
132
221
|
# Generate template variables
|
133
222
|
template_vars = get_template_variables(
|
@@ -148,49 +237,97 @@ class ComponentGenerator:
|
|
148
237
|
if custom_template:
|
149
238
|
template_name = custom_template
|
150
239
|
|
151
|
-
# Render and write file
|
152
|
-
|
240
|
+
# Render and write file with performance tracking
|
241
|
+
with PerformanceTracker(
|
242
|
+
"template_rendering",
|
243
|
+
template_name=template_name,
|
244
|
+
component_type=component_type,
|
245
|
+
):
|
246
|
+
content = self._render_template(template_name, template_vars)
|
153
247
|
|
154
|
-
# Write file with atomic operations and error handling
|
248
|
+
# Write file with atomic operations and error handling using secure path
|
155
249
|
backup_path = None
|
156
|
-
lock_file =
|
250
|
+
lock_file = secure_file_path.with_suffix(f"{secure_file_path.suffix}.lock")
|
157
251
|
|
158
252
|
try:
|
159
253
|
# Use file locking to prevent race conditions
|
160
|
-
import fcntl
|
161
254
|
import tempfile
|
162
255
|
|
256
|
+
import portalocker
|
257
|
+
|
163
258
|
# Create lock file for atomic operations
|
164
259
|
with open(lock_file, "w") as lock:
|
165
260
|
try:
|
166
|
-
|
167
|
-
except
|
261
|
+
portalocker.lock(lock, portalocker.LOCK_EX | portalocker.LOCK_NB)
|
262
|
+
except portalocker.LockException:
|
168
263
|
raise ValidationError(
|
169
|
-
f"File {
|
264
|
+
f"File {secure_file_path} is locked by another process"
|
170
265
|
)
|
171
266
|
|
172
267
|
# Create backup if file exists (within lock)
|
173
|
-
if
|
174
|
-
backup_path =
|
268
|
+
if secure_file_path.exists():
|
269
|
+
backup_path = secure_file_path.with_suffix(
|
270
|
+
f"{secure_file_path.suffix}.backup"
|
271
|
+
)
|
175
272
|
# Use atomic copy to prevent corruption
|
176
|
-
with
|
273
|
+
with (
|
274
|
+
open(secure_file_path, "rb") as src,
|
275
|
+
open(backup_path, "wb") as dst,
|
276
|
+
):
|
177
277
|
dst.write(src.read())
|
178
278
|
|
179
279
|
# Atomic write operation to prevent TOCTOU vulnerabilities
|
180
|
-
self._atomic_write_file(
|
280
|
+
self._atomic_write_file(secure_file_path, content)
|
181
281
|
|
182
282
|
# Clean up backup on success
|
183
283
|
if backup_path and backup_path.exists():
|
184
284
|
backup_path.unlink()
|
185
285
|
|
286
|
+
# Success - print message and log
|
287
|
+
self.console.print(f"[green]✓[/green] Created {secure_file_path}")
|
288
|
+
logger.info(
|
289
|
+
"Component created successfully",
|
290
|
+
operation="create_component",
|
291
|
+
file_path=str(secure_file_path),
|
292
|
+
component_name=component_name,
|
293
|
+
component_type=component_type,
|
294
|
+
layer=layer,
|
295
|
+
)
|
296
|
+
# secure_file_path is already guaranteed to be a Path object
|
297
|
+
# Return will happen after finally block
|
298
|
+
|
186
299
|
except Exception as e:
|
300
|
+
# Log the error with context
|
301
|
+
logger.error(
|
302
|
+
"Component creation failed",
|
303
|
+
operation="create_component",
|
304
|
+
error=str(e),
|
305
|
+
error_type=type(e).__name__,
|
306
|
+
component_name=component_name,
|
307
|
+
component_type=component_type,
|
308
|
+
file_path=(
|
309
|
+
str(secure_file_path) if "secure_file_path" in locals() else None
|
310
|
+
),
|
311
|
+
)
|
312
|
+
|
187
313
|
# Rollback on failure
|
188
314
|
if backup_path and backup_path.exists():
|
189
315
|
try:
|
190
|
-
if
|
191
|
-
|
192
|
-
backup_path.rename(
|
316
|
+
if secure_file_path.exists():
|
317
|
+
secure_file_path.unlink()
|
318
|
+
backup_path.rename(secure_file_path)
|
319
|
+
logger.info(
|
320
|
+
"Rollback completed successfully",
|
321
|
+
operation="rollback",
|
322
|
+
file_path=str(secure_file_path),
|
323
|
+
)
|
193
324
|
except Exception as rollback_error:
|
325
|
+
logger.error(
|
326
|
+
"Rollback failed",
|
327
|
+
operation="rollback",
|
328
|
+
error=str(rollback_error),
|
329
|
+
file_path=str(secure_file_path),
|
330
|
+
)
|
194
331
|
self.console.print(
|
195
332
|
f"[red]Error during rollback: {rollback_error}[/red]"
|
196
333
|
)
|
@@ -203,14 +340,11 @@ class ComponentGenerator:
|
|
203
340
|
if lock_file.exists():
|
204
341
|
try:
|
205
342
|
lock_file.unlink()
|
206
|
-
except
|
343
|
+
except (OSError, FileNotFoundError):
|
207
344
|
pass # Ignore cleanup errors
|
208
345
|
|
209
|
-
|
210
|
-
|
211
|
-
if not isinstance(file_path, Path):
|
212
|
-
file_path = Path(file_path)
|
213
|
-
return file_path
|
346
|
+
# Return the secure file path after successful completion
|
347
|
+
return secure_file_path
|
214
348
|
|
215
349
|
def _atomic_write_file(self, file_path: Path, content: str) -> None:
|
216
350
|
"""Write file atomically to prevent corruption and TOCTOU vulnerabilities.
|
@@ -225,19 +359,14 @@ class ComponentGenerator:
|
|
225
359
|
file_path = file_path.resolve()
|
226
360
|
|
227
361
|
# Use secure file operation with locking
|
228
|
-
|
229
|
-
file_path, self._perform_atomic_write, file_path, content
|
230
|
-
)
|
362
|
+
secure_file_operation(file_path, self._perform_atomic_write, file_path, content)
|
231
363
|
|
232
364
|
def _perform_atomic_write(self, file_path: Path, content: str) -> None:
|
233
365
|
"""Perform the actual atomic write operation."""
|
234
|
-
import tempfile
|
235
366
|
import os
|
367
|
+
import tempfile
|
236
368
|
|
237
|
-
#
|
238
|
-
if not isinstance(file_path, Path):
|
239
|
-
file_path = Path(file_path)
|
240
|
-
|
369
|
+
# file_path is already guaranteed to be a Path object by type annotation
|
241
370
|
# Ensure we have a valid parent directory
|
242
371
|
parent_dir = file_path.parent
|
243
372
|
if parent_dir is None or str(parent_dir) == ".":
|
@@ -263,12 +392,11 @@ class ComponentGenerator:
|
|
263
392
|
temp_file.write(content)
|
264
393
|
temp_file.flush()
|
265
394
|
os.fsync(temp_file.fileno()) # Ensure data is written to disk
|
266
|
-
|
395
|
+
# File descriptor is now closed by context manager
|
267
396
|
|
268
397
|
# Atomically move temporary file to target location
|
269
398
|
temp_path_obj = Path(temp_path)
|
270
399
|
temp_path_obj.replace(file_path)
|
271
|
-
temp_path = None # Successfully moved, don't clean up
|
272
400
|
|
273
401
|
# Verify file was written correctly
|
274
402
|
if not file_path.exists() or file_path.stat().st_size == 0:
|
@@ -288,28 +416,24 @@ class ComponentGenerator:
|
|
288
416
|
)
|
289
417
|
else:
|
290
418
|
raise create_secure_error("file_write", "write file", str(e))
|
291
|
-
|
292
|
-
# Clean up on failure
|
293
|
-
if temp_fd is not None:
|
294
|
-
try:
|
295
|
-
os.close(temp_fd)
|
296
|
-
except OSError:
|
297
|
-
pass
|
419
|
+
except Exception:
|
420
|
+
# Clean up temporary file on any other failure
|
298
421
|
if temp_path and Path(temp_path).exists():
|
299
422
|
try:
|
300
423
|
Path(temp_path).unlink()
|
301
424
|
except OSError:
|
302
425
|
pass
|
426
|
+
raise
|
303
427
|
|
304
428
|
def create_multiple_components(
|
305
429
|
self,
|
306
430
|
base_path: Path,
|
307
431
|
system_name: str,
|
308
432
|
module_name: str,
|
309
|
-
components_spec: Dict[str, Dict[str,
|
433
|
+
components_spec: Dict[str, Dict[str, List[str]]],
|
310
434
|
dry_run: bool = False,
|
311
435
|
force: bool = False,
|
312
|
-
) ->
|
436
|
+
) -> List[Path]:
|
313
437
|
"""Create multiple components from specification.
|
314
438
|
|
315
439
|
Args:
|
@@ -380,7 +504,7 @@ class ComponentGenerator:
|
|
380
504
|
layer_components = {
|
381
505
|
"domain": ["entities", "repositories", "value_objects"],
|
382
506
|
"application": ["services", "commands", "queries"],
|
383
|
-
"infrastructure": ["models", "repositories", "external"
|
507
|
+
"infrastructure": ["models", "repositories", "external"],
|
384
508
|
"presentation": ["api", "schemas"],
|
385
509
|
}
|
386
510
|
|
@@ -401,7 +525,7 @@ class ComponentGenerator:
|
|
401
525
|
layer: str,
|
402
526
|
component_type: str,
|
403
527
|
component_name: str,
|
404
|
-
) ->
|
528
|
+
) -> Tuple[Path, str]:
|
405
529
|
"""Get the file path and name for a component."""
|
406
530
|
# Determine file name based on component type
|
407
531
|
file_name = self._get_component_file_name(component_type, component_name)
|
@@ -432,7 +556,6 @@ class ComponentGenerator:
|
|
432
556
|
"queries": f"{snake_name}.py",
|
433
557
|
"models": f"{snake_name}_model.py",
|
434
558
|
"external": f"{snake_name}_client.py",
|
435
|
-
"internal": f"{snake_name}.py",
|
436
559
|
"api": f"{snake_name}_router.py",
|
437
560
|
"schemas": f"{snake_name}_schemas.py",
|
438
561
|
}
|
@@ -463,8 +586,8 @@ class ComponentGenerator:
|
|
463
586
|
|
464
587
|
def _validate_file_system(self, file_path: Path) -> None:
|
465
588
|
"""Validate file system security and basic requirements."""
|
466
|
-
import shutil
|
467
589
|
import os
|
590
|
+
import shutil
|
468
591
|
|
469
592
|
try:
|
470
593
|
# Prevent symlink attacks by checking the entire path
|
@@ -533,7 +656,14 @@ class ComponentGenerator:
|
|
533
656
|
while current_path != current_path.parent:
|
534
657
|
if current_path.exists() and current_path.is_symlink():
|
535
658
|
# Instead of hardcoded allowlist, use more sophisticated validation
|
536
|
-
|
659
|
+
# Use os.readlink for Python 3.8 compatibility
|
660
|
+
import os
|
661
|
+
|
662
|
+
try:
|
663
|
+
symlink_target = Path(os.readlink(current_path))
|
664
|
+
except (OSError, AttributeError):
|
665
|
+
# If readlink fails, treat as unsafe
|
666
|
+
raise ValidationError(f"Cannot resolve symlink: {current_path}")
|
537
667
|
|
538
668
|
# Check if symlink points outside the project or to dangerous locations
|
539
669
|
if symlink_target.is_absolute():
|
@@ -597,7 +727,7 @@ class ComponentGenerator:
|
|
597
727
|
safe_patterns = [
|
598
728
|
# Unix/Linux system symlinks
|
599
729
|
("/var", "/private/var"), # macOS /var -> /private/var
|
600
|
-
("/tmp", "/private/tmp"), # macOS /tmp -> /private/tmp
|
730
|
+
("/tmp", "/private/tmp"), # macOS /tmp -> /private/tmp # nosec B108
|
601
731
|
("/etc", "/private/etc"), # macOS /etc -> /private/etc
|
602
732
|
# Other common system symlinks
|
603
733
|
("/usr/bin", "/bin"),
|
@@ -668,8 +798,8 @@ class ComponentGenerator:
|
|
668
798
|
@functools.lru_cache(maxsize=128)
|
669
799
|
def _is_temp_path_cached(self, path_str: str) -> bool:
|
670
800
|
"""Cached implementation of temporary path detection."""
|
671
|
-
import tempfile
|
672
801
|
import os
|
802
|
+
import tempfile
|
673
803
|
|
674
804
|
path = Path(path_str)
|
675
805
|
|
@@ -679,8 +809,15 @@ class ComponentGenerator:
|
|
679
809
|
|
680
810
|
# Check if path is relative to system temp directory
|
681
811
|
try:
|
682
|
-
|
812
|
+
# Use compatibility method for Python 3.8
|
813
|
+
resolved_path = path.resolve()
|
814
|
+
# Check if path is under system temp using relative_to
|
815
|
+
try:
|
816
|
+
resolved_path.relative_to(system_temp)
|
683
817
|
return True
|
818
|
+
except ValueError:
|
819
|
+
# Path is not relative to system temp
|
820
|
+
pass
|
684
821
|
except (AttributeError, ValueError):
|
685
822
|
# Fallback for older Python versions or path resolution issues
|
686
823
|
if str(path.resolve()).startswith(str(system_temp)):
|
@@ -725,10 +862,10 @@ class ComponentGenerator:
|
|
725
862
|
|
726
863
|
# Check for common temp directory patterns
|
727
864
|
temp_patterns = [
|
728
|
-
"/tmp/",
|
865
|
+
"/tmp/", # nosec B108
|
729
866
|
"/temp/",
|
730
867
|
"/temporary/",
|
731
|
-
"/var/tmp/",
|
868
|
+
"/var/tmp/", # nosec B108
|
732
869
|
"/var/temp/",
|
733
870
|
"/private/var/folders/", # macOS temp
|
734
871
|
"appdata/local/temp/", # Windows temp
|
@@ -818,8 +955,8 @@ class ComponentGenerator:
|
|
818
955
|
raise ValidationError(
|
819
956
|
f"Template variable nesting too deep (max depth: 10, current: {depth})"
|
820
957
|
)
|
821
|
-
sanitized_vars = {}
|
822
|
-
suspicious_patterns_found = []
|
958
|
+
sanitized_vars: Dict[str, Any] = {}
|
959
|
+
suspicious_patterns_found: List[str] = []
|
823
960
|
|
824
961
|
for key, value in template_vars.items():
|
825
962
|
if isinstance(value, str):
|
@@ -834,7 +971,7 @@ class ComponentGenerator:
|
|
834
971
|
sanitized_vars[key] = value
|
835
972
|
elif isinstance(value, (list, tuple)):
|
836
973
|
# Recursively sanitize list/tuple items
|
837
|
-
sanitized_list = []
|
974
|
+
sanitized_list: List[Any] = []
|
838
975
|
for item in value:
|
839
976
|
if isinstance(item, dict):
|
840
977
|
sanitized_list.append(
|
@@ -860,21 +997,21 @@ class ComponentGenerator:
|
|
860
997
|
|
861
998
|
return sanitized_vars
|
862
999
|
|
863
|
-
def _sanitize_string_value(self, value:
|
1000
|
+
def _sanitize_string_value(self, value: Any) -> str:
|
864
1001
|
"""Enhanced sanitization for string values to prevent various injection attacks."""
|
1002
|
+
import unicodedata
|
1003
|
+
import urllib.parse
|
1004
|
+
|
865
1005
|
if not isinstance(value, str):
|
866
1006
|
value = str(value)
|
867
1007
|
|
868
1008
|
# URL decode to catch encoded injection attempts
|
869
|
-
import urllib.parse
|
870
|
-
|
871
1009
|
try:
|
872
1010
|
decoded_value = urllib.parse.unquote(value)
|
873
|
-
except
|
1011
|
+
except (ValueError, UnicodeDecodeError):
|
874
1012
|
decoded_value = value
|
875
1013
|
|
876
1014
|
# Unicode normalization to prevent Unicode-based attacks
|
877
|
-
import unicodedata
|
878
1015
|
|
879
1016
|
normalized_value = unicodedata.normalize("NFKC", decoded_value)
|
880
1017
|
|
@@ -908,8 +1045,8 @@ class ComponentGenerator:
|
|
908
1045
|
# Remove control characters and dangerous Unicode categories
|
909
1046
|
sanitized = re.sub(r"[\x00-\x1f\x7f-\x9f]", "", sanitized)
|
910
1047
|
|
911
|
-
# Remove potentially dangerous characters
|
912
|
-
sanitized = re.sub(r'[<>"\'
|
1048
|
+
# Remove potentially dangerous characters (but preserve forward slashes for URLs)
|
1049
|
+
sanitized = re.sub(r'[<>"\'\\`$]', "", sanitized)
|
913
1050
|
|
914
1051
|
# Limit length to prevent DoS
|
915
1052
|
if len(sanitized) > 1000:
|
@@ -1037,3 +1174,170 @@ class ComponentGenerator:
|
|
1037
1174
|
raise TemplateError(f"Error rendering template {template_name}: {e}")
|
1038
1175
|
except Exception as e:
|
1039
1176
|
raise TemplateError(f"Unexpected error rendering template: {e}")
|
1177
|
+
|
1178
|
+
def validate_component(self, component: Dict[str, Any]) -> bool:
|
1179
|
+
"""Validate component configuration and structure.
|
1180
|
+
|
1181
|
+
This method implements the ComponentGeneratorProtocol interface
|
1182
|
+
and provides comprehensive validation using type-safe strategies.
|
1183
|
+
|
1184
|
+
Args:
|
1185
|
+
component: Component configuration dictionary
|
1186
|
+
|
1187
|
+
Returns:
|
1188
|
+
True if component is valid
|
1189
|
+
|
1190
|
+
Raises:
|
1191
|
+
ValidationError: If component is invalid
|
1192
|
+
"""
|
1193
|
+
if not isinstance(component, dict):
|
1194
|
+
raise ValidationError(
|
1195
|
+
f"Component must be a dictionary, got {type(component)}"
|
1196
|
+
)
|
1197
|
+
|
1198
|
+
# Extract component type for validation strategy selection
|
1199
|
+
component_type = component.get("type", "unknown")
|
1200
|
+
|
1201
|
+
# Use appropriate validation strategy
|
1202
|
+
if component_type in self._validation_strategies:
|
1203
|
+
strategy = self._validation_strategies[component_type]
|
1204
|
+
return strategy.validate(component)
|
1205
|
+
|
1206
|
+
# Fallback to basic validation
|
1207
|
+
required_fields = ["name", "type"]
|
1208
|
+
for field in required_fields:
|
1209
|
+
if field not in component:
|
1210
|
+
raise ValidationError(f"Missing required field '{field}' in component")
|
1211
|
+
|
1212
|
+
# Validate component name
|
1213
|
+
try:
|
1214
|
+
validate_name(component["name"])
|
1215
|
+
except (ValueError, TypeError) as e:
|
1216
|
+
raise ValidationError(f"Invalid component name: {e}")
|
1217
|
+
except SecurityError:
|
1218
|
+
# Re-raise SecurityError as-is for proper handling
|
1219
|
+
raise
|
1220
|
+
|
1221
|
+
return True
|
1222
|
+
|
1223
|
+
def _setup_validation_strategies(
|
1224
|
+
self,
|
1225
|
+
) -> Dict[str, ComponentValidationStrategy[str]]:
|
1226
|
+
"""Setup type-safe validation strategies for different component types.
|
1227
|
+
|
1228
|
+
Returns:
|
1229
|
+
Dictionary mapping component types to their validation strategies
|
1230
|
+
"""
|
1231
|
+
strategies = {}
|
1232
|
+
|
1233
|
+
# Entity validation strategy
|
1234
|
+
entity_rules = {
|
1235
|
+
"required_fields": ["name", "type"],
|
1236
|
+
"field_types": {
|
1237
|
+
"name": str,
|
1238
|
+
"type": str,
|
1239
|
+
"attributes": (list, type(None)),
|
1240
|
+
"methods": (list, type(None)),
|
1241
|
+
},
|
1242
|
+
}
|
1243
|
+
strategies["entity"] = ComponentValidationStrategy("entity", entity_rules)
|
1244
|
+
|
1245
|
+
# Service validation strategy
|
1246
|
+
service_rules = {
|
1247
|
+
"required_fields": ["name", "type"],
|
1248
|
+
"field_types": {
|
1249
|
+
"name": str,
|
1250
|
+
"type": str,
|
1251
|
+
"dependencies": (list, type(None)),
|
1252
|
+
"methods": (list, type(None)),
|
1253
|
+
},
|
1254
|
+
}
|
1255
|
+
strategies["service"] = ComponentValidationStrategy("service", service_rules)
|
1256
|
+
|
1257
|
+
# Repository validation strategy
|
1258
|
+
repository_rules = {
|
1259
|
+
"required_fields": ["name", "type"],
|
1260
|
+
"field_types": {
|
1261
|
+
"name": str,
|
1262
|
+
"type": str,
|
1263
|
+
"entity_type": (str, type(None)),
|
1264
|
+
"methods": (list, type(None)),
|
1265
|
+
},
|
1266
|
+
}
|
1267
|
+
strategies["repository"] = ComponentValidationStrategy(
|
1268
|
+
"repository", repository_rules
|
1269
|
+
)
|
1270
|
+
|
1271
|
+
# Add more strategies for other component types
|
1272
|
+
for component_type in [
|
1273
|
+
"value_object",
|
1274
|
+
"command",
|
1275
|
+
"query",
|
1276
|
+
"model",
|
1277
|
+
"external",
|
1278
|
+
"api",
|
1279
|
+
"schema",
|
1280
|
+
]:
|
1281
|
+
basic_rules = {
|
1282
|
+
"required_fields": ["name", "type"],
|
1283
|
+
"field_types": {
|
1284
|
+
"name": str,
|
1285
|
+
"type": str,
|
1286
|
+
},
|
1287
|
+
}
|
1288
|
+
strategies[component_type] = ComponentValidationStrategy(
|
1289
|
+
component_type, basic_rules
|
1290
|
+
)
|
1291
|
+
|
1292
|
+
return strategies
|
1293
|
+
|
1294
|
+
def create_component_with_validation(
|
1295
|
+
self,
|
1296
|
+
component_config: Dict[str, Any],
|
1297
|
+
base_path: Path,
|
1298
|
+
dry_run: bool = False,
|
1299
|
+
force: bool = False,
|
1300
|
+
) -> Path:
|
1301
|
+
"""Create a component with enhanced type-safe validation.
|
1302
|
+
|
1303
|
+
This method provides an alternative interface that uses the new
|
1304
|
+
validation strategies and secure path handling.
|
1305
|
+
|
1306
|
+
Args:
|
1307
|
+
component_config: Component configuration dictionary
|
1308
|
+
base_path: Base directory for component creation
|
1309
|
+
dry_run: If True, only simulate the operation
|
1310
|
+
force: If True, overwrite existing files
|
1311
|
+
|
1312
|
+
Returns:
|
1313
|
+
Path to the created component file
|
1314
|
+
|
1315
|
+
Raises:
|
1316
|
+
ValidationError: If component configuration is invalid
|
1317
|
+
SecurityError: If security constraints are violated
|
1318
|
+
"""
|
1319
|
+
# Validate component configuration
|
1320
|
+
self.validate_component(component_config)
|
1321
|
+
|
1322
|
+
# Validate and secure the base path
|
1323
|
+
secure_base_path = Path(self.path_handler.process(base_path))
|
1324
|
+
|
1325
|
+
# Extract component details
|
1326
|
+
system_name = component_config.get("system_name", "default")
|
1327
|
+
module_name = component_config.get("module_name", "default")
|
1328
|
+
layer = component_config.get("layer", "domain")
|
1329
|
+
component_type = component_config["type"]
|
1330
|
+
component_name = component_config["name"]
|
1331
|
+
|
1332
|
+
# Use the existing create_component method with validated inputs
|
1333
|
+
result = self.create_component(
|
1334
|
+
base_path=secure_base_path,
|
1335
|
+
system_name=system_name,
|
1336
|
+
module_name=module_name,
|
1337
|
+
layer=layer,
|
1338
|
+
component_type=component_type,
|
1339
|
+
component_name=component_name,
|
1340
|
+
dry_run=dry_run,
|
1341
|
+
force=force,
|
1342
|
+
)
|
1343
|
+
return Path(result)
|