tnfr 4.5.2__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 +334 -50
- 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 +214 -37
- 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 +149 -556
- tnfr/cache.pyi +13 -0
- tnfr/cli/__init__.py +51 -16
- tnfr/cli/__init__.pyi +26 -0
- tnfr/cli/arguments.py +344 -32
- tnfr/cli/arguments.pyi +29 -0
- tnfr/cli/execution.py +676 -50
- tnfr/cli/execution.pyi +70 -0
- tnfr/cli/interactive_validator.py +614 -0
- tnfr/cli/utils.py +18 -3
- 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/{constants_glyphs.py → config/constants.py} +26 -20
- tnfr/config/constants.pyi +12 -0
- tnfr/config/defaults.py +54 -0
- tnfr/{constants/core.py → config/defaults_core.py} +59 -6
- 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 +51 -133
- tnfr/constants/__init__.pyi +92 -0
- tnfr/constants/aliases.py +33 -0
- tnfr/constants/aliases.pyi +27 -0
- tnfr/constants/init.py +3 -1
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +9 -15
- 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 +213 -633
- 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 +2699 -398
- 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 +496 -102
- 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 +10 -5
- 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 +77 -55
- 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 +29 -50
- tnfr/flatten.pyi +21 -0
- tnfr/gamma.py +66 -53
- tnfr/gamma.pyi +36 -0
- tnfr/glyph_history.py +144 -57
- tnfr/glyph_history.pyi +35 -0
- tnfr/glyph_runtime.py +19 -0
- tnfr/glyph_runtime.pyi +8 -0
- tnfr/immutable.py +70 -30
- tnfr/immutable.pyi +36 -0
- tnfr/initialization.py +22 -16
- tnfr/initialization.pyi +65 -0
- tnfr/io.py +5 -241
- tnfr/io.pyi +13 -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 +47 -9
- 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 +1510 -330
- tnfr/metrics/coherence.pyi +129 -0
- tnfr/metrics/common.py +23 -16
- tnfr/metrics/common.pyi +35 -0
- tnfr/metrics/core.py +251 -36
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +709 -110
- tnfr/metrics/diagnosis.pyi +86 -0
- tnfr/metrics/emergence.py +245 -0
- tnfr/metrics/export.py +60 -18
- tnfr/metrics/export.pyi +7 -0
- tnfr/metrics/glyph_timing.py +233 -43
- 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 +63 -28
- tnfr/metrics/reporting.pyi +25 -0
- tnfr/metrics/sense_index.py +1126 -43
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +215 -23
- tnfr/metrics/trig.pyi +13 -0
- tnfr/metrics/trig_cache.py +148 -24
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/multiscale/__init__.py +32 -0
- tnfr/multiscale/hierarchical.py +517 -0
- tnfr/node.py +646 -140
- tnfr/node.pyi +139 -0
- tnfr/observers.py +160 -45
- tnfr/observers.pyi +31 -0
- tnfr/ontosim.py +23 -19
- tnfr/ontosim.pyi +28 -0
- tnfr/operators/__init__.py +1358 -106
- 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 +107 -38
- 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 +1415 -91
- 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 +75 -151
- 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 +59 -22
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +92 -67
- 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 +639 -263
- 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 +2 -4
- 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 +300 -126
- 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 +743 -12
- 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/{callback_utils.py → utils/callbacks.py} +26 -39
- 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/{helpers → utils}/numeric.py +51 -24
- 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-4.5.2.dist-info → tnfr-8.5.0.dist-info}/entry_points.txt +1 -0
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/licenses/LICENSE.md +1 -1
- tnfr/collections_utils.py +0 -300
- tnfr/config.py +0 -32
- tnfr/grammar.py +0 -344
- tnfr/graph_utils.py +0 -84
- tnfr/helpers/__init__.py +0 -71
- tnfr/import_utils.py +0 -228
- tnfr/json_utils.py +0 -162
- tnfr/logging_utils.py +0 -116
- tnfr/presets.py +0 -60
- tnfr/validators.py +0 -84
- tnfr/value_utils.py +0 -59
- tnfr-4.5.2.dist-info/METADATA +0 -379
- tnfr-4.5.2.dist-info/RECORD +0 -67
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
tnfr/config/security.py
ADDED
|
@@ -0,0 +1,927 @@
|
|
|
1
|
+
"""Secure configuration management for the TNFR engine.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for loading configuration from environment
|
|
4
|
+
variables with validation and secure defaults. It ensures that sensitive
|
|
5
|
+
credentials are never hardcoded in source code.
|
|
6
|
+
|
|
7
|
+
Security Principles:
|
|
8
|
+
- Never hardcode secrets, API keys, or passwords
|
|
9
|
+
- Load sensitive values from environment variables
|
|
10
|
+
- Provide secure defaults for development
|
|
11
|
+
- Validate configuration before use
|
|
12
|
+
- Support multiple configuration sources (environment, .env files)
|
|
13
|
+
- Sanitize credentials in logs to prevent exposure
|
|
14
|
+
- Secure memory management for secrets
|
|
15
|
+
- Credential rotation and TTL support
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import secrets
|
|
23
|
+
import time
|
|
24
|
+
import warnings
|
|
25
|
+
from datetime import datetime, timedelta, timezone
|
|
26
|
+
from typing import Any, Callable, Optional
|
|
27
|
+
from urllib.parse import ParseResult, urlparse, urlunparse
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConfigurationError(Exception):
|
|
31
|
+
"""Raised when configuration is invalid or missing required values."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SecurityAuditWarning(UserWarning):
|
|
35
|
+
"""Warning for security audit findings that don't stop execution."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_env_variable(
|
|
39
|
+
name: str,
|
|
40
|
+
default: Optional[str] = None,
|
|
41
|
+
required: bool = False,
|
|
42
|
+
secret: bool = False,
|
|
43
|
+
) -> str | None:
|
|
44
|
+
"""Get an environment variable with validation.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
name : str
|
|
49
|
+
The name of the environment variable to retrieve.
|
|
50
|
+
default : str, optional
|
|
51
|
+
Default value if the environment variable is not set.
|
|
52
|
+
required : bool, default=False
|
|
53
|
+
If True, raise ConfigurationError if the variable is not set.
|
|
54
|
+
secret : bool, default=False
|
|
55
|
+
If True, this is a sensitive value (password, token, etc.).
|
|
56
|
+
Warnings will be issued if using defaults for secrets.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
str or None
|
|
61
|
+
The value of the environment variable, or the default value.
|
|
62
|
+
|
|
63
|
+
Raises
|
|
64
|
+
------
|
|
65
|
+
ConfigurationError
|
|
66
|
+
If required=True and the variable is not set.
|
|
67
|
+
|
|
68
|
+
Examples
|
|
69
|
+
--------
|
|
70
|
+
>>> # Get optional configuration with default
|
|
71
|
+
>>> log_level = get_env_variable("TNFR_LOG_LEVEL", default="INFO")
|
|
72
|
+
|
|
73
|
+
>>> # Get required secret (will raise if not set)
|
|
74
|
+
>>> api_token = get_env_variable(
|
|
75
|
+
... "GITHUB_TOKEN",
|
|
76
|
+
... required=True,
|
|
77
|
+
... secret=True
|
|
78
|
+
... )
|
|
79
|
+
|
|
80
|
+
>>> # Get optional secret (will warn if using default)
|
|
81
|
+
>>> redis_password = get_env_variable(
|
|
82
|
+
... "REDIS_PASSWORD",
|
|
83
|
+
... default="",
|
|
84
|
+
... secret=True
|
|
85
|
+
... )
|
|
86
|
+
"""
|
|
87
|
+
value = os.environ.get(name)
|
|
88
|
+
|
|
89
|
+
if value is None:
|
|
90
|
+
if required:
|
|
91
|
+
raise ConfigurationError(
|
|
92
|
+
f"Required environment variable '{name}' is not set. "
|
|
93
|
+
f"Please set it in your environment or .env file."
|
|
94
|
+
)
|
|
95
|
+
if secret and default is not None:
|
|
96
|
+
warnings.warn(
|
|
97
|
+
f"Using default value for secret '{name}'. "
|
|
98
|
+
f"Set the environment variable for production use.",
|
|
99
|
+
stacklevel=2,
|
|
100
|
+
)
|
|
101
|
+
return default
|
|
102
|
+
|
|
103
|
+
return value
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def load_pypi_credentials() -> dict[str, str | None]:
|
|
107
|
+
"""Load PyPI publishing credentials from environment.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
dict
|
|
112
|
+
Dictionary containing username, password, and repository settings.
|
|
113
|
+
|
|
114
|
+
Notes
|
|
115
|
+
-----
|
|
116
|
+
This function reads from multiple environment variables to support
|
|
117
|
+
different tools (twine, poetry, etc.):
|
|
118
|
+
|
|
119
|
+
- PYPI_USERNAME or TWINE_USERNAME
|
|
120
|
+
- PYPI_PASSWORD, PYPI_API_TOKEN, or TWINE_PASSWORD
|
|
121
|
+
- PYPI_REPOSITORY (defaults to 'pypi')
|
|
122
|
+
|
|
123
|
+
Best Practice
|
|
124
|
+
-------------
|
|
125
|
+
Use API tokens instead of passwords:
|
|
126
|
+
- PYPI_USERNAME=__token__
|
|
127
|
+
- PYPI_PASSWORD=pypi-XXXXXXXXXXXXXXXXXXXX...
|
|
128
|
+
|
|
129
|
+
Note: Example uses 'XXX' pattern to avoid triggering security scanners.
|
|
130
|
+
Actual PyPI tokens follow format: pypi-AgEIcHlwaS5vcmcC...
|
|
131
|
+
|
|
132
|
+
See Also
|
|
133
|
+
--------
|
|
134
|
+
https://pypi.org/help/#apitoken : PyPI API token documentation
|
|
135
|
+
"""
|
|
136
|
+
username = os.environ.get("PYPI_USERNAME") or os.environ.get("TWINE_USERNAME")
|
|
137
|
+
password = (
|
|
138
|
+
os.environ.get("PYPI_PASSWORD")
|
|
139
|
+
or os.environ.get("PYPI_API_TOKEN")
|
|
140
|
+
or os.environ.get("TWINE_PASSWORD")
|
|
141
|
+
)
|
|
142
|
+
repository = os.environ.get("PYPI_REPOSITORY", "pypi")
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
"username": username,
|
|
146
|
+
"password": password,
|
|
147
|
+
"repository": repository,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def load_github_credentials() -> dict[str, str | None]:
|
|
152
|
+
"""Load GitHub API credentials from environment.
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
dict
|
|
157
|
+
Dictionary containing token and repository information.
|
|
158
|
+
|
|
159
|
+
Notes
|
|
160
|
+
-----
|
|
161
|
+
This function reads GITHUB_TOKEN and GITHUB_REPOSITORY environment
|
|
162
|
+
variables commonly set in GitHub Actions and other CI environments.
|
|
163
|
+
|
|
164
|
+
Best Practice
|
|
165
|
+
-------------
|
|
166
|
+
Use fine-grained personal access tokens with minimal scopes:
|
|
167
|
+
- For security scans: read:security_events
|
|
168
|
+
- For releases: contents:write, packages:write
|
|
169
|
+
|
|
170
|
+
See Also
|
|
171
|
+
--------
|
|
172
|
+
https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token
|
|
173
|
+
"""
|
|
174
|
+
token = os.environ.get("GITHUB_TOKEN")
|
|
175
|
+
repository = os.environ.get("GITHUB_REPOSITORY")
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
"token": token,
|
|
179
|
+
"repository": repository,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def load_redis_config(validate_url: bool = True) -> dict[str, Any]:
|
|
184
|
+
"""Load Redis connection configuration from environment.
|
|
185
|
+
|
|
186
|
+
Parameters
|
|
187
|
+
----------
|
|
188
|
+
validate_url : bool, default=True
|
|
189
|
+
Whether to validate the constructed Redis URL.
|
|
190
|
+
|
|
191
|
+
Returns
|
|
192
|
+
-------
|
|
193
|
+
dict
|
|
194
|
+
Dictionary containing Redis connection parameters.
|
|
195
|
+
|
|
196
|
+
Notes
|
|
197
|
+
-----
|
|
198
|
+
Supports standard Redis configuration variables:
|
|
199
|
+
|
|
200
|
+
- REDIS_HOST (default: 'localhost')
|
|
201
|
+
- REDIS_PORT (default: 6379)
|
|
202
|
+
- REDIS_PASSWORD (optional)
|
|
203
|
+
- REDIS_DB (default: 0)
|
|
204
|
+
- REDIS_USE_TLS (default: False)
|
|
205
|
+
- REDIS_URL (alternative: full URL, overrides individual params)
|
|
206
|
+
|
|
207
|
+
Security
|
|
208
|
+
--------
|
|
209
|
+
Always use authentication (REDIS_PASSWORD) in production.
|
|
210
|
+
Enable TLS (REDIS_USE_TLS=true) for network connections.
|
|
211
|
+
URLs with credentials are validated and sanitized for logging.
|
|
212
|
+
|
|
213
|
+
See Also
|
|
214
|
+
--------
|
|
215
|
+
tnfr.utils.RedisCacheLayer : Redis cache implementation
|
|
216
|
+
SecureCredentialValidator : URL validation and sanitization
|
|
217
|
+
"""
|
|
218
|
+
# Check if full URL is provided
|
|
219
|
+
redis_url = get_env_variable("REDIS_URL", default=None)
|
|
220
|
+
|
|
221
|
+
if redis_url:
|
|
222
|
+
# Validate URL if requested
|
|
223
|
+
if validate_url:
|
|
224
|
+
SecureCredentialValidator.validate_redis_url(redis_url)
|
|
225
|
+
|
|
226
|
+
# Parse URL to extract components
|
|
227
|
+
parsed = urlparse(redis_url)
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
"host": parsed.hostname or "localhost",
|
|
231
|
+
"port": parsed.port or 6379,
|
|
232
|
+
"password": parsed.password,
|
|
233
|
+
"db": int(parsed.path.lstrip("/") or "0") if parsed.path else 0,
|
|
234
|
+
"ssl": parsed.scheme == "rediss",
|
|
235
|
+
"url": redis_url,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# Load from individual variables
|
|
239
|
+
host = get_env_variable("REDIS_HOST", default="localhost")
|
|
240
|
+
port_str = get_env_variable("REDIS_PORT", default="6379")
|
|
241
|
+
password = get_env_variable("REDIS_PASSWORD", default=None, secret=True)
|
|
242
|
+
db_str = get_env_variable("REDIS_DB", default="0")
|
|
243
|
+
use_tls_str = get_env_variable("REDIS_USE_TLS", default="false")
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
port = int(port_str)
|
|
247
|
+
except ValueError:
|
|
248
|
+
raise ConfigurationError(f"REDIS_PORT must be an integer, got: {port_str}")
|
|
249
|
+
|
|
250
|
+
# Validate port range
|
|
251
|
+
if not (1 <= port <= 65535):
|
|
252
|
+
raise ConfigurationError(f"REDIS_PORT must be between 1 and 65535, got: {port}")
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
db = int(db_str)
|
|
256
|
+
except ValueError:
|
|
257
|
+
raise ConfigurationError(f"REDIS_DB must be an integer, got: {db_str}")
|
|
258
|
+
|
|
259
|
+
use_tls = use_tls_str.lower() in ("true", "1", "yes", "on")
|
|
260
|
+
|
|
261
|
+
# Construct URL for validation
|
|
262
|
+
if validate_url:
|
|
263
|
+
scheme = "rediss" if use_tls else "redis"
|
|
264
|
+
if password:
|
|
265
|
+
url = f"{scheme}://:{password}@{host}:{port}/{db}"
|
|
266
|
+
else:
|
|
267
|
+
url = f"{scheme}://{host}:{port}/{db}"
|
|
268
|
+
SecureCredentialValidator.validate_redis_url(url)
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
"host": host,
|
|
272
|
+
"port": port,
|
|
273
|
+
"password": password,
|
|
274
|
+
"db": db,
|
|
275
|
+
"ssl": use_tls,
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def get_cache_secret() -> bytes | None:
|
|
280
|
+
"""Get the cache signing secret from environment.
|
|
281
|
+
|
|
282
|
+
Returns
|
|
283
|
+
-------
|
|
284
|
+
bytes or None
|
|
285
|
+
The cache secret as bytes, or None if not configured.
|
|
286
|
+
|
|
287
|
+
Notes
|
|
288
|
+
-----
|
|
289
|
+
Reads from TNFR_CACHE_SECRET environment variable. The secret should
|
|
290
|
+
be a hex-encoded string (recommended length: 64 characters / 32 bytes).
|
|
291
|
+
|
|
292
|
+
Security
|
|
293
|
+
--------
|
|
294
|
+
Use a cryptographically strong random secret:
|
|
295
|
+
|
|
296
|
+
>>> import secrets
|
|
297
|
+
>>> secret = secrets.token_hex(32) # 64-character hex string
|
|
298
|
+
>>> # Set TNFR_CACHE_SECRET=<secret> in your environment
|
|
299
|
+
|
|
300
|
+
See Also
|
|
301
|
+
--------
|
|
302
|
+
tnfr.utils.ShelveCacheLayer : Shelf cache with signature support
|
|
303
|
+
tnfr.utils.RedisCacheLayer : Redis cache with signature support
|
|
304
|
+
"""
|
|
305
|
+
secret_hex = get_env_variable("TNFR_CACHE_SECRET", secret=True)
|
|
306
|
+
if secret_hex is None:
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
return bytes.fromhex(secret_hex)
|
|
311
|
+
except ValueError as exc:
|
|
312
|
+
raise ConfigurationError(
|
|
313
|
+
f"TNFR_CACHE_SECRET must be a hex-encoded string: {exc}"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def validate_no_hardcoded_secrets(value: str) -> bool:
|
|
318
|
+
"""Validate that a string doesn't look like a hardcoded secret.
|
|
319
|
+
|
|
320
|
+
Parameters
|
|
321
|
+
----------
|
|
322
|
+
value : str
|
|
323
|
+
The string to validate.
|
|
324
|
+
|
|
325
|
+
Returns
|
|
326
|
+
-------
|
|
327
|
+
bool
|
|
328
|
+
True if the value passes validation.
|
|
329
|
+
|
|
330
|
+
Raises
|
|
331
|
+
------
|
|
332
|
+
ValueError
|
|
333
|
+
If the value appears to be a hardcoded secret.
|
|
334
|
+
|
|
335
|
+
Notes
|
|
336
|
+
-----
|
|
337
|
+
This is a heuristic check for common secret patterns:
|
|
338
|
+
|
|
339
|
+
- Long alphanumeric strings (potential tokens)
|
|
340
|
+
- Known secret prefixes (ghp_, pypi-, sk-, etc.)
|
|
341
|
+
- Base64-encoded strings
|
|
342
|
+
|
|
343
|
+
For production environments, consider using more sophisticated
|
|
344
|
+
tools like `detect-secrets` which employ entropy analysis for
|
|
345
|
+
better accuracy.
|
|
346
|
+
|
|
347
|
+
Examples
|
|
348
|
+
--------
|
|
349
|
+
>>> validate_no_hardcoded_secrets("my-password")
|
|
350
|
+
True
|
|
351
|
+
|
|
352
|
+
>>> validate_no_hardcoded_secrets("ghp_abcd1234...")
|
|
353
|
+
Traceback (most recent call last):
|
|
354
|
+
...
|
|
355
|
+
ValueError: Value appears to be a hardcoded GitHub token
|
|
356
|
+
"""
|
|
357
|
+
# Check for known secret prefixes
|
|
358
|
+
secret_prefixes = [
|
|
359
|
+
("ghp_", "GitHub token"),
|
|
360
|
+
("gho_", "GitHub OAuth token"),
|
|
361
|
+
("ghu_", "GitHub user token"),
|
|
362
|
+
("ghs_", "GitHub server token"),
|
|
363
|
+
("ghr_", "GitHub refresh token"),
|
|
364
|
+
("pypi-", "PyPI token"),
|
|
365
|
+
("sk-", "OpenAI API key"),
|
|
366
|
+
("xoxb-", "Slack bot token"),
|
|
367
|
+
("xoxp-", "Slack user token"),
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
for prefix, name in secret_prefixes:
|
|
371
|
+
if value.startswith(prefix):
|
|
372
|
+
raise ValueError(f"Value appears to be a hardcoded {name}")
|
|
373
|
+
|
|
374
|
+
# Check for suspiciously long alphanumeric strings
|
|
375
|
+
# Note: This is a simple heuristic. For production use, consider
|
|
376
|
+
# entropy-based analysis (e.g., using detect-secrets library)
|
|
377
|
+
if len(value) > 32 and value.replace("-", "").replace("_", "").isalnum():
|
|
378
|
+
# Allow environment variable names (typically uppercase)
|
|
379
|
+
if not value.isupper():
|
|
380
|
+
warnings.warn(
|
|
381
|
+
f"Value looks like it might be a hardcoded secret: {value[:10]}...",
|
|
382
|
+
stacklevel=2,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
return True
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class SecureCredentialValidator:
|
|
389
|
+
"""Robust credential and configuration validator.
|
|
390
|
+
|
|
391
|
+
Validates credentials and configuration with strict security criteria
|
|
392
|
+
following TNFR principles of structural coherence and stability.
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
ALLOWED_SCHEMES = frozenset(["redis", "rediss"]) # Only secure schemes
|
|
396
|
+
MAX_URL_LENGTH = 512 # Prevent DoS attacks
|
|
397
|
+
MIN_SECRET_LENGTH = 8 # Minimum secret strength
|
|
398
|
+
|
|
399
|
+
@staticmethod
|
|
400
|
+
def validate_redis_url(url: str) -> bool:
|
|
401
|
+
"""Validate Redis URL with strict security criteria.
|
|
402
|
+
|
|
403
|
+
Parameters
|
|
404
|
+
----------
|
|
405
|
+
url : str
|
|
406
|
+
Redis URL to validate.
|
|
407
|
+
|
|
408
|
+
Returns
|
|
409
|
+
-------
|
|
410
|
+
bool
|
|
411
|
+
True if URL is valid.
|
|
412
|
+
|
|
413
|
+
Raises
|
|
414
|
+
------
|
|
415
|
+
ValueError
|
|
416
|
+
If URL fails validation checks.
|
|
417
|
+
|
|
418
|
+
Examples
|
|
419
|
+
--------
|
|
420
|
+
>>> SecureCredentialValidator.validate_redis_url("redis://localhost:6379/0")
|
|
421
|
+
True
|
|
422
|
+
|
|
423
|
+
>>> SecureCredentialValidator.validate_redis_url("http://evil.com")
|
|
424
|
+
Traceback (most recent call last):
|
|
425
|
+
...
|
|
426
|
+
ValueError: Unsupported scheme: http
|
|
427
|
+
"""
|
|
428
|
+
if not url or not isinstance(url, str):
|
|
429
|
+
raise ValueError("Redis URL must be a non-empty string")
|
|
430
|
+
|
|
431
|
+
if len(url) > SecureCredentialValidator.MAX_URL_LENGTH:
|
|
432
|
+
raise ValueError(
|
|
433
|
+
f"Redis URL exceeds maximum length of {SecureCredentialValidator.MAX_URL_LENGTH}"
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
parsed = urlparse(url)
|
|
438
|
+
except Exception as exc:
|
|
439
|
+
raise ValueError(f"Invalid URL format: {exc}")
|
|
440
|
+
|
|
441
|
+
if parsed.scheme not in SecureCredentialValidator.ALLOWED_SCHEMES:
|
|
442
|
+
raise ValueError(
|
|
443
|
+
f"Unsupported scheme: {parsed.scheme}. "
|
|
444
|
+
f"Allowed: {', '.join(SecureCredentialValidator.ALLOWED_SCHEMES)}"
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
if not parsed.hostname:
|
|
448
|
+
raise ValueError("Redis URL must include a hostname")
|
|
449
|
+
|
|
450
|
+
# Validate port if specified
|
|
451
|
+
if parsed.port is not None:
|
|
452
|
+
if not (1 <= parsed.port <= 65535):
|
|
453
|
+
raise ValueError(f"Invalid port number: {parsed.port}")
|
|
454
|
+
|
|
455
|
+
return True
|
|
456
|
+
|
|
457
|
+
@staticmethod
|
|
458
|
+
def sanitize_for_logging(url: str) -> str:
|
|
459
|
+
"""Sanitize URL for safe logging (hide credentials).
|
|
460
|
+
|
|
461
|
+
Parameters
|
|
462
|
+
----------
|
|
463
|
+
url : str
|
|
464
|
+
URL that may contain credentials.
|
|
465
|
+
|
|
466
|
+
Returns
|
|
467
|
+
-------
|
|
468
|
+
str
|
|
469
|
+
Sanitized URL with credentials masked.
|
|
470
|
+
|
|
471
|
+
Examples
|
|
472
|
+
--------
|
|
473
|
+
>>> SecureCredentialValidator.sanitize_for_logging(
|
|
474
|
+
... "redis://user:secret@host:6379/0"
|
|
475
|
+
... )
|
|
476
|
+
'redis://user:***@host:6379/0'
|
|
477
|
+
"""
|
|
478
|
+
if not url:
|
|
479
|
+
return url
|
|
480
|
+
|
|
481
|
+
try:
|
|
482
|
+
parsed = urlparse(url)
|
|
483
|
+
|
|
484
|
+
# Check if parsing actually succeeded
|
|
485
|
+
if not parsed.scheme and not parsed.netloc:
|
|
486
|
+
# This is not a valid URL
|
|
487
|
+
return "<invalid-url>"
|
|
488
|
+
except Exception:
|
|
489
|
+
# If parsing fails, return a safe placeholder
|
|
490
|
+
return "<invalid-url>"
|
|
491
|
+
|
|
492
|
+
# Mask password if present
|
|
493
|
+
if parsed.password:
|
|
494
|
+
# Replace password with ***
|
|
495
|
+
netloc = parsed.netloc
|
|
496
|
+
if "@" in netloc:
|
|
497
|
+
userinfo, hostinfo = netloc.rsplit("@", 1)
|
|
498
|
+
if ":" in userinfo:
|
|
499
|
+
username, _ = userinfo.split(":", 1)
|
|
500
|
+
netloc = f"{username}:***@{hostinfo}"
|
|
501
|
+
else:
|
|
502
|
+
netloc = f"***@{hostinfo}"
|
|
503
|
+
|
|
504
|
+
sanitized = parsed._replace(netloc=netloc)
|
|
505
|
+
return urlunparse(sanitized)
|
|
506
|
+
|
|
507
|
+
return url
|
|
508
|
+
|
|
509
|
+
@staticmethod
|
|
510
|
+
def validate_secret_strength(secret: str | bytes, min_length: int = 8) -> bool:
|
|
511
|
+
"""Validate that a secret meets minimum strength requirements.
|
|
512
|
+
|
|
513
|
+
Parameters
|
|
514
|
+
----------
|
|
515
|
+
secret : str or bytes
|
|
516
|
+
The secret to validate.
|
|
517
|
+
min_length : int, default=8
|
|
518
|
+
Minimum required length.
|
|
519
|
+
|
|
520
|
+
Returns
|
|
521
|
+
-------
|
|
522
|
+
bool
|
|
523
|
+
True if secret is strong enough.
|
|
524
|
+
|
|
525
|
+
Raises
|
|
526
|
+
------
|
|
527
|
+
ValueError
|
|
528
|
+
If secret is too weak.
|
|
529
|
+
"""
|
|
530
|
+
if isinstance(secret, bytes):
|
|
531
|
+
length = len(secret)
|
|
532
|
+
secret_str = secret.decode("utf-8", errors="ignore")
|
|
533
|
+
else:
|
|
534
|
+
length = len(secret)
|
|
535
|
+
secret_str = secret
|
|
536
|
+
|
|
537
|
+
# Check for common weak passwords first (before length check)
|
|
538
|
+
# This provides more specific error messages
|
|
539
|
+
weak_passwords = ["password", "123456", "admin", "secret", "test", "changeme"]
|
|
540
|
+
if secret_str.lower() in weak_passwords:
|
|
541
|
+
raise ValueError("Secret matches a known weak password")
|
|
542
|
+
|
|
543
|
+
# Then check length
|
|
544
|
+
if length < min_length:
|
|
545
|
+
raise ValueError(f"Secret too short: {length} < {min_length} (minimum)")
|
|
546
|
+
|
|
547
|
+
return True
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
class SecureSecretManager:
|
|
551
|
+
"""Secure secret management with automatic memory cleanup.
|
|
552
|
+
|
|
553
|
+
Manages secrets in memory with secure cleanup to prevent exposure
|
|
554
|
+
through memory dumps. Implements structural coherence principles
|
|
555
|
+
by ensuring secrets maintain integrity throughout their lifecycle.
|
|
556
|
+
"""
|
|
557
|
+
|
|
558
|
+
def __init__(self) -> None:
|
|
559
|
+
"""Initialize secure secret manager."""
|
|
560
|
+
self._secrets: dict[str, bytearray] = {}
|
|
561
|
+
self._access_log: list[tuple[str, float]] = []
|
|
562
|
+
|
|
563
|
+
def store_secret(self, key: str, secret: bytes | str) -> None:
|
|
564
|
+
"""Store a secret securely.
|
|
565
|
+
|
|
566
|
+
Parameters
|
|
567
|
+
----------
|
|
568
|
+
key : str
|
|
569
|
+
Identifier for the secret.
|
|
570
|
+
secret : bytes or str
|
|
571
|
+
The secret value to store.
|
|
572
|
+
"""
|
|
573
|
+
if isinstance(secret, str):
|
|
574
|
+
secret_bytes = secret.encode("utf-8")
|
|
575
|
+
else:
|
|
576
|
+
secret_bytes = secret
|
|
577
|
+
|
|
578
|
+
# Store as mutable bytearray for secure clearing
|
|
579
|
+
self._secrets[key] = bytearray(secret_bytes)
|
|
580
|
+
|
|
581
|
+
def get_secret(self, key: str) -> bytes:
|
|
582
|
+
"""Get a secret with access tracking.
|
|
583
|
+
|
|
584
|
+
Parameters
|
|
585
|
+
----------
|
|
586
|
+
key : str
|
|
587
|
+
Secret identifier.
|
|
588
|
+
|
|
589
|
+
Returns
|
|
590
|
+
-------
|
|
591
|
+
bytes
|
|
592
|
+
Copy of the secret (not direct reference).
|
|
593
|
+
"""
|
|
594
|
+
self._access_log.append((key, time.time()))
|
|
595
|
+
secret_array = self._secrets.get(key)
|
|
596
|
+
if secret_array is None:
|
|
597
|
+
return b""
|
|
598
|
+
# Return copy to prevent external mutation
|
|
599
|
+
return bytes(secret_array)
|
|
600
|
+
|
|
601
|
+
def clear_secret(self, key: str) -> None:
|
|
602
|
+
"""Clear a secret from memory securely.
|
|
603
|
+
|
|
604
|
+
Parameters
|
|
605
|
+
----------
|
|
606
|
+
key : str
|
|
607
|
+
Secret identifier to clear.
|
|
608
|
+
"""
|
|
609
|
+
if key in self._secrets:
|
|
610
|
+
# Overwrite with random bytes before deletion
|
|
611
|
+
secret_array = self._secrets[key]
|
|
612
|
+
for i in range(len(secret_array)):
|
|
613
|
+
secret_array[i] = secrets.randbits(8) & 0xFF
|
|
614
|
+
del self._secrets[key]
|
|
615
|
+
|
|
616
|
+
def clear_all(self) -> None:
|
|
617
|
+
"""Clear all secrets from memory."""
|
|
618
|
+
for key in list(self._secrets.keys()):
|
|
619
|
+
self.clear_secret(key)
|
|
620
|
+
|
|
621
|
+
def get_access_log(self) -> list[tuple[str, float]]:
|
|
622
|
+
"""Get access log for auditing.
|
|
623
|
+
|
|
624
|
+
Returns
|
|
625
|
+
-------
|
|
626
|
+
list of tuples
|
|
627
|
+
List of (key, timestamp) tuples.
|
|
628
|
+
"""
|
|
629
|
+
return self._access_log.copy()
|
|
630
|
+
|
|
631
|
+
def __del__(self) -> None:
|
|
632
|
+
"""Cleanup on destruction."""
|
|
633
|
+
self.clear_all()
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
class CredentialRotationManager:
|
|
637
|
+
"""Manages credential rotation with TTL support.
|
|
638
|
+
|
|
639
|
+
Implements structural reorganization principle by managing
|
|
640
|
+
credential lifecycle and triggering rotation when coherence
|
|
641
|
+
(validity period) decreases.
|
|
642
|
+
"""
|
|
643
|
+
|
|
644
|
+
def __init__(
|
|
645
|
+
self,
|
|
646
|
+
rotation_interval: timedelta = timedelta(hours=24),
|
|
647
|
+
warning_threshold: timedelta = timedelta(hours=2),
|
|
648
|
+
) -> None:
|
|
649
|
+
"""Initialize rotation manager.
|
|
650
|
+
|
|
651
|
+
Parameters
|
|
652
|
+
----------
|
|
653
|
+
rotation_interval : timedelta, default=24 hours
|
|
654
|
+
How often credentials should be rotated.
|
|
655
|
+
warning_threshold : timedelta, default=2 hours
|
|
656
|
+
When to warn about upcoming expiration.
|
|
657
|
+
"""
|
|
658
|
+
self.rotation_interval = rotation_interval
|
|
659
|
+
self.warning_threshold = warning_threshold
|
|
660
|
+
self._last_rotation: dict[str, datetime] = {}
|
|
661
|
+
self._rotation_callbacks: dict[str, Callable[[], None]] = {}
|
|
662
|
+
|
|
663
|
+
def register_credential(
|
|
664
|
+
self,
|
|
665
|
+
credential_key: str,
|
|
666
|
+
rotation_callback: Optional[Callable[[], None]] = None,
|
|
667
|
+
) -> None:
|
|
668
|
+
"""Register a credential for rotation tracking.
|
|
669
|
+
|
|
670
|
+
Parameters
|
|
671
|
+
----------
|
|
672
|
+
credential_key : str
|
|
673
|
+
Identifier for the credential.
|
|
674
|
+
rotation_callback : callable, optional
|
|
675
|
+
Function to call when rotation is needed.
|
|
676
|
+
"""
|
|
677
|
+
self._last_rotation[credential_key] = datetime.now(timezone.utc)
|
|
678
|
+
if rotation_callback is not None:
|
|
679
|
+
self._rotation_callbacks[credential_key] = rotation_callback
|
|
680
|
+
|
|
681
|
+
def needs_rotation(self, credential_key: str) -> bool:
|
|
682
|
+
"""Check if credential needs rotation.
|
|
683
|
+
|
|
684
|
+
Parameters
|
|
685
|
+
----------
|
|
686
|
+
credential_key : str
|
|
687
|
+
Credential identifier.
|
|
688
|
+
|
|
689
|
+
Returns
|
|
690
|
+
-------
|
|
691
|
+
bool
|
|
692
|
+
True if rotation is needed.
|
|
693
|
+
"""
|
|
694
|
+
last = self._last_rotation.get(credential_key)
|
|
695
|
+
if last is None:
|
|
696
|
+
return True
|
|
697
|
+
age = datetime.now(timezone.utc) - last
|
|
698
|
+
return age >= self.rotation_interval
|
|
699
|
+
|
|
700
|
+
def needs_warning(self, credential_key: str) -> bool:
|
|
701
|
+
"""Check if credential is nearing expiration.
|
|
702
|
+
|
|
703
|
+
Parameters
|
|
704
|
+
----------
|
|
705
|
+
credential_key : str
|
|
706
|
+
Credential identifier.
|
|
707
|
+
|
|
708
|
+
Returns
|
|
709
|
+
-------
|
|
710
|
+
bool
|
|
711
|
+
True if warning should be issued.
|
|
712
|
+
"""
|
|
713
|
+
last = self._last_rotation.get(credential_key)
|
|
714
|
+
if last is None:
|
|
715
|
+
return True
|
|
716
|
+
age = datetime.now(timezone.utc) - last
|
|
717
|
+
time_until_rotation = self.rotation_interval - age
|
|
718
|
+
return time_until_rotation <= self.warning_threshold
|
|
719
|
+
|
|
720
|
+
def rotate_if_needed(self, credential_key: str) -> bool:
|
|
721
|
+
"""Rotate credential if needed.
|
|
722
|
+
|
|
723
|
+
Parameters
|
|
724
|
+
----------
|
|
725
|
+
credential_key : str
|
|
726
|
+
Credential identifier.
|
|
727
|
+
|
|
728
|
+
Returns
|
|
729
|
+
-------
|
|
730
|
+
bool
|
|
731
|
+
True if rotation was performed.
|
|
732
|
+
"""
|
|
733
|
+
if self.needs_rotation(credential_key):
|
|
734
|
+
callback = self._rotation_callbacks.get(credential_key)
|
|
735
|
+
if callback is not None:
|
|
736
|
+
callback()
|
|
737
|
+
self._last_rotation[credential_key] = datetime.now(timezone.utc)
|
|
738
|
+
return True
|
|
739
|
+
return False
|
|
740
|
+
|
|
741
|
+
def get_credential_age(self, credential_key: str) -> timedelta | None:
|
|
742
|
+
"""Get age of credential.
|
|
743
|
+
|
|
744
|
+
Parameters
|
|
745
|
+
----------
|
|
746
|
+
credential_key : str
|
|
747
|
+
Credential identifier.
|
|
748
|
+
|
|
749
|
+
Returns
|
|
750
|
+
-------
|
|
751
|
+
timedelta or None
|
|
752
|
+
Age of credential, or None if not registered.
|
|
753
|
+
"""
|
|
754
|
+
last = self._last_rotation.get(credential_key)
|
|
755
|
+
if last is None:
|
|
756
|
+
return None
|
|
757
|
+
return datetime.now(timezone.utc) - last
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
class SecurityAuditor:
|
|
761
|
+
"""Security auditor for configuration and environment.
|
|
762
|
+
|
|
763
|
+
Implements diagnostic nodal analysis to identify security
|
|
764
|
+
coherence issues and dissonances in configuration.
|
|
765
|
+
"""
|
|
766
|
+
|
|
767
|
+
SENSITIVE_PATTERNS = frozenset(
|
|
768
|
+
[
|
|
769
|
+
"password",
|
|
770
|
+
"secret",
|
|
771
|
+
"key",
|
|
772
|
+
"token",
|
|
773
|
+
"credential",
|
|
774
|
+
"api_key",
|
|
775
|
+
"apikey",
|
|
776
|
+
"auth",
|
|
777
|
+
"private",
|
|
778
|
+
]
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
WEAK_VALUES = frozenset(
|
|
782
|
+
[
|
|
783
|
+
"password",
|
|
784
|
+
"123456",
|
|
785
|
+
"admin",
|
|
786
|
+
"secret",
|
|
787
|
+
"test",
|
|
788
|
+
"changeme",
|
|
789
|
+
"default",
|
|
790
|
+
"root",
|
|
791
|
+
"toor",
|
|
792
|
+
]
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
def audit_environment_variables(self) -> list[str]:
|
|
796
|
+
"""Audit environment variables for security issues.
|
|
797
|
+
|
|
798
|
+
Returns
|
|
799
|
+
-------
|
|
800
|
+
list of str
|
|
801
|
+
List of security issues found.
|
|
802
|
+
"""
|
|
803
|
+
issues = []
|
|
804
|
+
|
|
805
|
+
for var_name in os.environ:
|
|
806
|
+
var_name_lower = var_name.lower()
|
|
807
|
+
var_value = os.environ[var_name]
|
|
808
|
+
|
|
809
|
+
# Check if this is a sensitive variable
|
|
810
|
+
is_sensitive = any(
|
|
811
|
+
pattern in var_name_lower for pattern in self.SENSITIVE_PATTERNS
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
if is_sensitive:
|
|
815
|
+
# Check for weak values
|
|
816
|
+
if var_value.lower() in self.WEAK_VALUES:
|
|
817
|
+
issues.append(
|
|
818
|
+
f"Weak/default value in sensitive variable: {var_name}"
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
# Check for too short secrets
|
|
822
|
+
if len(var_value) < 8:
|
|
823
|
+
issues.append(
|
|
824
|
+
f"Secret too short ({len(var_value)} chars) in: {var_name}"
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
# Check if secret looks like a placeholder
|
|
828
|
+
if var_value in ["your-secret", "your-token", "changeme", "..."]:
|
|
829
|
+
issues.append(f"Placeholder value detected in: {var_name}")
|
|
830
|
+
|
|
831
|
+
return issues
|
|
832
|
+
|
|
833
|
+
def check_redis_config_security(self) -> list[str]:
|
|
834
|
+
"""Check Redis configuration for security issues.
|
|
835
|
+
|
|
836
|
+
Returns
|
|
837
|
+
-------
|
|
838
|
+
list of str
|
|
839
|
+
List of security issues found.
|
|
840
|
+
"""
|
|
841
|
+
issues = []
|
|
842
|
+
|
|
843
|
+
# Check if password is set
|
|
844
|
+
redis_password = os.environ.get("REDIS_PASSWORD")
|
|
845
|
+
if not redis_password:
|
|
846
|
+
issues.append("REDIS_PASSWORD not set - authentication disabled")
|
|
847
|
+
|
|
848
|
+
# Check if TLS is enabled
|
|
849
|
+
redis_use_tls = os.environ.get("REDIS_USE_TLS", "false").lower()
|
|
850
|
+
if redis_use_tls not in ("true", "1", "yes", "on"):
|
|
851
|
+
issues.append("REDIS_USE_TLS not enabled - unencrypted connection")
|
|
852
|
+
|
|
853
|
+
return issues
|
|
854
|
+
|
|
855
|
+
def check_cache_secret_security(self) -> list[str]:
|
|
856
|
+
"""Check cache secret configuration.
|
|
857
|
+
|
|
858
|
+
Returns
|
|
859
|
+
-------
|
|
860
|
+
list of str
|
|
861
|
+
List of security issues found.
|
|
862
|
+
"""
|
|
863
|
+
issues = []
|
|
864
|
+
|
|
865
|
+
cache_secret = os.environ.get("TNFR_CACHE_SECRET")
|
|
866
|
+
if not cache_secret:
|
|
867
|
+
issues.append("TNFR_CACHE_SECRET not set - unsigned cache data")
|
|
868
|
+
else:
|
|
869
|
+
# Check if secret is strong enough
|
|
870
|
+
try:
|
|
871
|
+
secret_bytes = bytes.fromhex(cache_secret)
|
|
872
|
+
if len(secret_bytes) < 16:
|
|
873
|
+
issues.append(
|
|
874
|
+
f"TNFR_CACHE_SECRET too short: {len(secret_bytes)} bytes "
|
|
875
|
+
"(recommend 32+ bytes)"
|
|
876
|
+
)
|
|
877
|
+
except ValueError:
|
|
878
|
+
issues.append("TNFR_CACHE_SECRET is not valid hex")
|
|
879
|
+
|
|
880
|
+
return issues
|
|
881
|
+
|
|
882
|
+
def run_full_audit(self) -> dict[str, list[str]]:
|
|
883
|
+
"""Run complete security audit.
|
|
884
|
+
|
|
885
|
+
Returns
|
|
886
|
+
-------
|
|
887
|
+
dict
|
|
888
|
+
Dictionary mapping audit category to list of issues.
|
|
889
|
+
"""
|
|
890
|
+
return {
|
|
891
|
+
"environment_variables": self.audit_environment_variables(),
|
|
892
|
+
"redis_config": self.check_redis_config_security(),
|
|
893
|
+
"cache_secret": self.check_cache_secret_security(),
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
# Global instances for convenience
|
|
898
|
+
_global_secret_manager: Optional[SecureSecretManager] = None
|
|
899
|
+
_global_rotation_manager: Optional[CredentialRotationManager] = None
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def get_secret_manager() -> SecureSecretManager:
|
|
903
|
+
"""Get global secret manager instance.
|
|
904
|
+
|
|
905
|
+
Returns
|
|
906
|
+
-------
|
|
907
|
+
SecureSecretManager
|
|
908
|
+
Global secret manager instance.
|
|
909
|
+
"""
|
|
910
|
+
global _global_secret_manager
|
|
911
|
+
if _global_secret_manager is None:
|
|
912
|
+
_global_secret_manager = SecureSecretManager()
|
|
913
|
+
return _global_secret_manager
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def get_rotation_manager() -> CredentialRotationManager:
|
|
917
|
+
"""Get global rotation manager instance.
|
|
918
|
+
|
|
919
|
+
Returns
|
|
920
|
+
-------
|
|
921
|
+
CredentialRotationManager
|
|
922
|
+
Global rotation manager instance.
|
|
923
|
+
"""
|
|
924
|
+
global _global_rotation_manager
|
|
925
|
+
if _global_rotation_manager is None:
|
|
926
|
+
_global_rotation_manager = CredentialRotationManager()
|
|
927
|
+
return _global_rotation_manager
|