tnfr 3.0.3__py3-none-any.whl → 8.5.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 tnfr might be problematic. Click here for more details.
- tnfr/__init__.py +375 -56
- tnfr/__init__.pyi +33 -0
- tnfr/_compat.py +10 -0
- tnfr/_generated_version.py +34 -0
- tnfr/_version.py +49 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +723 -0
- tnfr/alias.pyi +108 -0
- tnfr/backends/__init__.py +354 -0
- tnfr/backends/jax_backend.py +173 -0
- tnfr/backends/numpy_backend.py +238 -0
- tnfr/backends/optimized_numpy.py +420 -0
- tnfr/backends/torch_backend.py +408 -0
- tnfr/cache.py +171 -0
- tnfr/cache.pyi +13 -0
- tnfr/cli/__init__.py +110 -0
- tnfr/cli/__init__.pyi +26 -0
- tnfr/cli/arguments.py +489 -0
- tnfr/cli/arguments.pyi +29 -0
- tnfr/cli/execution.py +914 -0
- tnfr/cli/execution.pyi +70 -0
- tnfr/cli/interactive_validator.py +614 -0
- tnfr/cli/utils.py +51 -0
- tnfr/cli/utils.pyi +7 -0
- tnfr/cli/validate.py +236 -0
- tnfr/compat/__init__.py +85 -0
- tnfr/compat/dataclass.py +136 -0
- tnfr/compat/jsonschema_stub.py +61 -0
- tnfr/compat/matplotlib_stub.py +73 -0
- tnfr/compat/numpy_stub.py +155 -0
- tnfr/config/__init__.py +224 -0
- tnfr/config/__init__.pyi +10 -0
- tnfr/config/constants.py +104 -0
- tnfr/config/constants.pyi +12 -0
- tnfr/config/defaults.py +54 -0
- tnfr/config/defaults_core.py +212 -0
- tnfr/config/defaults_init.py +33 -0
- tnfr/config/defaults_metric.py +104 -0
- tnfr/config/feature_flags.py +81 -0
- tnfr/config/feature_flags.pyi +16 -0
- tnfr/config/glyph_constants.py +31 -0
- tnfr/config/init.py +77 -0
- tnfr/config/init.pyi +8 -0
- tnfr/config/operator_names.py +254 -0
- tnfr/config/operator_names.pyi +36 -0
- tnfr/config/physics_derivation.py +354 -0
- tnfr/config/presets.py +83 -0
- tnfr/config/presets.pyi +7 -0
- tnfr/config/security.py +927 -0
- tnfr/config/thresholds.py +114 -0
- tnfr/config/tnfr_config.py +498 -0
- tnfr/constants/__init__.py +92 -0
- tnfr/constants/__init__.pyi +92 -0
- tnfr/constants/aliases.py +33 -0
- tnfr/constants/aliases.pyi +27 -0
- tnfr/constants/init.py +33 -0
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +104 -0
- tnfr/constants/metric.pyi +19 -0
- tnfr/core/__init__.py +33 -0
- tnfr/core/container.py +226 -0
- tnfr/core/default_implementations.py +329 -0
- tnfr/core/interfaces.py +279 -0
- tnfr/dynamics/__init__.py +238 -0
- tnfr/dynamics/__init__.pyi +83 -0
- tnfr/dynamics/adaptation.py +267 -0
- tnfr/dynamics/adaptation.pyi +7 -0
- tnfr/dynamics/adaptive_sequences.py +189 -0
- tnfr/dynamics/adaptive_sequences.pyi +14 -0
- tnfr/dynamics/aliases.py +23 -0
- tnfr/dynamics/aliases.pyi +19 -0
- tnfr/dynamics/bifurcation.py +232 -0
- tnfr/dynamics/canonical.py +229 -0
- tnfr/dynamics/canonical.pyi +48 -0
- tnfr/dynamics/coordination.py +385 -0
- tnfr/dynamics/coordination.pyi +25 -0
- tnfr/dynamics/dnfr.py +3034 -0
- tnfr/dynamics/dnfr.pyi +26 -0
- tnfr/dynamics/dynamic_limits.py +225 -0
- tnfr/dynamics/feedback.py +252 -0
- tnfr/dynamics/feedback.pyi +24 -0
- tnfr/dynamics/fused_dnfr.py +454 -0
- tnfr/dynamics/homeostasis.py +157 -0
- tnfr/dynamics/homeostasis.pyi +14 -0
- tnfr/dynamics/integrators.py +661 -0
- tnfr/dynamics/integrators.pyi +36 -0
- tnfr/dynamics/learning.py +310 -0
- tnfr/dynamics/learning.pyi +33 -0
- tnfr/dynamics/metabolism.py +254 -0
- tnfr/dynamics/nbody.py +796 -0
- tnfr/dynamics/nbody_tnfr.py +783 -0
- tnfr/dynamics/propagation.py +326 -0
- tnfr/dynamics/runtime.py +908 -0
- tnfr/dynamics/runtime.pyi +77 -0
- tnfr/dynamics/sampling.py +36 -0
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +711 -0
- tnfr/dynamics/selectors.pyi +85 -0
- tnfr/dynamics/structural_clip.py +207 -0
- tnfr/errors/__init__.py +37 -0
- tnfr/errors/contextual.py +492 -0
- tnfr/execution.py +223 -0
- tnfr/execution.pyi +45 -0
- tnfr/extensions/__init__.py +205 -0
- tnfr/extensions/__init__.pyi +18 -0
- tnfr/extensions/base.py +173 -0
- tnfr/extensions/base.pyi +35 -0
- tnfr/extensions/business/__init__.py +71 -0
- tnfr/extensions/business/__init__.pyi +11 -0
- tnfr/extensions/business/cookbook.py +88 -0
- tnfr/extensions/business/cookbook.pyi +8 -0
- tnfr/extensions/business/health_analyzers.py +202 -0
- tnfr/extensions/business/health_analyzers.pyi +9 -0
- tnfr/extensions/business/patterns.py +183 -0
- tnfr/extensions/business/patterns.pyi +8 -0
- tnfr/extensions/medical/__init__.py +73 -0
- tnfr/extensions/medical/__init__.pyi +11 -0
- tnfr/extensions/medical/cookbook.py +88 -0
- tnfr/extensions/medical/cookbook.pyi +8 -0
- tnfr/extensions/medical/health_analyzers.py +181 -0
- tnfr/extensions/medical/health_analyzers.pyi +9 -0
- tnfr/extensions/medical/patterns.py +163 -0
- tnfr/extensions/medical/patterns.pyi +8 -0
- tnfr/flatten.py +262 -0
- tnfr/flatten.pyi +21 -0
- tnfr/gamma.py +354 -0
- tnfr/gamma.pyi +36 -0
- tnfr/glyph_history.py +377 -0
- tnfr/glyph_history.pyi +35 -0
- tnfr/glyph_runtime.py +19 -0
- tnfr/glyph_runtime.pyi +8 -0
- tnfr/immutable.py +218 -0
- tnfr/immutable.pyi +36 -0
- tnfr/initialization.py +203 -0
- tnfr/initialization.pyi +65 -0
- tnfr/io.py +10 -0
- tnfr/io.pyi +13 -0
- tnfr/locking.py +37 -0
- tnfr/locking.pyi +7 -0
- tnfr/mathematics/__init__.py +79 -0
- tnfr/mathematics/backend.py +453 -0
- tnfr/mathematics/backend.pyi +99 -0
- tnfr/mathematics/dynamics.py +408 -0
- tnfr/mathematics/dynamics.pyi +90 -0
- tnfr/mathematics/epi.py +391 -0
- tnfr/mathematics/epi.pyi +65 -0
- tnfr/mathematics/generators.py +242 -0
- tnfr/mathematics/generators.pyi +29 -0
- tnfr/mathematics/metrics.py +119 -0
- tnfr/mathematics/metrics.pyi +16 -0
- tnfr/mathematics/operators.py +239 -0
- tnfr/mathematics/operators.pyi +59 -0
- tnfr/mathematics/operators_factory.py +124 -0
- tnfr/mathematics/operators_factory.pyi +11 -0
- tnfr/mathematics/projection.py +87 -0
- tnfr/mathematics/projection.pyi +33 -0
- tnfr/mathematics/runtime.py +182 -0
- tnfr/mathematics/runtime.pyi +64 -0
- tnfr/mathematics/spaces.py +256 -0
- tnfr/mathematics/spaces.pyi +83 -0
- tnfr/mathematics/transforms.py +305 -0
- tnfr/mathematics/transforms.pyi +62 -0
- tnfr/metrics/__init__.py +79 -0
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/buffer_cache.py +163 -0
- tnfr/metrics/buffer_cache.pyi +24 -0
- tnfr/metrics/cache_utils.py +214 -0
- tnfr/metrics/coherence.py +2009 -0
- tnfr/metrics/coherence.pyi +129 -0
- tnfr/metrics/common.py +158 -0
- tnfr/metrics/common.pyi +35 -0
- tnfr/metrics/core.py +316 -0
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +833 -0
- tnfr/metrics/diagnosis.pyi +86 -0
- tnfr/metrics/emergence.py +245 -0
- tnfr/metrics/export.py +179 -0
- tnfr/metrics/export.pyi +7 -0
- tnfr/metrics/glyph_timing.py +379 -0
- tnfr/metrics/glyph_timing.pyi +81 -0
- tnfr/metrics/learning_metrics.py +280 -0
- tnfr/metrics/learning_metrics.pyi +21 -0
- tnfr/metrics/phase_coherence.py +351 -0
- tnfr/metrics/phase_compatibility.py +349 -0
- tnfr/metrics/reporting.py +183 -0
- tnfr/metrics/reporting.pyi +25 -0
- tnfr/metrics/sense_index.py +1203 -0
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +373 -0
- tnfr/metrics/trig.pyi +13 -0
- tnfr/metrics/trig_cache.py +233 -0
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/multiscale/__init__.py +32 -0
- tnfr/multiscale/hierarchical.py +517 -0
- tnfr/node.py +763 -0
- tnfr/node.pyi +139 -0
- tnfr/observers.py +255 -130
- tnfr/observers.pyi +31 -0
- tnfr/ontosim.py +144 -137
- tnfr/ontosim.pyi +28 -0
- tnfr/operators/__init__.py +1672 -0
- tnfr/operators/__init__.pyi +31 -0
- tnfr/operators/algebra.py +277 -0
- tnfr/operators/canonical_patterns.py +420 -0
- tnfr/operators/cascade.py +267 -0
- tnfr/operators/cycle_detection.py +358 -0
- tnfr/operators/definitions.py +4108 -0
- tnfr/operators/definitions.pyi +78 -0
- tnfr/operators/grammar.py +1164 -0
- tnfr/operators/grammar.pyi +140 -0
- tnfr/operators/hamiltonian.py +710 -0
- tnfr/operators/health_analyzer.py +809 -0
- tnfr/operators/jitter.py +272 -0
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/lifecycle.py +314 -0
- tnfr/operators/metabolism.py +618 -0
- tnfr/operators/metrics.py +2138 -0
- tnfr/operators/network_analysis/__init__.py +27 -0
- tnfr/operators/network_analysis/source_detection.py +186 -0
- tnfr/operators/nodal_equation.py +395 -0
- tnfr/operators/pattern_detection.py +660 -0
- tnfr/operators/patterns.py +669 -0
- tnfr/operators/postconditions/__init__.py +38 -0
- tnfr/operators/postconditions/mutation.py +236 -0
- tnfr/operators/preconditions/__init__.py +1226 -0
- tnfr/operators/preconditions/coherence.py +305 -0
- tnfr/operators/preconditions/dissonance.py +236 -0
- tnfr/operators/preconditions/emission.py +128 -0
- tnfr/operators/preconditions/mutation.py +580 -0
- tnfr/operators/preconditions/reception.py +125 -0
- tnfr/operators/preconditions/resonance.py +364 -0
- tnfr/operators/registry.py +74 -0
- tnfr/operators/registry.pyi +9 -0
- tnfr/operators/remesh.py +1809 -0
- tnfr/operators/remesh.pyi +26 -0
- tnfr/operators/structural_units.py +268 -0
- tnfr/operators/unified_grammar.py +105 -0
- tnfr/parallel/__init__.py +54 -0
- tnfr/parallel/auto_scaler.py +234 -0
- tnfr/parallel/distributed.py +384 -0
- tnfr/parallel/engine.py +238 -0
- tnfr/parallel/gpu_engine.py +420 -0
- tnfr/parallel/monitoring.py +248 -0
- tnfr/parallel/partitioner.py +459 -0
- tnfr/py.typed +0 -0
- tnfr/recipes/__init__.py +22 -0
- tnfr/recipes/cookbook.py +743 -0
- tnfr/rng.py +178 -0
- tnfr/rng.pyi +26 -0
- tnfr/schemas/__init__.py +8 -0
- tnfr/schemas/grammar.json +94 -0
- tnfr/sdk/__init__.py +107 -0
- tnfr/sdk/__init__.pyi +19 -0
- tnfr/sdk/adaptive_system.py +173 -0
- tnfr/sdk/adaptive_system.pyi +21 -0
- tnfr/sdk/builders.py +370 -0
- tnfr/sdk/builders.pyi +51 -0
- tnfr/sdk/fluent.py +1121 -0
- tnfr/sdk/fluent.pyi +74 -0
- tnfr/sdk/templates.py +342 -0
- tnfr/sdk/templates.pyi +41 -0
- tnfr/sdk/utils.py +341 -0
- tnfr/secure_config.py +46 -0
- tnfr/security/__init__.py +70 -0
- tnfr/security/database.py +514 -0
- tnfr/security/subprocess.py +503 -0
- tnfr/security/validation.py +290 -0
- tnfr/selector.py +247 -0
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +378 -0
- tnfr/sense.pyi +23 -0
- tnfr/services/__init__.py +17 -0
- tnfr/services/orchestrator.py +325 -0
- tnfr/sparse/__init__.py +39 -0
- tnfr/sparse/representations.py +492 -0
- tnfr/structural.py +705 -0
- tnfr/structural.pyi +83 -0
- tnfr/telemetry/__init__.py +35 -0
- tnfr/telemetry/cache_metrics.py +226 -0
- tnfr/telemetry/cache_metrics.pyi +64 -0
- tnfr/telemetry/nu_f.py +422 -0
- tnfr/telemetry/nu_f.pyi +108 -0
- tnfr/telemetry/verbosity.py +36 -0
- tnfr/telemetry/verbosity.pyi +15 -0
- tnfr/tokens.py +58 -0
- tnfr/tokens.pyi +36 -0
- tnfr/tools/__init__.py +20 -0
- tnfr/tools/domain_templates.py +478 -0
- tnfr/tools/sequence_generator.py +846 -0
- tnfr/topology/__init__.py +13 -0
- tnfr/topology/asymmetry.py +151 -0
- tnfr/trace.py +543 -0
- tnfr/trace.pyi +42 -0
- tnfr/tutorials/__init__.py +38 -0
- tnfr/tutorials/autonomous_evolution.py +285 -0
- tnfr/tutorials/interactive.py +1576 -0
- tnfr/tutorials/structural_metabolism.py +238 -0
- tnfr/types.py +775 -0
- tnfr/types.pyi +357 -0
- tnfr/units.py +68 -0
- tnfr/units.pyi +13 -0
- tnfr/utils/__init__.py +282 -0
- tnfr/utils/__init__.pyi +215 -0
- tnfr/utils/cache.py +4223 -0
- tnfr/utils/cache.pyi +470 -0
- tnfr/utils/callbacks.py +375 -0
- tnfr/utils/callbacks.pyi +49 -0
- tnfr/utils/chunks.py +108 -0
- tnfr/utils/chunks.pyi +22 -0
- tnfr/utils/data.py +428 -0
- tnfr/utils/data.pyi +74 -0
- tnfr/utils/graph.py +85 -0
- tnfr/utils/graph.pyi +10 -0
- tnfr/utils/init.py +821 -0
- tnfr/utils/init.pyi +80 -0
- tnfr/utils/io.py +559 -0
- tnfr/utils/io.pyi +66 -0
- tnfr/utils/numeric.py +114 -0
- tnfr/utils/numeric.pyi +21 -0
- tnfr/validation/__init__.py +257 -0
- tnfr/validation/__init__.pyi +85 -0
- tnfr/validation/compatibility.py +460 -0
- tnfr/validation/compatibility.pyi +6 -0
- tnfr/validation/config.py +73 -0
- tnfr/validation/graph.py +139 -0
- tnfr/validation/graph.pyi +18 -0
- tnfr/validation/input_validation.py +755 -0
- tnfr/validation/invariants.py +712 -0
- tnfr/validation/rules.py +253 -0
- tnfr/validation/rules.pyi +44 -0
- tnfr/validation/runtime.py +279 -0
- tnfr/validation/runtime.pyi +28 -0
- tnfr/validation/sequence_validator.py +162 -0
- tnfr/validation/soft_filters.py +170 -0
- tnfr/validation/soft_filters.pyi +32 -0
- tnfr/validation/spectral.py +164 -0
- tnfr/validation/spectral.pyi +42 -0
- tnfr/validation/validator.py +1266 -0
- tnfr/validation/window.py +39 -0
- tnfr/validation/window.pyi +1 -0
- tnfr/visualization/__init__.py +98 -0
- tnfr/visualization/cascade_viz.py +256 -0
- tnfr/visualization/hierarchy.py +284 -0
- tnfr/visualization/sequence_plotter.py +784 -0
- tnfr/viz/__init__.py +60 -0
- tnfr/viz/matplotlib.py +278 -0
- tnfr/viz/matplotlib.pyi +35 -0
- tnfr-8.5.0.dist-info/METADATA +573 -0
- tnfr-8.5.0.dist-info/RECORD +353 -0
- tnfr-8.5.0.dist-info/entry_points.txt +3 -0
- tnfr-3.0.3.dist-info/licenses/LICENSE.txt → tnfr-8.5.0.dist-info/licenses/LICENSE.md +1 -1
- tnfr/constants.py +0 -183
- tnfr/dynamics.py +0 -543
- tnfr/helpers.py +0 -198
- tnfr/main.py +0 -37
- tnfr/operators.py +0 -296
- tnfr-3.0.3.dist-info/METADATA +0 -35
- tnfr-3.0.3.dist-info/RECORD +0 -13
- {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
- {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"""Command execution security utilities for TNFR.
|
|
2
|
+
|
|
3
|
+
This module provides secure wrappers for subprocess execution and input validation
|
|
4
|
+
to prevent command injection attacks while maintaining TNFR structural coherence.
|
|
5
|
+
|
|
6
|
+
TNFR Context
|
|
7
|
+
------------
|
|
8
|
+
These utilities ensure that external process execution maintains the integrity of
|
|
9
|
+
the TNFR computational environment without introducing security vulnerabilities.
|
|
10
|
+
They act as a coherence boundary between user input and system command execution.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import subprocess
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Sequence
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"validate_git_ref",
|
|
23
|
+
"validate_path_safe",
|
|
24
|
+
"validate_file_path",
|
|
25
|
+
"resolve_safe_path",
|
|
26
|
+
"validate_version_string",
|
|
27
|
+
"run_command_safely",
|
|
28
|
+
"CommandValidationError",
|
|
29
|
+
"PathTraversalError",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CommandValidationError(ValueError):
|
|
34
|
+
"""Raised when command input validation fails."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PathTraversalError(ValueError):
|
|
38
|
+
"""Raised when path traversal attempt is detected."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Allowlisted commands that are safe to execute
|
|
42
|
+
ALLOWED_COMMANDS = frozenset(
|
|
43
|
+
{
|
|
44
|
+
"git",
|
|
45
|
+
"python",
|
|
46
|
+
"python3",
|
|
47
|
+
"stubgen",
|
|
48
|
+
"gh",
|
|
49
|
+
"pip",
|
|
50
|
+
"twine",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Pattern for valid git refs (branches, tags, commit SHAs)
|
|
55
|
+
GIT_REF_PATTERN = re.compile(r"^[a-zA-Z0-9/_\-\.]+$")
|
|
56
|
+
|
|
57
|
+
# Pattern for semantic version strings
|
|
58
|
+
VERSION_PATTERN = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)(-[a-zA-Z0-9\-\.]+)?$")
|
|
59
|
+
|
|
60
|
+
# Pattern for safe path components (no path traversal)
|
|
61
|
+
SAFE_PATH_PATTERN = re.compile(r"^[a-zA-Z0-9/_\-\.]+$")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def validate_git_ref(ref: str) -> str:
|
|
65
|
+
"""Validate a git reference (branch, tag, or SHA).
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
ref : str
|
|
70
|
+
The git reference to validate.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
str
|
|
75
|
+
The validated reference.
|
|
76
|
+
|
|
77
|
+
Raises
|
|
78
|
+
------
|
|
79
|
+
CommandValidationError
|
|
80
|
+
If the reference contains invalid characters.
|
|
81
|
+
|
|
82
|
+
Examples
|
|
83
|
+
--------
|
|
84
|
+
>>> validate_git_ref("main")
|
|
85
|
+
'main'
|
|
86
|
+
>>> validate_git_ref("feature/new-operator")
|
|
87
|
+
'feature/new-operator'
|
|
88
|
+
>>> validate_git_ref("v1.0.0")
|
|
89
|
+
'v1.0.0'
|
|
90
|
+
>>> validate_git_ref("abc123def")
|
|
91
|
+
'abc123def'
|
|
92
|
+
"""
|
|
93
|
+
if not ref:
|
|
94
|
+
raise CommandValidationError("Git reference cannot be empty")
|
|
95
|
+
|
|
96
|
+
if not GIT_REF_PATTERN.match(ref):
|
|
97
|
+
raise CommandValidationError(
|
|
98
|
+
f"Invalid git reference: {ref!r}. "
|
|
99
|
+
"References must contain only alphanumeric characters, "
|
|
100
|
+
"hyphens, underscores, slashes, and dots."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Additional security: prevent path traversal patterns
|
|
104
|
+
if ".." in ref or ref.startswith("/") or ref.startswith("~"):
|
|
105
|
+
raise CommandValidationError(
|
|
106
|
+
f"Invalid git reference: {ref!r}. "
|
|
107
|
+
"References cannot contain path traversal patterns."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return ref
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def validate_version_string(version: str) -> str:
|
|
114
|
+
"""Validate a semantic version string.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
version : str
|
|
119
|
+
The version string to validate.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
str
|
|
124
|
+
The validated version string.
|
|
125
|
+
|
|
126
|
+
Raises
|
|
127
|
+
------
|
|
128
|
+
CommandValidationError
|
|
129
|
+
If the version string is invalid.
|
|
130
|
+
|
|
131
|
+
Examples
|
|
132
|
+
--------
|
|
133
|
+
>>> validate_version_string("1.0.0")
|
|
134
|
+
'1.0.0'
|
|
135
|
+
>>> validate_version_string("v16.2.3")
|
|
136
|
+
'v16.2.3'
|
|
137
|
+
>>> validate_version_string("2.0.0-beta.1")
|
|
138
|
+
'2.0.0-beta.1'
|
|
139
|
+
"""
|
|
140
|
+
if not version:
|
|
141
|
+
raise CommandValidationError("Version string cannot be empty")
|
|
142
|
+
|
|
143
|
+
if not VERSION_PATTERN.match(version):
|
|
144
|
+
raise CommandValidationError(
|
|
145
|
+
f"Invalid version string: {version!r}. "
|
|
146
|
+
"Version must follow semantic versioning (e.g., '1.0.0' or 'v1.0.0')."
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return version
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def validate_path_safe(path: str | Path) -> Path:
|
|
153
|
+
"""Validate that a path is safe (no path traversal attacks).
|
|
154
|
+
|
|
155
|
+
.. deprecated:: 0.2
|
|
156
|
+
Use :func:`validate_file_path` instead for more comprehensive validation.
|
|
157
|
+
|
|
158
|
+
Parameters
|
|
159
|
+
----------
|
|
160
|
+
path : str | Path
|
|
161
|
+
The path to validate.
|
|
162
|
+
|
|
163
|
+
Returns
|
|
164
|
+
-------
|
|
165
|
+
Path
|
|
166
|
+
The validated path as a Path object.
|
|
167
|
+
|
|
168
|
+
Raises
|
|
169
|
+
------
|
|
170
|
+
CommandValidationError
|
|
171
|
+
If the path contains unsafe patterns.
|
|
172
|
+
|
|
173
|
+
Examples
|
|
174
|
+
--------
|
|
175
|
+
>>> validate_path_safe("src/tnfr/core.py")
|
|
176
|
+
PosixPath('src/tnfr/core.py')
|
|
177
|
+
>>> validate_path_safe(Path("tests/unit"))
|
|
178
|
+
PosixPath('tests/unit')
|
|
179
|
+
"""
|
|
180
|
+
path_obj = Path(path)
|
|
181
|
+
path_str = str(path_obj)
|
|
182
|
+
|
|
183
|
+
# Check for absolute paths in untrusted input
|
|
184
|
+
if path_obj.is_absolute():
|
|
185
|
+
raise CommandValidationError(
|
|
186
|
+
f"Absolute paths not allowed in user input: {path_str!r}"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Check for path traversal
|
|
190
|
+
if ".." in path_obj.parts:
|
|
191
|
+
raise CommandValidationError(f"Path traversal not allowed: {path_str!r}")
|
|
192
|
+
|
|
193
|
+
# Check for special characters that could be exploited
|
|
194
|
+
if not SAFE_PATH_PATTERN.match(path_str):
|
|
195
|
+
raise CommandValidationError(f"Path contains invalid characters: {path_str!r}")
|
|
196
|
+
|
|
197
|
+
return path_obj
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def validate_file_path(
|
|
201
|
+
path: str | Path,
|
|
202
|
+
*,
|
|
203
|
+
allow_absolute: bool = False,
|
|
204
|
+
allowed_extensions: Sequence[str] | None = None,
|
|
205
|
+
) -> Path:
|
|
206
|
+
"""Validate file path to prevent path traversal and unauthorized access.
|
|
207
|
+
|
|
208
|
+
This function provides comprehensive path validation to prevent:
|
|
209
|
+
- Path traversal attacks (../../../etc/passwd)
|
|
210
|
+
- Unauthorized file access
|
|
211
|
+
- Special character exploits
|
|
212
|
+
- Symlink attacks
|
|
213
|
+
|
|
214
|
+
TNFR Context
|
|
215
|
+
------------
|
|
216
|
+
Maintains structural coherence by ensuring file operations preserve:
|
|
217
|
+
- Configuration integrity (EPI structure preservation)
|
|
218
|
+
- Data export authenticity (coherence metrics validity)
|
|
219
|
+
- Model persistence safety (NFR state protection)
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
path : str | Path
|
|
224
|
+
The file path to validate.
|
|
225
|
+
allow_absolute : bool, default=False
|
|
226
|
+
Whether to allow absolute paths. Default is False for user input.
|
|
227
|
+
allowed_extensions : Sequence[str] | None, default=None
|
|
228
|
+
List of allowed file extensions (e.g., ['.json', '.yaml', '.toml']).
|
|
229
|
+
If None, any extension is allowed.
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
Path
|
|
234
|
+
The validated path as a Path object.
|
|
235
|
+
|
|
236
|
+
Raises
|
|
237
|
+
------
|
|
238
|
+
PathTraversalError
|
|
239
|
+
If path traversal patterns are detected.
|
|
240
|
+
ValueError
|
|
241
|
+
If the path is invalid or contains unsafe patterns.
|
|
242
|
+
|
|
243
|
+
Examples
|
|
244
|
+
--------
|
|
245
|
+
>>> validate_file_path("config.json", allowed_extensions=['.json', '.yaml'])
|
|
246
|
+
PosixPath('config.json')
|
|
247
|
+
|
|
248
|
+
>>> validate_file_path("data/export.csv")
|
|
249
|
+
PosixPath('data/export.csv')
|
|
250
|
+
|
|
251
|
+
>>> validate_file_path("../../../etc/passwd") # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
252
|
+
Traceback (most recent call last):
|
|
253
|
+
...
|
|
254
|
+
PathTraversalError: Path traversal detected
|
|
255
|
+
"""
|
|
256
|
+
if not path:
|
|
257
|
+
raise ValueError("Path cannot be empty")
|
|
258
|
+
|
|
259
|
+
# Convert to Path object
|
|
260
|
+
path_obj = Path(path)
|
|
261
|
+
path_str = str(path)
|
|
262
|
+
path_parts = Path(path).parts
|
|
263
|
+
|
|
264
|
+
# Check for null bytes (common in exploit attempts) - do this before resolve()
|
|
265
|
+
if "\x00" in path_str:
|
|
266
|
+
raise ValueError(f"Null byte detected in path: {path!r}")
|
|
267
|
+
|
|
268
|
+
# Check for path traversal attempts in the original path first
|
|
269
|
+
if ".." in path_parts:
|
|
270
|
+
raise PathTraversalError(
|
|
271
|
+
f"Path traversal detected in {path!r}. "
|
|
272
|
+
"Relative parent directory references (..) are not allowed."
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# Normalize the path to resolve any . or .. components
|
|
276
|
+
try:
|
|
277
|
+
# Use resolve() with strict=False to normalize without checking existence
|
|
278
|
+
normalized = path_obj.resolve()
|
|
279
|
+
except (OSError, RuntimeError, ValueError) as e:
|
|
280
|
+
# Catch embedded null byte errors from resolve()
|
|
281
|
+
error_msg = str(e)
|
|
282
|
+
if "null byte" in error_msg.lower():
|
|
283
|
+
raise ValueError(f"Null byte detected in path: {path!r}") from e
|
|
284
|
+
raise ValueError(f"Invalid path: {path}") from e
|
|
285
|
+
|
|
286
|
+
# Check for absolute paths if not allowed
|
|
287
|
+
if not allow_absolute and normalized.is_absolute():
|
|
288
|
+
# For relative paths, ensure they don't escape to absolute paths
|
|
289
|
+
if not Path(path).is_absolute():
|
|
290
|
+
# This is a relative path that was resolved to absolute
|
|
291
|
+
# We need to check if it contains .. components
|
|
292
|
+
pass
|
|
293
|
+
else:
|
|
294
|
+
raise ValueError(
|
|
295
|
+
f"Absolute paths not allowed: {path}. "
|
|
296
|
+
"Use allow_absolute=True if this is intentional."
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Check for other dangerous patterns
|
|
300
|
+
dangerous_patterns = [
|
|
301
|
+
("~", "Home directory expansion"),
|
|
302
|
+
("\n", "Newline character"),
|
|
303
|
+
("\r", "Carriage return"),
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
for pattern, desc in dangerous_patterns:
|
|
307
|
+
if pattern in path_str:
|
|
308
|
+
raise ValueError(f"{desc} not allowed in path: {path!r}")
|
|
309
|
+
|
|
310
|
+
# Validate file extension if restrictions are specified
|
|
311
|
+
if allowed_extensions is not None:
|
|
312
|
+
suffix = path_obj.suffix.lower()
|
|
313
|
+
allowed_lower = [ext.lower() for ext in allowed_extensions]
|
|
314
|
+
if suffix not in allowed_lower:
|
|
315
|
+
raise ValueError(
|
|
316
|
+
f"File extension {suffix!r} not allowed. "
|
|
317
|
+
f"Allowed extensions: {allowed_extensions}"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
return path_obj
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def resolve_safe_path(
|
|
324
|
+
path: str | Path,
|
|
325
|
+
base_dir: str | Path,
|
|
326
|
+
*,
|
|
327
|
+
must_exist: bool = False,
|
|
328
|
+
allowed_extensions: Sequence[str] | None = None,
|
|
329
|
+
) -> Path:
|
|
330
|
+
"""Resolve a path safely within a base directory.
|
|
331
|
+
|
|
332
|
+
This function ensures that the resolved path stays within the specified
|
|
333
|
+
base directory, preventing path traversal attacks while allowing normal
|
|
334
|
+
subdirectory navigation.
|
|
335
|
+
|
|
336
|
+
TNFR Context
|
|
337
|
+
------------
|
|
338
|
+
Ensures configuration and data files maintain operational fractality by
|
|
339
|
+
restricting file access to designated structural boundaries (base directories).
|
|
340
|
+
|
|
341
|
+
Parameters
|
|
342
|
+
----------
|
|
343
|
+
path : str | Path
|
|
344
|
+
The path to resolve (can be relative or absolute).
|
|
345
|
+
base_dir : str | Path
|
|
346
|
+
The base directory that the path must stay within.
|
|
347
|
+
must_exist : bool, default=False
|
|
348
|
+
If True, raise ValueError if the resolved path doesn't exist.
|
|
349
|
+
allowed_extensions : Sequence[str] | None, default=None
|
|
350
|
+
List of allowed file extensions.
|
|
351
|
+
|
|
352
|
+
Returns
|
|
353
|
+
-------
|
|
354
|
+
Path
|
|
355
|
+
The validated, resolved absolute path.
|
|
356
|
+
|
|
357
|
+
Raises
|
|
358
|
+
------
|
|
359
|
+
PathTraversalError
|
|
360
|
+
If the resolved path escapes the base directory.
|
|
361
|
+
ValueError
|
|
362
|
+
If the path is invalid or doesn't meet requirements.
|
|
363
|
+
|
|
364
|
+
Examples
|
|
365
|
+
--------
|
|
366
|
+
>>> base = Path("/home/user/tnfr")
|
|
367
|
+
>>> resolve_safe_path("config/settings.json", base) # doctest: +SKIP
|
|
368
|
+
PosixPath('/home/user/tnfr/config/settings.json')
|
|
369
|
+
|
|
370
|
+
>>> resolve_safe_path("../../../etc/passwd", base) # doctest: +SKIP +IGNORE_EXCEPTION_DETAIL
|
|
371
|
+
Traceback (most recent call last):
|
|
372
|
+
...
|
|
373
|
+
PathTraversalError: Path escapes base directory
|
|
374
|
+
"""
|
|
375
|
+
if not path:
|
|
376
|
+
raise ValueError("Path cannot be empty")
|
|
377
|
+
if not base_dir:
|
|
378
|
+
raise ValueError("Base directory cannot be empty")
|
|
379
|
+
|
|
380
|
+
# First validate the path itself
|
|
381
|
+
path_obj = validate_file_path(
|
|
382
|
+
path,
|
|
383
|
+
allow_absolute=True,
|
|
384
|
+
allowed_extensions=allowed_extensions,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Resolve base directory to absolute path
|
|
388
|
+
base_path = Path(base_dir).resolve()
|
|
389
|
+
|
|
390
|
+
# Resolve the target path
|
|
391
|
+
# If path is relative, resolve it relative to base_dir
|
|
392
|
+
if not path_obj.is_absolute():
|
|
393
|
+
resolved = (base_path / path_obj).resolve()
|
|
394
|
+
else:
|
|
395
|
+
resolved = path_obj.resolve()
|
|
396
|
+
|
|
397
|
+
# Security check: ensure resolved path is within base directory
|
|
398
|
+
try:
|
|
399
|
+
resolved.relative_to(base_path)
|
|
400
|
+
except ValueError as e:
|
|
401
|
+
raise PathTraversalError(
|
|
402
|
+
f"Path {path!r} escapes base directory {base_dir!r}. "
|
|
403
|
+
f"Resolved path: {resolved}"
|
|
404
|
+
) from e
|
|
405
|
+
|
|
406
|
+
# Check existence if required
|
|
407
|
+
if must_exist and not resolved.exists():
|
|
408
|
+
raise ValueError(f"Path does not exist: {resolved}")
|
|
409
|
+
|
|
410
|
+
return resolved
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def run_command_safely(
|
|
414
|
+
command: Sequence[str],
|
|
415
|
+
*,
|
|
416
|
+
check: bool = True,
|
|
417
|
+
capture_output: bool = True,
|
|
418
|
+
text: bool = True,
|
|
419
|
+
timeout: int | None = None,
|
|
420
|
+
cwd: str | Path | None = None,
|
|
421
|
+
env: dict[str, str] | None = None,
|
|
422
|
+
) -> subprocess.CompletedProcess[Any]:
|
|
423
|
+
"""Execute a command safely with validation.
|
|
424
|
+
|
|
425
|
+
This function provides a secure wrapper around subprocess.run that:
|
|
426
|
+
1. Never uses shell=True
|
|
427
|
+
2. Validates the command is in the allowlist
|
|
428
|
+
3. Ensures all arguments are strings
|
|
429
|
+
4. Provides timeout protection
|
|
430
|
+
|
|
431
|
+
Parameters
|
|
432
|
+
----------
|
|
433
|
+
command : Sequence[str]
|
|
434
|
+
Command and arguments as a list of strings.
|
|
435
|
+
check : bool, optional
|
|
436
|
+
If True, raise CalledProcessError on non-zero exit. Default is True.
|
|
437
|
+
capture_output : bool, optional
|
|
438
|
+
If True, capture stdout and stderr. Default is True.
|
|
439
|
+
text : bool, optional
|
|
440
|
+
If True, decode output as text. Default is True.
|
|
441
|
+
timeout : int | None, optional
|
|
442
|
+
Maximum time in seconds to wait for command completion.
|
|
443
|
+
cwd : str | Path | None, optional
|
|
444
|
+
Working directory for command execution.
|
|
445
|
+
env : dict[str, str] | None, optional
|
|
446
|
+
Environment variables for the subprocess.
|
|
447
|
+
|
|
448
|
+
Returns
|
|
449
|
+
-------
|
|
450
|
+
subprocess.CompletedProcess
|
|
451
|
+
The result of the command execution.
|
|
452
|
+
|
|
453
|
+
Raises
|
|
454
|
+
------
|
|
455
|
+
CommandValidationError
|
|
456
|
+
If the command is not in the allowlist or arguments are invalid.
|
|
457
|
+
subprocess.CalledProcessError
|
|
458
|
+
If check=True and the command returns non-zero exit code.
|
|
459
|
+
subprocess.TimeoutExpired
|
|
460
|
+
If timeout is exceeded.
|
|
461
|
+
|
|
462
|
+
Examples
|
|
463
|
+
--------
|
|
464
|
+
>>> result = run_command_safely(["git", "status"])
|
|
465
|
+
>>> result.returncode
|
|
466
|
+
0
|
|
467
|
+
>>> result = run_command_safely(["git", "log", "-1", "--oneline"])
|
|
468
|
+
"""
|
|
469
|
+
if not command:
|
|
470
|
+
raise CommandValidationError("Command cannot be empty")
|
|
471
|
+
|
|
472
|
+
# Validate all arguments are strings
|
|
473
|
+
if not all(isinstance(arg, str) for arg in command):
|
|
474
|
+
raise CommandValidationError(
|
|
475
|
+
"All command arguments must be strings. "
|
|
476
|
+
f"Got: {[type(arg).__name__ for arg in command]}"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# Extract base command (handle paths like /usr/bin/python)
|
|
480
|
+
base_cmd = Path(command[0]).name
|
|
481
|
+
|
|
482
|
+
# Validate command is in allowlist
|
|
483
|
+
if base_cmd not in ALLOWED_COMMANDS:
|
|
484
|
+
raise CommandValidationError(
|
|
485
|
+
f"Command not in allowlist: {base_cmd!r}. "
|
|
486
|
+
f"Allowed commands: {sorted(ALLOWED_COMMANDS)}"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# Validate cwd if provided
|
|
490
|
+
if cwd is not None:
|
|
491
|
+
cwd = str(cwd)
|
|
492
|
+
|
|
493
|
+
# Execute with shell=False (explicit for clarity)
|
|
494
|
+
return subprocess.run(
|
|
495
|
+
list(command),
|
|
496
|
+
check=check,
|
|
497
|
+
capture_output=capture_output,
|
|
498
|
+
text=text,
|
|
499
|
+
timeout=timeout,
|
|
500
|
+
cwd=cwd,
|
|
501
|
+
env=env,
|
|
502
|
+
shell=False, # CRITICAL: Never use shell=True
|
|
503
|
+
)
|