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.

Files changed (360) hide show
  1. tnfr/__init__.py +375 -56
  2. tnfr/__init__.pyi +33 -0
  3. tnfr/_compat.py +10 -0
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +49 -0
  6. tnfr/_version.pyi +7 -0
  7. tnfr/alias.py +723 -0
  8. tnfr/alias.pyi +108 -0
  9. tnfr/backends/__init__.py +354 -0
  10. tnfr/backends/jax_backend.py +173 -0
  11. tnfr/backends/numpy_backend.py +238 -0
  12. tnfr/backends/optimized_numpy.py +420 -0
  13. tnfr/backends/torch_backend.py +408 -0
  14. tnfr/cache.py +171 -0
  15. tnfr/cache.pyi +13 -0
  16. tnfr/cli/__init__.py +110 -0
  17. tnfr/cli/__init__.pyi +26 -0
  18. tnfr/cli/arguments.py +489 -0
  19. tnfr/cli/arguments.pyi +29 -0
  20. tnfr/cli/execution.py +914 -0
  21. tnfr/cli/execution.pyi +70 -0
  22. tnfr/cli/interactive_validator.py +614 -0
  23. tnfr/cli/utils.py +51 -0
  24. tnfr/cli/utils.pyi +7 -0
  25. tnfr/cli/validate.py +236 -0
  26. tnfr/compat/__init__.py +85 -0
  27. tnfr/compat/dataclass.py +136 -0
  28. tnfr/compat/jsonschema_stub.py +61 -0
  29. tnfr/compat/matplotlib_stub.py +73 -0
  30. tnfr/compat/numpy_stub.py +155 -0
  31. tnfr/config/__init__.py +224 -0
  32. tnfr/config/__init__.pyi +10 -0
  33. tnfr/config/constants.py +104 -0
  34. tnfr/config/constants.pyi +12 -0
  35. tnfr/config/defaults.py +54 -0
  36. tnfr/config/defaults_core.py +212 -0
  37. tnfr/config/defaults_init.py +33 -0
  38. tnfr/config/defaults_metric.py +104 -0
  39. tnfr/config/feature_flags.py +81 -0
  40. tnfr/config/feature_flags.pyi +16 -0
  41. tnfr/config/glyph_constants.py +31 -0
  42. tnfr/config/init.py +77 -0
  43. tnfr/config/init.pyi +8 -0
  44. tnfr/config/operator_names.py +254 -0
  45. tnfr/config/operator_names.pyi +36 -0
  46. tnfr/config/physics_derivation.py +354 -0
  47. tnfr/config/presets.py +83 -0
  48. tnfr/config/presets.pyi +7 -0
  49. tnfr/config/security.py +927 -0
  50. tnfr/config/thresholds.py +114 -0
  51. tnfr/config/tnfr_config.py +498 -0
  52. tnfr/constants/__init__.py +92 -0
  53. tnfr/constants/__init__.pyi +92 -0
  54. tnfr/constants/aliases.py +33 -0
  55. tnfr/constants/aliases.pyi +27 -0
  56. tnfr/constants/init.py +33 -0
  57. tnfr/constants/init.pyi +12 -0
  58. tnfr/constants/metric.py +104 -0
  59. tnfr/constants/metric.pyi +19 -0
  60. tnfr/core/__init__.py +33 -0
  61. tnfr/core/container.py +226 -0
  62. tnfr/core/default_implementations.py +329 -0
  63. tnfr/core/interfaces.py +279 -0
  64. tnfr/dynamics/__init__.py +238 -0
  65. tnfr/dynamics/__init__.pyi +83 -0
  66. tnfr/dynamics/adaptation.py +267 -0
  67. tnfr/dynamics/adaptation.pyi +7 -0
  68. tnfr/dynamics/adaptive_sequences.py +189 -0
  69. tnfr/dynamics/adaptive_sequences.pyi +14 -0
  70. tnfr/dynamics/aliases.py +23 -0
  71. tnfr/dynamics/aliases.pyi +19 -0
  72. tnfr/dynamics/bifurcation.py +232 -0
  73. tnfr/dynamics/canonical.py +229 -0
  74. tnfr/dynamics/canonical.pyi +48 -0
  75. tnfr/dynamics/coordination.py +385 -0
  76. tnfr/dynamics/coordination.pyi +25 -0
  77. tnfr/dynamics/dnfr.py +3034 -0
  78. tnfr/dynamics/dnfr.pyi +26 -0
  79. tnfr/dynamics/dynamic_limits.py +225 -0
  80. tnfr/dynamics/feedback.py +252 -0
  81. tnfr/dynamics/feedback.pyi +24 -0
  82. tnfr/dynamics/fused_dnfr.py +454 -0
  83. tnfr/dynamics/homeostasis.py +157 -0
  84. tnfr/dynamics/homeostasis.pyi +14 -0
  85. tnfr/dynamics/integrators.py +661 -0
  86. tnfr/dynamics/integrators.pyi +36 -0
  87. tnfr/dynamics/learning.py +310 -0
  88. tnfr/dynamics/learning.pyi +33 -0
  89. tnfr/dynamics/metabolism.py +254 -0
  90. tnfr/dynamics/nbody.py +796 -0
  91. tnfr/dynamics/nbody_tnfr.py +783 -0
  92. tnfr/dynamics/propagation.py +326 -0
  93. tnfr/dynamics/runtime.py +908 -0
  94. tnfr/dynamics/runtime.pyi +77 -0
  95. tnfr/dynamics/sampling.py +36 -0
  96. tnfr/dynamics/sampling.pyi +7 -0
  97. tnfr/dynamics/selectors.py +711 -0
  98. tnfr/dynamics/selectors.pyi +85 -0
  99. tnfr/dynamics/structural_clip.py +207 -0
  100. tnfr/errors/__init__.py +37 -0
  101. tnfr/errors/contextual.py +492 -0
  102. tnfr/execution.py +223 -0
  103. tnfr/execution.pyi +45 -0
  104. tnfr/extensions/__init__.py +205 -0
  105. tnfr/extensions/__init__.pyi +18 -0
  106. tnfr/extensions/base.py +173 -0
  107. tnfr/extensions/base.pyi +35 -0
  108. tnfr/extensions/business/__init__.py +71 -0
  109. tnfr/extensions/business/__init__.pyi +11 -0
  110. tnfr/extensions/business/cookbook.py +88 -0
  111. tnfr/extensions/business/cookbook.pyi +8 -0
  112. tnfr/extensions/business/health_analyzers.py +202 -0
  113. tnfr/extensions/business/health_analyzers.pyi +9 -0
  114. tnfr/extensions/business/patterns.py +183 -0
  115. tnfr/extensions/business/patterns.pyi +8 -0
  116. tnfr/extensions/medical/__init__.py +73 -0
  117. tnfr/extensions/medical/__init__.pyi +11 -0
  118. tnfr/extensions/medical/cookbook.py +88 -0
  119. tnfr/extensions/medical/cookbook.pyi +8 -0
  120. tnfr/extensions/medical/health_analyzers.py +181 -0
  121. tnfr/extensions/medical/health_analyzers.pyi +9 -0
  122. tnfr/extensions/medical/patterns.py +163 -0
  123. tnfr/extensions/medical/patterns.pyi +8 -0
  124. tnfr/flatten.py +262 -0
  125. tnfr/flatten.pyi +21 -0
  126. tnfr/gamma.py +354 -0
  127. tnfr/gamma.pyi +36 -0
  128. tnfr/glyph_history.py +377 -0
  129. tnfr/glyph_history.pyi +35 -0
  130. tnfr/glyph_runtime.py +19 -0
  131. tnfr/glyph_runtime.pyi +8 -0
  132. tnfr/immutable.py +218 -0
  133. tnfr/immutable.pyi +36 -0
  134. tnfr/initialization.py +203 -0
  135. tnfr/initialization.pyi +65 -0
  136. tnfr/io.py +10 -0
  137. tnfr/io.pyi +13 -0
  138. tnfr/locking.py +37 -0
  139. tnfr/locking.pyi +7 -0
  140. tnfr/mathematics/__init__.py +79 -0
  141. tnfr/mathematics/backend.py +453 -0
  142. tnfr/mathematics/backend.pyi +99 -0
  143. tnfr/mathematics/dynamics.py +408 -0
  144. tnfr/mathematics/dynamics.pyi +90 -0
  145. tnfr/mathematics/epi.py +391 -0
  146. tnfr/mathematics/epi.pyi +65 -0
  147. tnfr/mathematics/generators.py +242 -0
  148. tnfr/mathematics/generators.pyi +29 -0
  149. tnfr/mathematics/metrics.py +119 -0
  150. tnfr/mathematics/metrics.pyi +16 -0
  151. tnfr/mathematics/operators.py +239 -0
  152. tnfr/mathematics/operators.pyi +59 -0
  153. tnfr/mathematics/operators_factory.py +124 -0
  154. tnfr/mathematics/operators_factory.pyi +11 -0
  155. tnfr/mathematics/projection.py +87 -0
  156. tnfr/mathematics/projection.pyi +33 -0
  157. tnfr/mathematics/runtime.py +182 -0
  158. tnfr/mathematics/runtime.pyi +64 -0
  159. tnfr/mathematics/spaces.py +256 -0
  160. tnfr/mathematics/spaces.pyi +83 -0
  161. tnfr/mathematics/transforms.py +305 -0
  162. tnfr/mathematics/transforms.pyi +62 -0
  163. tnfr/metrics/__init__.py +79 -0
  164. tnfr/metrics/__init__.pyi +20 -0
  165. tnfr/metrics/buffer_cache.py +163 -0
  166. tnfr/metrics/buffer_cache.pyi +24 -0
  167. tnfr/metrics/cache_utils.py +214 -0
  168. tnfr/metrics/coherence.py +2009 -0
  169. tnfr/metrics/coherence.pyi +129 -0
  170. tnfr/metrics/common.py +158 -0
  171. tnfr/metrics/common.pyi +35 -0
  172. tnfr/metrics/core.py +316 -0
  173. tnfr/metrics/core.pyi +13 -0
  174. tnfr/metrics/diagnosis.py +833 -0
  175. tnfr/metrics/diagnosis.pyi +86 -0
  176. tnfr/metrics/emergence.py +245 -0
  177. tnfr/metrics/export.py +179 -0
  178. tnfr/metrics/export.pyi +7 -0
  179. tnfr/metrics/glyph_timing.py +379 -0
  180. tnfr/metrics/glyph_timing.pyi +81 -0
  181. tnfr/metrics/learning_metrics.py +280 -0
  182. tnfr/metrics/learning_metrics.pyi +21 -0
  183. tnfr/metrics/phase_coherence.py +351 -0
  184. tnfr/metrics/phase_compatibility.py +349 -0
  185. tnfr/metrics/reporting.py +183 -0
  186. tnfr/metrics/reporting.pyi +25 -0
  187. tnfr/metrics/sense_index.py +1203 -0
  188. tnfr/metrics/sense_index.pyi +9 -0
  189. tnfr/metrics/trig.py +373 -0
  190. tnfr/metrics/trig.pyi +13 -0
  191. tnfr/metrics/trig_cache.py +233 -0
  192. tnfr/metrics/trig_cache.pyi +10 -0
  193. tnfr/multiscale/__init__.py +32 -0
  194. tnfr/multiscale/hierarchical.py +517 -0
  195. tnfr/node.py +763 -0
  196. tnfr/node.pyi +139 -0
  197. tnfr/observers.py +255 -130
  198. tnfr/observers.pyi +31 -0
  199. tnfr/ontosim.py +144 -137
  200. tnfr/ontosim.pyi +28 -0
  201. tnfr/operators/__init__.py +1672 -0
  202. tnfr/operators/__init__.pyi +31 -0
  203. tnfr/operators/algebra.py +277 -0
  204. tnfr/operators/canonical_patterns.py +420 -0
  205. tnfr/operators/cascade.py +267 -0
  206. tnfr/operators/cycle_detection.py +358 -0
  207. tnfr/operators/definitions.py +4108 -0
  208. tnfr/operators/definitions.pyi +78 -0
  209. tnfr/operators/grammar.py +1164 -0
  210. tnfr/operators/grammar.pyi +140 -0
  211. tnfr/operators/hamiltonian.py +710 -0
  212. tnfr/operators/health_analyzer.py +809 -0
  213. tnfr/operators/jitter.py +272 -0
  214. tnfr/operators/jitter.pyi +11 -0
  215. tnfr/operators/lifecycle.py +314 -0
  216. tnfr/operators/metabolism.py +618 -0
  217. tnfr/operators/metrics.py +2138 -0
  218. tnfr/operators/network_analysis/__init__.py +27 -0
  219. tnfr/operators/network_analysis/source_detection.py +186 -0
  220. tnfr/operators/nodal_equation.py +395 -0
  221. tnfr/operators/pattern_detection.py +660 -0
  222. tnfr/operators/patterns.py +669 -0
  223. tnfr/operators/postconditions/__init__.py +38 -0
  224. tnfr/operators/postconditions/mutation.py +236 -0
  225. tnfr/operators/preconditions/__init__.py +1226 -0
  226. tnfr/operators/preconditions/coherence.py +305 -0
  227. tnfr/operators/preconditions/dissonance.py +236 -0
  228. tnfr/operators/preconditions/emission.py +128 -0
  229. tnfr/operators/preconditions/mutation.py +580 -0
  230. tnfr/operators/preconditions/reception.py +125 -0
  231. tnfr/operators/preconditions/resonance.py +364 -0
  232. tnfr/operators/registry.py +74 -0
  233. tnfr/operators/registry.pyi +9 -0
  234. tnfr/operators/remesh.py +1809 -0
  235. tnfr/operators/remesh.pyi +26 -0
  236. tnfr/operators/structural_units.py +268 -0
  237. tnfr/operators/unified_grammar.py +105 -0
  238. tnfr/parallel/__init__.py +54 -0
  239. tnfr/parallel/auto_scaler.py +234 -0
  240. tnfr/parallel/distributed.py +384 -0
  241. tnfr/parallel/engine.py +238 -0
  242. tnfr/parallel/gpu_engine.py +420 -0
  243. tnfr/parallel/monitoring.py +248 -0
  244. tnfr/parallel/partitioner.py +459 -0
  245. tnfr/py.typed +0 -0
  246. tnfr/recipes/__init__.py +22 -0
  247. tnfr/recipes/cookbook.py +743 -0
  248. tnfr/rng.py +178 -0
  249. tnfr/rng.pyi +26 -0
  250. tnfr/schemas/__init__.py +8 -0
  251. tnfr/schemas/grammar.json +94 -0
  252. tnfr/sdk/__init__.py +107 -0
  253. tnfr/sdk/__init__.pyi +19 -0
  254. tnfr/sdk/adaptive_system.py +173 -0
  255. tnfr/sdk/adaptive_system.pyi +21 -0
  256. tnfr/sdk/builders.py +370 -0
  257. tnfr/sdk/builders.pyi +51 -0
  258. tnfr/sdk/fluent.py +1121 -0
  259. tnfr/sdk/fluent.pyi +74 -0
  260. tnfr/sdk/templates.py +342 -0
  261. tnfr/sdk/templates.pyi +41 -0
  262. tnfr/sdk/utils.py +341 -0
  263. tnfr/secure_config.py +46 -0
  264. tnfr/security/__init__.py +70 -0
  265. tnfr/security/database.py +514 -0
  266. tnfr/security/subprocess.py +503 -0
  267. tnfr/security/validation.py +290 -0
  268. tnfr/selector.py +247 -0
  269. tnfr/selector.pyi +19 -0
  270. tnfr/sense.py +378 -0
  271. tnfr/sense.pyi +23 -0
  272. tnfr/services/__init__.py +17 -0
  273. tnfr/services/orchestrator.py +325 -0
  274. tnfr/sparse/__init__.py +39 -0
  275. tnfr/sparse/representations.py +492 -0
  276. tnfr/structural.py +705 -0
  277. tnfr/structural.pyi +83 -0
  278. tnfr/telemetry/__init__.py +35 -0
  279. tnfr/telemetry/cache_metrics.py +226 -0
  280. tnfr/telemetry/cache_metrics.pyi +64 -0
  281. tnfr/telemetry/nu_f.py +422 -0
  282. tnfr/telemetry/nu_f.pyi +108 -0
  283. tnfr/telemetry/verbosity.py +36 -0
  284. tnfr/telemetry/verbosity.pyi +15 -0
  285. tnfr/tokens.py +58 -0
  286. tnfr/tokens.pyi +36 -0
  287. tnfr/tools/__init__.py +20 -0
  288. tnfr/tools/domain_templates.py +478 -0
  289. tnfr/tools/sequence_generator.py +846 -0
  290. tnfr/topology/__init__.py +13 -0
  291. tnfr/topology/asymmetry.py +151 -0
  292. tnfr/trace.py +543 -0
  293. tnfr/trace.pyi +42 -0
  294. tnfr/tutorials/__init__.py +38 -0
  295. tnfr/tutorials/autonomous_evolution.py +285 -0
  296. tnfr/tutorials/interactive.py +1576 -0
  297. tnfr/tutorials/structural_metabolism.py +238 -0
  298. tnfr/types.py +775 -0
  299. tnfr/types.pyi +357 -0
  300. tnfr/units.py +68 -0
  301. tnfr/units.pyi +13 -0
  302. tnfr/utils/__init__.py +282 -0
  303. tnfr/utils/__init__.pyi +215 -0
  304. tnfr/utils/cache.py +4223 -0
  305. tnfr/utils/cache.pyi +470 -0
  306. tnfr/utils/callbacks.py +375 -0
  307. tnfr/utils/callbacks.pyi +49 -0
  308. tnfr/utils/chunks.py +108 -0
  309. tnfr/utils/chunks.pyi +22 -0
  310. tnfr/utils/data.py +428 -0
  311. tnfr/utils/data.pyi +74 -0
  312. tnfr/utils/graph.py +85 -0
  313. tnfr/utils/graph.pyi +10 -0
  314. tnfr/utils/init.py +821 -0
  315. tnfr/utils/init.pyi +80 -0
  316. tnfr/utils/io.py +559 -0
  317. tnfr/utils/io.pyi +66 -0
  318. tnfr/utils/numeric.py +114 -0
  319. tnfr/utils/numeric.pyi +21 -0
  320. tnfr/validation/__init__.py +257 -0
  321. tnfr/validation/__init__.pyi +85 -0
  322. tnfr/validation/compatibility.py +460 -0
  323. tnfr/validation/compatibility.pyi +6 -0
  324. tnfr/validation/config.py +73 -0
  325. tnfr/validation/graph.py +139 -0
  326. tnfr/validation/graph.pyi +18 -0
  327. tnfr/validation/input_validation.py +755 -0
  328. tnfr/validation/invariants.py +712 -0
  329. tnfr/validation/rules.py +253 -0
  330. tnfr/validation/rules.pyi +44 -0
  331. tnfr/validation/runtime.py +279 -0
  332. tnfr/validation/runtime.pyi +28 -0
  333. tnfr/validation/sequence_validator.py +162 -0
  334. tnfr/validation/soft_filters.py +170 -0
  335. tnfr/validation/soft_filters.pyi +32 -0
  336. tnfr/validation/spectral.py +164 -0
  337. tnfr/validation/spectral.pyi +42 -0
  338. tnfr/validation/validator.py +1266 -0
  339. tnfr/validation/window.py +39 -0
  340. tnfr/validation/window.pyi +1 -0
  341. tnfr/visualization/__init__.py +98 -0
  342. tnfr/visualization/cascade_viz.py +256 -0
  343. tnfr/visualization/hierarchy.py +284 -0
  344. tnfr/visualization/sequence_plotter.py +784 -0
  345. tnfr/viz/__init__.py +60 -0
  346. tnfr/viz/matplotlib.py +278 -0
  347. tnfr/viz/matplotlib.pyi +35 -0
  348. tnfr-8.5.0.dist-info/METADATA +573 -0
  349. tnfr-8.5.0.dist-info/RECORD +353 -0
  350. tnfr-8.5.0.dist-info/entry_points.txt +3 -0
  351. tnfr-3.0.3.dist-info/licenses/LICENSE.txt → tnfr-8.5.0.dist-info/licenses/LICENSE.md +1 -1
  352. tnfr/constants.py +0 -183
  353. tnfr/dynamics.py +0 -543
  354. tnfr/helpers.py +0 -198
  355. tnfr/main.py +0 -37
  356. tnfr/operators.py +0 -296
  357. tnfr-3.0.3.dist-info/METADATA +0 -35
  358. tnfr-3.0.3.dist-info/RECORD +0 -13
  359. {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
  360. {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
@@ -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