truthound 1.0.8__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.
- truthound/__init__.py +162 -0
- truthound/adapters.py +100 -0
- truthound/api.py +365 -0
- truthound/audit/__init__.py +248 -0
- truthound/audit/core.py +967 -0
- truthound/audit/filters.py +620 -0
- truthound/audit/formatters.py +707 -0
- truthound/audit/logger.py +902 -0
- truthound/audit/middleware.py +571 -0
- truthound/audit/storage.py +1083 -0
- truthound/benchmark/__init__.py +123 -0
- truthound/benchmark/base.py +757 -0
- truthound/benchmark/comparison.py +635 -0
- truthound/benchmark/generators.py +706 -0
- truthound/benchmark/reporters.py +718 -0
- truthound/benchmark/runner.py +635 -0
- truthound/benchmark/scenarios.py +712 -0
- truthound/cache.py +252 -0
- truthound/checkpoint/__init__.py +136 -0
- truthound/checkpoint/actions/__init__.py +164 -0
- truthound/checkpoint/actions/base.py +324 -0
- truthound/checkpoint/actions/custom.py +234 -0
- truthound/checkpoint/actions/discord_notify.py +290 -0
- truthound/checkpoint/actions/email_notify.py +405 -0
- truthound/checkpoint/actions/github_action.py +406 -0
- truthound/checkpoint/actions/opsgenie.py +1499 -0
- truthound/checkpoint/actions/pagerduty.py +226 -0
- truthound/checkpoint/actions/slack_notify.py +233 -0
- truthound/checkpoint/actions/store_result.py +249 -0
- truthound/checkpoint/actions/teams_notify.py +1570 -0
- truthound/checkpoint/actions/telegram_notify.py +419 -0
- truthound/checkpoint/actions/update_docs.py +552 -0
- truthound/checkpoint/actions/webhook.py +293 -0
- truthound/checkpoint/analytics/__init__.py +147 -0
- truthound/checkpoint/analytics/aggregations/__init__.py +23 -0
- truthound/checkpoint/analytics/aggregations/rollup.py +481 -0
- truthound/checkpoint/analytics/aggregations/time_bucket.py +306 -0
- truthound/checkpoint/analytics/analyzers/__init__.py +17 -0
- truthound/checkpoint/analytics/analyzers/anomaly.py +386 -0
- truthound/checkpoint/analytics/analyzers/base.py +270 -0
- truthound/checkpoint/analytics/analyzers/forecast.py +421 -0
- truthound/checkpoint/analytics/analyzers/trend.py +314 -0
- truthound/checkpoint/analytics/models.py +292 -0
- truthound/checkpoint/analytics/protocols.py +549 -0
- truthound/checkpoint/analytics/service.py +718 -0
- truthound/checkpoint/analytics/stores/__init__.py +16 -0
- truthound/checkpoint/analytics/stores/base.py +306 -0
- truthound/checkpoint/analytics/stores/memory_store.py +353 -0
- truthound/checkpoint/analytics/stores/sqlite_store.py +557 -0
- truthound/checkpoint/analytics/stores/timescale_store.py +501 -0
- truthound/checkpoint/async_actions.py +794 -0
- truthound/checkpoint/async_base.py +708 -0
- truthound/checkpoint/async_checkpoint.py +617 -0
- truthound/checkpoint/async_runner.py +639 -0
- truthound/checkpoint/checkpoint.py +527 -0
- truthound/checkpoint/ci/__init__.py +61 -0
- truthound/checkpoint/ci/detector.py +355 -0
- truthound/checkpoint/ci/reporter.py +436 -0
- truthound/checkpoint/ci/templates.py +454 -0
- truthound/checkpoint/circuitbreaker/__init__.py +133 -0
- truthound/checkpoint/circuitbreaker/breaker.py +542 -0
- truthound/checkpoint/circuitbreaker/core.py +252 -0
- truthound/checkpoint/circuitbreaker/detection.py +459 -0
- truthound/checkpoint/circuitbreaker/middleware.py +389 -0
- truthound/checkpoint/circuitbreaker/registry.py +357 -0
- truthound/checkpoint/distributed/__init__.py +139 -0
- truthound/checkpoint/distributed/backends/__init__.py +35 -0
- truthound/checkpoint/distributed/backends/celery_backend.py +503 -0
- truthound/checkpoint/distributed/backends/kubernetes_backend.py +696 -0
- truthound/checkpoint/distributed/backends/local_backend.py +397 -0
- truthound/checkpoint/distributed/backends/ray_backend.py +625 -0
- truthound/checkpoint/distributed/base.py +774 -0
- truthound/checkpoint/distributed/orchestrator.py +765 -0
- truthound/checkpoint/distributed/protocols.py +842 -0
- truthound/checkpoint/distributed/registry.py +449 -0
- truthound/checkpoint/idempotency/__init__.py +120 -0
- truthound/checkpoint/idempotency/core.py +295 -0
- truthound/checkpoint/idempotency/fingerprint.py +454 -0
- truthound/checkpoint/idempotency/locking.py +604 -0
- truthound/checkpoint/idempotency/service.py +592 -0
- truthound/checkpoint/idempotency/stores.py +653 -0
- truthound/checkpoint/monitoring/__init__.py +134 -0
- truthound/checkpoint/monitoring/aggregators/__init__.py +15 -0
- truthound/checkpoint/monitoring/aggregators/base.py +372 -0
- truthound/checkpoint/monitoring/aggregators/realtime.py +300 -0
- truthound/checkpoint/monitoring/aggregators/window.py +493 -0
- truthound/checkpoint/monitoring/collectors/__init__.py +17 -0
- truthound/checkpoint/monitoring/collectors/base.py +257 -0
- truthound/checkpoint/monitoring/collectors/memory_collector.py +617 -0
- truthound/checkpoint/monitoring/collectors/prometheus_collector.py +451 -0
- truthound/checkpoint/monitoring/collectors/redis_collector.py +518 -0
- truthound/checkpoint/monitoring/events.py +410 -0
- truthound/checkpoint/monitoring/protocols.py +636 -0
- truthound/checkpoint/monitoring/service.py +578 -0
- truthound/checkpoint/monitoring/views/__init__.py +17 -0
- truthound/checkpoint/monitoring/views/base.py +172 -0
- truthound/checkpoint/monitoring/views/queue_view.py +220 -0
- truthound/checkpoint/monitoring/views/task_view.py +240 -0
- truthound/checkpoint/monitoring/views/worker_view.py +263 -0
- truthound/checkpoint/registry.py +337 -0
- truthound/checkpoint/runner.py +356 -0
- truthound/checkpoint/transaction/__init__.py +133 -0
- truthound/checkpoint/transaction/base.py +389 -0
- truthound/checkpoint/transaction/compensatable.py +537 -0
- truthound/checkpoint/transaction/coordinator.py +576 -0
- truthound/checkpoint/transaction/executor.py +622 -0
- truthound/checkpoint/transaction/idempotency.py +534 -0
- truthound/checkpoint/transaction/saga/__init__.py +143 -0
- truthound/checkpoint/transaction/saga/builder.py +584 -0
- truthound/checkpoint/transaction/saga/definition.py +515 -0
- truthound/checkpoint/transaction/saga/event_store.py +542 -0
- truthound/checkpoint/transaction/saga/patterns.py +833 -0
- truthound/checkpoint/transaction/saga/runner.py +718 -0
- truthound/checkpoint/transaction/saga/state_machine.py +793 -0
- truthound/checkpoint/transaction/saga/strategies.py +780 -0
- truthound/checkpoint/transaction/saga/testing.py +886 -0
- truthound/checkpoint/triggers/__init__.py +58 -0
- truthound/checkpoint/triggers/base.py +237 -0
- truthound/checkpoint/triggers/event.py +385 -0
- truthound/checkpoint/triggers/schedule.py +355 -0
- truthound/cli.py +2358 -0
- truthound/cli_modules/__init__.py +124 -0
- truthound/cli_modules/advanced/__init__.py +45 -0
- truthound/cli_modules/advanced/benchmark.py +343 -0
- truthound/cli_modules/advanced/docs.py +225 -0
- truthound/cli_modules/advanced/lineage.py +209 -0
- truthound/cli_modules/advanced/ml.py +320 -0
- truthound/cli_modules/advanced/realtime.py +196 -0
- truthound/cli_modules/checkpoint/__init__.py +46 -0
- truthound/cli_modules/checkpoint/init.py +114 -0
- truthound/cli_modules/checkpoint/list.py +71 -0
- truthound/cli_modules/checkpoint/run.py +159 -0
- truthound/cli_modules/checkpoint/validate.py +67 -0
- truthound/cli_modules/common/__init__.py +71 -0
- truthound/cli_modules/common/errors.py +414 -0
- truthound/cli_modules/common/options.py +419 -0
- truthound/cli_modules/common/output.py +507 -0
- truthound/cli_modules/common/protocol.py +552 -0
- truthound/cli_modules/core/__init__.py +48 -0
- truthound/cli_modules/core/check.py +123 -0
- truthound/cli_modules/core/compare.py +104 -0
- truthound/cli_modules/core/learn.py +57 -0
- truthound/cli_modules/core/mask.py +77 -0
- truthound/cli_modules/core/profile.py +65 -0
- truthound/cli_modules/core/scan.py +61 -0
- truthound/cli_modules/profiler/__init__.py +51 -0
- truthound/cli_modules/profiler/auto_profile.py +175 -0
- truthound/cli_modules/profiler/metadata.py +107 -0
- truthound/cli_modules/profiler/suite.py +283 -0
- truthound/cli_modules/registry.py +431 -0
- truthound/cli_modules/scaffolding/__init__.py +89 -0
- truthound/cli_modules/scaffolding/base.py +631 -0
- truthound/cli_modules/scaffolding/commands.py +545 -0
- truthound/cli_modules/scaffolding/plugins.py +1072 -0
- truthound/cli_modules/scaffolding/reporters.py +594 -0
- truthound/cli_modules/scaffolding/validators.py +1127 -0
- truthound/common/__init__.py +18 -0
- truthound/common/resilience/__init__.py +130 -0
- truthound/common/resilience/bulkhead.py +266 -0
- truthound/common/resilience/circuit_breaker.py +516 -0
- truthound/common/resilience/composite.py +332 -0
- truthound/common/resilience/config.py +292 -0
- truthound/common/resilience/protocols.py +217 -0
- truthound/common/resilience/rate_limiter.py +404 -0
- truthound/common/resilience/retry.py +341 -0
- truthound/datadocs/__init__.py +260 -0
- truthound/datadocs/base.py +571 -0
- truthound/datadocs/builder.py +761 -0
- truthound/datadocs/charts.py +764 -0
- truthound/datadocs/dashboard/__init__.py +63 -0
- truthound/datadocs/dashboard/app.py +576 -0
- truthound/datadocs/dashboard/components.py +584 -0
- truthound/datadocs/dashboard/state.py +240 -0
- truthound/datadocs/engine/__init__.py +46 -0
- truthound/datadocs/engine/context.py +376 -0
- truthound/datadocs/engine/pipeline.py +618 -0
- truthound/datadocs/engine/registry.py +469 -0
- truthound/datadocs/exporters/__init__.py +49 -0
- truthound/datadocs/exporters/base.py +198 -0
- truthound/datadocs/exporters/html.py +178 -0
- truthound/datadocs/exporters/json_exporter.py +253 -0
- truthound/datadocs/exporters/markdown.py +284 -0
- truthound/datadocs/exporters/pdf.py +392 -0
- truthound/datadocs/i18n/__init__.py +86 -0
- truthound/datadocs/i18n/catalog.py +960 -0
- truthound/datadocs/i18n/formatting.py +505 -0
- truthound/datadocs/i18n/loader.py +256 -0
- truthound/datadocs/i18n/plurals.py +378 -0
- truthound/datadocs/renderers/__init__.py +42 -0
- truthound/datadocs/renderers/base.py +401 -0
- truthound/datadocs/renderers/custom.py +342 -0
- truthound/datadocs/renderers/jinja.py +697 -0
- truthound/datadocs/sections.py +736 -0
- truthound/datadocs/styles.py +931 -0
- truthound/datadocs/themes/__init__.py +101 -0
- truthound/datadocs/themes/base.py +336 -0
- truthound/datadocs/themes/default.py +417 -0
- truthound/datadocs/themes/enterprise.py +419 -0
- truthound/datadocs/themes/loader.py +336 -0
- truthound/datadocs/themes.py +301 -0
- truthound/datadocs/transformers/__init__.py +57 -0
- truthound/datadocs/transformers/base.py +268 -0
- truthound/datadocs/transformers/enrichers.py +544 -0
- truthound/datadocs/transformers/filters.py +447 -0
- truthound/datadocs/transformers/i18n.py +468 -0
- truthound/datadocs/versioning/__init__.py +62 -0
- truthound/datadocs/versioning/diff.py +639 -0
- truthound/datadocs/versioning/storage.py +497 -0
- truthound/datadocs/versioning/version.py +358 -0
- truthound/datasources/__init__.py +223 -0
- truthound/datasources/_async_protocols.py +222 -0
- truthound/datasources/_protocols.py +159 -0
- truthound/datasources/adapters.py +428 -0
- truthound/datasources/async_base.py +599 -0
- truthound/datasources/async_factory.py +511 -0
- truthound/datasources/base.py +516 -0
- truthound/datasources/factory.py +433 -0
- truthound/datasources/nosql/__init__.py +47 -0
- truthound/datasources/nosql/base.py +487 -0
- truthound/datasources/nosql/elasticsearch.py +801 -0
- truthound/datasources/nosql/mongodb.py +636 -0
- truthound/datasources/pandas_optimized.py +582 -0
- truthound/datasources/pandas_source.py +216 -0
- truthound/datasources/polars_source.py +395 -0
- truthound/datasources/spark_source.py +479 -0
- truthound/datasources/sql/__init__.py +154 -0
- truthound/datasources/sql/base.py +710 -0
- truthound/datasources/sql/bigquery.py +410 -0
- truthound/datasources/sql/cloud_base.py +199 -0
- truthound/datasources/sql/databricks.py +471 -0
- truthound/datasources/sql/mysql.py +316 -0
- truthound/datasources/sql/oracle.py +427 -0
- truthound/datasources/sql/postgresql.py +321 -0
- truthound/datasources/sql/redshift.py +479 -0
- truthound/datasources/sql/snowflake.py +439 -0
- truthound/datasources/sql/sqlite.py +286 -0
- truthound/datasources/sql/sqlserver.py +437 -0
- truthound/datasources/streaming/__init__.py +47 -0
- truthound/datasources/streaming/base.py +350 -0
- truthound/datasources/streaming/kafka.py +670 -0
- truthound/decorators.py +98 -0
- truthound/docs/__init__.py +69 -0
- truthound/docs/extractor.py +971 -0
- truthound/docs/generator.py +601 -0
- truthound/docs/parser.py +1037 -0
- truthound/docs/renderer.py +999 -0
- truthound/drift/__init__.py +22 -0
- truthound/drift/compare.py +189 -0
- truthound/drift/detectors.py +464 -0
- truthound/drift/report.py +160 -0
- truthound/execution/__init__.py +65 -0
- truthound/execution/_protocols.py +324 -0
- truthound/execution/base.py +576 -0
- truthound/execution/distributed/__init__.py +179 -0
- truthound/execution/distributed/aggregations.py +731 -0
- truthound/execution/distributed/arrow_bridge.py +817 -0
- truthound/execution/distributed/base.py +550 -0
- truthound/execution/distributed/dask_engine.py +976 -0
- truthound/execution/distributed/mixins.py +766 -0
- truthound/execution/distributed/protocols.py +756 -0
- truthound/execution/distributed/ray_engine.py +1127 -0
- truthound/execution/distributed/registry.py +446 -0
- truthound/execution/distributed/spark_engine.py +1011 -0
- truthound/execution/distributed/validator_adapter.py +682 -0
- truthound/execution/pandas_engine.py +401 -0
- truthound/execution/polars_engine.py +497 -0
- truthound/execution/pushdown/__init__.py +230 -0
- truthound/execution/pushdown/ast.py +1550 -0
- truthound/execution/pushdown/builder.py +1550 -0
- truthound/execution/pushdown/dialects.py +1072 -0
- truthound/execution/pushdown/executor.py +829 -0
- truthound/execution/pushdown/optimizer.py +1041 -0
- truthound/execution/sql_engine.py +518 -0
- truthound/infrastructure/__init__.py +189 -0
- truthound/infrastructure/audit.py +1515 -0
- truthound/infrastructure/config.py +1133 -0
- truthound/infrastructure/encryption.py +1132 -0
- truthound/infrastructure/logging.py +1503 -0
- truthound/infrastructure/metrics.py +1220 -0
- truthound/lineage/__init__.py +89 -0
- truthound/lineage/base.py +746 -0
- truthound/lineage/impact_analysis.py +474 -0
- truthound/lineage/integrations/__init__.py +22 -0
- truthound/lineage/integrations/openlineage.py +548 -0
- truthound/lineage/tracker.py +512 -0
- truthound/lineage/visualization/__init__.py +33 -0
- truthound/lineage/visualization/protocols.py +145 -0
- truthound/lineage/visualization/renderers/__init__.py +20 -0
- truthound/lineage/visualization/renderers/cytoscape.py +329 -0
- truthound/lineage/visualization/renderers/d3.py +331 -0
- truthound/lineage/visualization/renderers/graphviz.py +276 -0
- truthound/lineage/visualization/renderers/mermaid.py +308 -0
- truthound/maskers.py +113 -0
- truthound/ml/__init__.py +124 -0
- truthound/ml/anomaly_models/__init__.py +31 -0
- truthound/ml/anomaly_models/ensemble.py +362 -0
- truthound/ml/anomaly_models/isolation_forest.py +444 -0
- truthound/ml/anomaly_models/statistical.py +392 -0
- truthound/ml/base.py +1178 -0
- truthound/ml/drift_detection/__init__.py +26 -0
- truthound/ml/drift_detection/concept.py +381 -0
- truthound/ml/drift_detection/distribution.py +361 -0
- truthound/ml/drift_detection/feature.py +442 -0
- truthound/ml/drift_detection/multivariate.py +495 -0
- truthound/ml/monitoring/__init__.py +88 -0
- truthound/ml/monitoring/alerting/__init__.py +33 -0
- truthound/ml/monitoring/alerting/handlers.py +427 -0
- truthound/ml/monitoring/alerting/rules.py +508 -0
- truthound/ml/monitoring/collectors/__init__.py +19 -0
- truthound/ml/monitoring/collectors/composite.py +105 -0
- truthound/ml/monitoring/collectors/drift.py +324 -0
- truthound/ml/monitoring/collectors/performance.py +179 -0
- truthound/ml/monitoring/collectors/quality.py +369 -0
- truthound/ml/monitoring/monitor.py +536 -0
- truthound/ml/monitoring/protocols.py +451 -0
- truthound/ml/monitoring/stores/__init__.py +15 -0
- truthound/ml/monitoring/stores/memory.py +201 -0
- truthound/ml/monitoring/stores/prometheus.py +296 -0
- truthound/ml/rule_learning/__init__.py +25 -0
- truthound/ml/rule_learning/constraint_miner.py +443 -0
- truthound/ml/rule_learning/pattern_learner.py +499 -0
- truthound/ml/rule_learning/profile_learner.py +462 -0
- truthound/multitenancy/__init__.py +326 -0
- truthound/multitenancy/core.py +852 -0
- truthound/multitenancy/integration.py +597 -0
- truthound/multitenancy/isolation.py +630 -0
- truthound/multitenancy/manager.py +770 -0
- truthound/multitenancy/middleware.py +765 -0
- truthound/multitenancy/quota.py +537 -0
- truthound/multitenancy/resolvers.py +603 -0
- truthound/multitenancy/storage.py +703 -0
- truthound/observability/__init__.py +307 -0
- truthound/observability/context.py +531 -0
- truthound/observability/instrumentation.py +611 -0
- truthound/observability/logging.py +887 -0
- truthound/observability/metrics.py +1157 -0
- truthound/observability/tracing/__init__.py +178 -0
- truthound/observability/tracing/baggage.py +310 -0
- truthound/observability/tracing/config.py +426 -0
- truthound/observability/tracing/exporter.py +787 -0
- truthound/observability/tracing/integration.py +1018 -0
- truthound/observability/tracing/otel/__init__.py +146 -0
- truthound/observability/tracing/otel/adapter.py +982 -0
- truthound/observability/tracing/otel/bridge.py +1177 -0
- truthound/observability/tracing/otel/compat.py +681 -0
- truthound/observability/tracing/otel/config.py +691 -0
- truthound/observability/tracing/otel/detection.py +327 -0
- truthound/observability/tracing/otel/protocols.py +426 -0
- truthound/observability/tracing/processor.py +561 -0
- truthound/observability/tracing/propagator.py +757 -0
- truthound/observability/tracing/provider.py +569 -0
- truthound/observability/tracing/resource.py +515 -0
- truthound/observability/tracing/sampler.py +487 -0
- truthound/observability/tracing/span.py +676 -0
- truthound/plugins/__init__.py +198 -0
- truthound/plugins/base.py +599 -0
- truthound/plugins/cli.py +680 -0
- truthound/plugins/dependencies/__init__.py +42 -0
- truthound/plugins/dependencies/graph.py +422 -0
- truthound/plugins/dependencies/resolver.py +417 -0
- truthound/plugins/discovery.py +379 -0
- truthound/plugins/docs/__init__.py +46 -0
- truthound/plugins/docs/extractor.py +444 -0
- truthound/plugins/docs/renderer.py +499 -0
- truthound/plugins/enterprise_manager.py +877 -0
- truthound/plugins/examples/__init__.py +19 -0
- truthound/plugins/examples/custom_validators.py +317 -0
- truthound/plugins/examples/slack_notifier.py +312 -0
- truthound/plugins/examples/xml_reporter.py +254 -0
- truthound/plugins/hooks.py +558 -0
- truthound/plugins/lifecycle/__init__.py +43 -0
- truthound/plugins/lifecycle/hot_reload.py +402 -0
- truthound/plugins/lifecycle/manager.py +371 -0
- truthound/plugins/manager.py +736 -0
- truthound/plugins/registry.py +338 -0
- truthound/plugins/security/__init__.py +93 -0
- truthound/plugins/security/exceptions.py +332 -0
- truthound/plugins/security/policies.py +348 -0
- truthound/plugins/security/protocols.py +643 -0
- truthound/plugins/security/sandbox/__init__.py +45 -0
- truthound/plugins/security/sandbox/context.py +158 -0
- truthound/plugins/security/sandbox/engines/__init__.py +19 -0
- truthound/plugins/security/sandbox/engines/container.py +379 -0
- truthound/plugins/security/sandbox/engines/noop.py +144 -0
- truthound/plugins/security/sandbox/engines/process.py +336 -0
- truthound/plugins/security/sandbox/factory.py +211 -0
- truthound/plugins/security/signing/__init__.py +57 -0
- truthound/plugins/security/signing/service.py +330 -0
- truthound/plugins/security/signing/trust_store.py +368 -0
- truthound/plugins/security/signing/verifier.py +459 -0
- truthound/plugins/versioning/__init__.py +41 -0
- truthound/plugins/versioning/constraints.py +297 -0
- truthound/plugins/versioning/resolver.py +329 -0
- truthound/profiler/__init__.py +1729 -0
- truthound/profiler/_lazy.py +452 -0
- truthound/profiler/ab_testing/__init__.py +80 -0
- truthound/profiler/ab_testing/analysis.py +449 -0
- truthound/profiler/ab_testing/base.py +257 -0
- truthound/profiler/ab_testing/experiment.py +395 -0
- truthound/profiler/ab_testing/tracking.py +368 -0
- truthound/profiler/auto_threshold.py +1170 -0
- truthound/profiler/base.py +579 -0
- truthound/profiler/cache_patterns.py +911 -0
- truthound/profiler/caching.py +1303 -0
- truthound/profiler/column_profiler.py +712 -0
- truthound/profiler/comparison.py +1007 -0
- truthound/profiler/custom_patterns.py +1170 -0
- truthound/profiler/dashboard/__init__.py +50 -0
- truthound/profiler/dashboard/app.py +476 -0
- truthound/profiler/dashboard/components.py +457 -0
- truthound/profiler/dashboard/config.py +72 -0
- truthound/profiler/distributed/__init__.py +83 -0
- truthound/profiler/distributed/base.py +281 -0
- truthound/profiler/distributed/dask_backend.py +498 -0
- truthound/profiler/distributed/local_backend.py +293 -0
- truthound/profiler/distributed/profiler.py +304 -0
- truthound/profiler/distributed/ray_backend.py +374 -0
- truthound/profiler/distributed/spark_backend.py +375 -0
- truthound/profiler/distributed.py +1366 -0
- truthound/profiler/enterprise_sampling.py +1065 -0
- truthound/profiler/errors.py +488 -0
- truthound/profiler/evolution/__init__.py +91 -0
- truthound/profiler/evolution/alerts.py +426 -0
- truthound/profiler/evolution/changes.py +206 -0
- truthound/profiler/evolution/compatibility.py +365 -0
- truthound/profiler/evolution/detector.py +372 -0
- truthound/profiler/evolution/protocols.py +121 -0
- truthound/profiler/generators/__init__.py +48 -0
- truthound/profiler/generators/base.py +384 -0
- truthound/profiler/generators/ml_rules.py +375 -0
- truthound/profiler/generators/pattern_rules.py +384 -0
- truthound/profiler/generators/schema_rules.py +267 -0
- truthound/profiler/generators/stats_rules.py +324 -0
- truthound/profiler/generators/suite_generator.py +857 -0
- truthound/profiler/i18n.py +1542 -0
- truthound/profiler/incremental.py +554 -0
- truthound/profiler/incremental_validation.py +1710 -0
- truthound/profiler/integration/__init__.py +73 -0
- truthound/profiler/integration/adapters.py +345 -0
- truthound/profiler/integration/context.py +371 -0
- truthound/profiler/integration/executor.py +527 -0
- truthound/profiler/integration/naming.py +75 -0
- truthound/profiler/integration/protocols.py +243 -0
- truthound/profiler/memory.py +1185 -0
- truthound/profiler/migration/__init__.py +60 -0
- truthound/profiler/migration/base.py +345 -0
- truthound/profiler/migration/manager.py +444 -0
- truthound/profiler/migration/v1_0_to_v1_1.py +484 -0
- truthound/profiler/ml/__init__.py +73 -0
- truthound/profiler/ml/base.py +244 -0
- truthound/profiler/ml/classifier.py +507 -0
- truthound/profiler/ml/feature_extraction.py +604 -0
- truthound/profiler/ml/pretrained.py +448 -0
- truthound/profiler/ml_inference.py +1276 -0
- truthound/profiler/native_patterns.py +815 -0
- truthound/profiler/observability.py +1184 -0
- truthound/profiler/process_timeout.py +1566 -0
- truthound/profiler/progress.py +568 -0
- truthound/profiler/progress_callbacks.py +1734 -0
- truthound/profiler/quality.py +1345 -0
- truthound/profiler/resilience.py +1180 -0
- truthound/profiler/sampled_matcher.py +794 -0
- truthound/profiler/sampling.py +1288 -0
- truthound/profiler/scheduling/__init__.py +82 -0
- truthound/profiler/scheduling/protocols.py +214 -0
- truthound/profiler/scheduling/scheduler.py +474 -0
- truthound/profiler/scheduling/storage.py +457 -0
- truthound/profiler/scheduling/triggers.py +449 -0
- truthound/profiler/schema.py +603 -0
- truthound/profiler/streaming.py +685 -0
- truthound/profiler/streaming_patterns.py +1354 -0
- truthound/profiler/suite_cli.py +625 -0
- truthound/profiler/suite_config.py +789 -0
- truthound/profiler/suite_export.py +1268 -0
- truthound/profiler/table_profiler.py +547 -0
- truthound/profiler/timeout.py +565 -0
- truthound/profiler/validation.py +1532 -0
- truthound/profiler/visualization/__init__.py +118 -0
- truthound/profiler/visualization/base.py +346 -0
- truthound/profiler/visualization/generator.py +1259 -0
- truthound/profiler/visualization/plotly_renderer.py +811 -0
- truthound/profiler/visualization/renderers.py +669 -0
- truthound/profiler/visualization/sections.py +540 -0
- truthound/profiler/visualization.py +2122 -0
- truthound/profiler/yaml_validation.py +1151 -0
- truthound/py.typed +0 -0
- truthound/ratelimit/__init__.py +248 -0
- truthound/ratelimit/algorithms.py +1108 -0
- truthound/ratelimit/core.py +573 -0
- truthound/ratelimit/integration.py +532 -0
- truthound/ratelimit/limiter.py +663 -0
- truthound/ratelimit/middleware.py +700 -0
- truthound/ratelimit/policy.py +792 -0
- truthound/ratelimit/storage.py +763 -0
- truthound/rbac/__init__.py +340 -0
- truthound/rbac/core.py +976 -0
- truthound/rbac/integration.py +760 -0
- truthound/rbac/manager.py +1052 -0
- truthound/rbac/middleware.py +842 -0
- truthound/rbac/policy.py +954 -0
- truthound/rbac/storage.py +878 -0
- truthound/realtime/__init__.py +141 -0
- truthound/realtime/adapters/__init__.py +43 -0
- truthound/realtime/adapters/base.py +533 -0
- truthound/realtime/adapters/kafka.py +487 -0
- truthound/realtime/adapters/kinesis.py +479 -0
- truthound/realtime/adapters/mock.py +243 -0
- truthound/realtime/base.py +553 -0
- truthound/realtime/factory.py +382 -0
- truthound/realtime/incremental.py +660 -0
- truthound/realtime/processing/__init__.py +67 -0
- truthound/realtime/processing/exactly_once.py +575 -0
- truthound/realtime/processing/state.py +547 -0
- truthound/realtime/processing/windows.py +647 -0
- truthound/realtime/protocols.py +569 -0
- truthound/realtime/streaming.py +605 -0
- truthound/realtime/testing/__init__.py +32 -0
- truthound/realtime/testing/containers.py +615 -0
- truthound/realtime/testing/fixtures.py +484 -0
- truthound/report.py +280 -0
- truthound/reporters/__init__.py +46 -0
- truthound/reporters/_protocols.py +30 -0
- truthound/reporters/base.py +324 -0
- truthound/reporters/ci/__init__.py +66 -0
- truthound/reporters/ci/azure.py +436 -0
- truthound/reporters/ci/base.py +509 -0
- truthound/reporters/ci/bitbucket.py +567 -0
- truthound/reporters/ci/circleci.py +547 -0
- truthound/reporters/ci/detection.py +364 -0
- truthound/reporters/ci/factory.py +182 -0
- truthound/reporters/ci/github.py +388 -0
- truthound/reporters/ci/gitlab.py +471 -0
- truthound/reporters/ci/jenkins.py +525 -0
- truthound/reporters/console_reporter.py +299 -0
- truthound/reporters/factory.py +211 -0
- truthound/reporters/html_reporter.py +524 -0
- truthound/reporters/json_reporter.py +256 -0
- truthound/reporters/markdown_reporter.py +280 -0
- truthound/reporters/sdk/__init__.py +174 -0
- truthound/reporters/sdk/builder.py +558 -0
- truthound/reporters/sdk/mixins.py +1150 -0
- truthound/reporters/sdk/schema.py +1493 -0
- truthound/reporters/sdk/templates.py +666 -0
- truthound/reporters/sdk/testing.py +968 -0
- truthound/scanners.py +170 -0
- truthound/scheduling/__init__.py +122 -0
- truthound/scheduling/cron.py +1136 -0
- truthound/scheduling/presets.py +212 -0
- truthound/schema.py +275 -0
- truthound/secrets/__init__.py +173 -0
- truthound/secrets/base.py +618 -0
- truthound/secrets/cloud.py +682 -0
- truthound/secrets/integration.py +507 -0
- truthound/secrets/manager.py +633 -0
- truthound/secrets/oidc/__init__.py +172 -0
- truthound/secrets/oidc/base.py +902 -0
- truthound/secrets/oidc/credential_provider.py +623 -0
- truthound/secrets/oidc/exchangers.py +1001 -0
- truthound/secrets/oidc/github/__init__.py +110 -0
- truthound/secrets/oidc/github/claims.py +718 -0
- truthound/secrets/oidc/github/enhanced_provider.py +693 -0
- truthound/secrets/oidc/github/trust_policy.py +742 -0
- truthound/secrets/oidc/github/verification.py +723 -0
- truthound/secrets/oidc/github/workflow.py +691 -0
- truthound/secrets/oidc/providers.py +825 -0
- truthound/secrets/providers.py +506 -0
- truthound/secrets/resolver.py +495 -0
- truthound/stores/__init__.py +177 -0
- truthound/stores/backends/__init__.py +18 -0
- truthound/stores/backends/_protocols.py +340 -0
- truthound/stores/backends/azure_blob.py +530 -0
- truthound/stores/backends/concurrent_filesystem.py +915 -0
- truthound/stores/backends/connection_pool.py +1365 -0
- truthound/stores/backends/database.py +743 -0
- truthound/stores/backends/filesystem.py +538 -0
- truthound/stores/backends/gcs.py +399 -0
- truthound/stores/backends/memory.py +354 -0
- truthound/stores/backends/s3.py +434 -0
- truthound/stores/backpressure/__init__.py +84 -0
- truthound/stores/backpressure/base.py +375 -0
- truthound/stores/backpressure/circuit_breaker.py +434 -0
- truthound/stores/backpressure/monitor.py +376 -0
- truthound/stores/backpressure/strategies.py +677 -0
- truthound/stores/base.py +551 -0
- truthound/stores/batching/__init__.py +65 -0
- truthound/stores/batching/base.py +305 -0
- truthound/stores/batching/buffer.py +370 -0
- truthound/stores/batching/store.py +248 -0
- truthound/stores/batching/writer.py +521 -0
- truthound/stores/caching/__init__.py +60 -0
- truthound/stores/caching/backends.py +684 -0
- truthound/stores/caching/base.py +356 -0
- truthound/stores/caching/store.py +305 -0
- truthound/stores/compression/__init__.py +193 -0
- truthound/stores/compression/adaptive.py +694 -0
- truthound/stores/compression/base.py +514 -0
- truthound/stores/compression/pipeline.py +868 -0
- truthound/stores/compression/providers.py +672 -0
- truthound/stores/compression/streaming.py +832 -0
- truthound/stores/concurrency/__init__.py +81 -0
- truthound/stores/concurrency/atomic.py +556 -0
- truthound/stores/concurrency/index.py +775 -0
- truthound/stores/concurrency/locks.py +576 -0
- truthound/stores/concurrency/manager.py +482 -0
- truthound/stores/encryption/__init__.py +297 -0
- truthound/stores/encryption/base.py +952 -0
- truthound/stores/encryption/keys.py +1191 -0
- truthound/stores/encryption/pipeline.py +903 -0
- truthound/stores/encryption/providers.py +953 -0
- truthound/stores/encryption/streaming.py +950 -0
- truthound/stores/expectations.py +227 -0
- truthound/stores/factory.py +246 -0
- truthound/stores/migration/__init__.py +75 -0
- truthound/stores/migration/base.py +480 -0
- truthound/stores/migration/manager.py +347 -0
- truthound/stores/migration/registry.py +382 -0
- truthound/stores/migration/store.py +559 -0
- truthound/stores/observability/__init__.py +106 -0
- truthound/stores/observability/audit.py +718 -0
- truthound/stores/observability/config.py +270 -0
- truthound/stores/observability/factory.py +208 -0
- truthound/stores/observability/metrics.py +636 -0
- truthound/stores/observability/protocols.py +410 -0
- truthound/stores/observability/store.py +570 -0
- truthound/stores/observability/tracing.py +784 -0
- truthound/stores/replication/__init__.py +76 -0
- truthound/stores/replication/base.py +260 -0
- truthound/stores/replication/monitor.py +269 -0
- truthound/stores/replication/store.py +439 -0
- truthound/stores/replication/syncer.py +391 -0
- truthound/stores/results.py +359 -0
- truthound/stores/retention/__init__.py +77 -0
- truthound/stores/retention/base.py +378 -0
- truthound/stores/retention/policies.py +621 -0
- truthound/stores/retention/scheduler.py +279 -0
- truthound/stores/retention/store.py +526 -0
- truthound/stores/streaming/__init__.py +138 -0
- truthound/stores/streaming/base.py +801 -0
- truthound/stores/streaming/database.py +984 -0
- truthound/stores/streaming/filesystem.py +719 -0
- truthound/stores/streaming/reader.py +629 -0
- truthound/stores/streaming/s3.py +843 -0
- truthound/stores/streaming/writer.py +790 -0
- truthound/stores/tiering/__init__.py +108 -0
- truthound/stores/tiering/base.py +462 -0
- truthound/stores/tiering/manager.py +249 -0
- truthound/stores/tiering/policies.py +692 -0
- truthound/stores/tiering/store.py +526 -0
- truthound/stores/versioning/__init__.py +56 -0
- truthound/stores/versioning/base.py +376 -0
- truthound/stores/versioning/store.py +660 -0
- truthound/stores/versioning/strategies.py +353 -0
- truthound/types.py +56 -0
- truthound/validators/__init__.py +774 -0
- truthound/validators/aggregate/__init__.py +27 -0
- truthound/validators/aggregate/central.py +116 -0
- truthound/validators/aggregate/extremes.py +116 -0
- truthound/validators/aggregate/spread.py +118 -0
- truthound/validators/aggregate/sum.py +64 -0
- truthound/validators/aggregate/type.py +78 -0
- truthound/validators/anomaly/__init__.py +93 -0
- truthound/validators/anomaly/base.py +431 -0
- truthound/validators/anomaly/ml_based.py +1190 -0
- truthound/validators/anomaly/multivariate.py +647 -0
- truthound/validators/anomaly/statistical.py +599 -0
- truthound/validators/base.py +1089 -0
- truthound/validators/business_rule/__init__.py +46 -0
- truthound/validators/business_rule/base.py +147 -0
- truthound/validators/business_rule/checksum.py +509 -0
- truthound/validators/business_rule/financial.py +526 -0
- truthound/validators/cache.py +733 -0
- truthound/validators/completeness/__init__.py +39 -0
- truthound/validators/completeness/conditional.py +73 -0
- truthound/validators/completeness/default.py +98 -0
- truthound/validators/completeness/empty.py +103 -0
- truthound/validators/completeness/nan.py +337 -0
- truthound/validators/completeness/null.py +152 -0
- truthound/validators/cross_table/__init__.py +17 -0
- truthound/validators/cross_table/aggregate.py +333 -0
- truthound/validators/cross_table/row_count.py +122 -0
- truthound/validators/datetime/__init__.py +29 -0
- truthound/validators/datetime/format.py +78 -0
- truthound/validators/datetime/freshness.py +269 -0
- truthound/validators/datetime/order.py +73 -0
- truthound/validators/datetime/parseable.py +185 -0
- truthound/validators/datetime/range.py +202 -0
- truthound/validators/datetime/timezone.py +69 -0
- truthound/validators/distribution/__init__.py +49 -0
- truthound/validators/distribution/distribution.py +128 -0
- truthound/validators/distribution/monotonic.py +119 -0
- truthound/validators/distribution/outlier.py +178 -0
- truthound/validators/distribution/quantile.py +80 -0
- truthound/validators/distribution/range.py +254 -0
- truthound/validators/distribution/set.py +125 -0
- truthound/validators/distribution/statistical.py +459 -0
- truthound/validators/drift/__init__.py +79 -0
- truthound/validators/drift/base.py +427 -0
- truthound/validators/drift/multi_feature.py +401 -0
- truthound/validators/drift/numeric.py +395 -0
- truthound/validators/drift/psi.py +446 -0
- truthound/validators/drift/statistical.py +510 -0
- truthound/validators/enterprise.py +1658 -0
- truthound/validators/geospatial/__init__.py +80 -0
- truthound/validators/geospatial/base.py +97 -0
- truthound/validators/geospatial/boundary.py +238 -0
- truthound/validators/geospatial/coordinate.py +351 -0
- truthound/validators/geospatial/distance.py +399 -0
- truthound/validators/geospatial/polygon.py +665 -0
- truthound/validators/i18n/__init__.py +308 -0
- truthound/validators/i18n/bidi.py +571 -0
- truthound/validators/i18n/catalogs.py +570 -0
- truthound/validators/i18n/dialects.py +763 -0
- truthound/validators/i18n/extended_catalogs.py +549 -0
- truthound/validators/i18n/formatting.py +1434 -0
- truthound/validators/i18n/loader.py +1020 -0
- truthound/validators/i18n/messages.py +521 -0
- truthound/validators/i18n/plural.py +683 -0
- truthound/validators/i18n/protocols.py +855 -0
- truthound/validators/i18n/tms.py +1162 -0
- truthound/validators/localization/__init__.py +53 -0
- truthound/validators/localization/base.py +122 -0
- truthound/validators/localization/chinese.py +362 -0
- truthound/validators/localization/japanese.py +275 -0
- truthound/validators/localization/korean.py +524 -0
- truthound/validators/memory/__init__.py +94 -0
- truthound/validators/memory/approximate_knn.py +506 -0
- truthound/validators/memory/base.py +547 -0
- truthound/validators/memory/sgd_online.py +719 -0
- truthound/validators/memory/streaming_ecdf.py +753 -0
- truthound/validators/ml_feature/__init__.py +54 -0
- truthound/validators/ml_feature/base.py +249 -0
- truthound/validators/ml_feature/correlation.py +299 -0
- truthound/validators/ml_feature/leakage.py +344 -0
- truthound/validators/ml_feature/null_impact.py +270 -0
- truthound/validators/ml_feature/scale.py +264 -0
- truthound/validators/multi_column/__init__.py +89 -0
- truthound/validators/multi_column/arithmetic.py +284 -0
- truthound/validators/multi_column/base.py +231 -0
- truthound/validators/multi_column/comparison.py +273 -0
- truthound/validators/multi_column/consistency.py +312 -0
- truthound/validators/multi_column/statistical.py +299 -0
- truthound/validators/optimization/__init__.py +164 -0
- truthound/validators/optimization/aggregation.py +563 -0
- truthound/validators/optimization/covariance.py +556 -0
- truthound/validators/optimization/geo.py +626 -0
- truthound/validators/optimization/graph.py +587 -0
- truthound/validators/optimization/orchestrator.py +970 -0
- truthound/validators/optimization/profiling.py +1312 -0
- truthound/validators/privacy/__init__.py +223 -0
- truthound/validators/privacy/base.py +635 -0
- truthound/validators/privacy/ccpa.py +670 -0
- truthound/validators/privacy/gdpr.py +728 -0
- truthound/validators/privacy/global_patterns.py +604 -0
- truthound/validators/privacy/plugins.py +867 -0
- truthound/validators/profiling/__init__.py +52 -0
- truthound/validators/profiling/base.py +175 -0
- truthound/validators/profiling/cardinality.py +312 -0
- truthound/validators/profiling/entropy.py +391 -0
- truthound/validators/profiling/frequency.py +455 -0
- truthound/validators/pushdown_support.py +660 -0
- truthound/validators/query/__init__.py +91 -0
- truthound/validators/query/aggregate.py +346 -0
- truthound/validators/query/base.py +246 -0
- truthound/validators/query/column.py +249 -0
- truthound/validators/query/expression.py +274 -0
- truthound/validators/query/result.py +323 -0
- truthound/validators/query/row_count.py +264 -0
- truthound/validators/referential/__init__.py +80 -0
- truthound/validators/referential/base.py +395 -0
- truthound/validators/referential/cascade.py +391 -0
- truthound/validators/referential/circular.py +563 -0
- truthound/validators/referential/foreign_key.py +624 -0
- truthound/validators/referential/orphan.py +485 -0
- truthound/validators/registry.py +112 -0
- truthound/validators/schema/__init__.py +41 -0
- truthound/validators/schema/column_count.py +142 -0
- truthound/validators/schema/column_exists.py +80 -0
- truthound/validators/schema/column_order.py +82 -0
- truthound/validators/schema/column_pair.py +85 -0
- truthound/validators/schema/column_pair_set.py +195 -0
- truthound/validators/schema/column_type.py +94 -0
- truthound/validators/schema/multi_column.py +53 -0
- truthound/validators/schema/multi_column_aggregate.py +175 -0
- truthound/validators/schema/referential.py +274 -0
- truthound/validators/schema/table_schema.py +91 -0
- truthound/validators/schema_validator.py +219 -0
- truthound/validators/sdk/__init__.py +250 -0
- truthound/validators/sdk/builder.py +680 -0
- truthound/validators/sdk/decorators.py +474 -0
- truthound/validators/sdk/enterprise/__init__.py +211 -0
- truthound/validators/sdk/enterprise/docs.py +725 -0
- truthound/validators/sdk/enterprise/fuzzing.py +659 -0
- truthound/validators/sdk/enterprise/licensing.py +709 -0
- truthound/validators/sdk/enterprise/manager.py +543 -0
- truthound/validators/sdk/enterprise/resources.py +628 -0
- truthound/validators/sdk/enterprise/sandbox.py +766 -0
- truthound/validators/sdk/enterprise/signing.py +603 -0
- truthound/validators/sdk/enterprise/templates.py +865 -0
- truthound/validators/sdk/enterprise/versioning.py +659 -0
- truthound/validators/sdk/templates.py +757 -0
- truthound/validators/sdk/testing.py +807 -0
- truthound/validators/security/__init__.py +181 -0
- truthound/validators/security/redos/__init__.py +182 -0
- truthound/validators/security/redos/core.py +861 -0
- truthound/validators/security/redos/cpu_monitor.py +593 -0
- truthound/validators/security/redos/cve_database.py +791 -0
- truthound/validators/security/redos/ml/__init__.py +155 -0
- truthound/validators/security/redos/ml/base.py +785 -0
- truthound/validators/security/redos/ml/datasets.py +618 -0
- truthound/validators/security/redos/ml/features.py +359 -0
- truthound/validators/security/redos/ml/models.py +1000 -0
- truthound/validators/security/redos/ml/predictor.py +507 -0
- truthound/validators/security/redos/ml/storage.py +632 -0
- truthound/validators/security/redos/ml/training.py +571 -0
- truthound/validators/security/redos/ml_analyzer.py +937 -0
- truthound/validators/security/redos/optimizer.py +674 -0
- truthound/validators/security/redos/profiler.py +682 -0
- truthound/validators/security/redos/re2_engine.py +709 -0
- truthound/validators/security/redos.py +886 -0
- truthound/validators/security/sql_security.py +1247 -0
- truthound/validators/streaming/__init__.py +126 -0
- truthound/validators/streaming/base.py +292 -0
- truthound/validators/streaming/completeness.py +210 -0
- truthound/validators/streaming/mixin.py +575 -0
- truthound/validators/streaming/range.py +308 -0
- truthound/validators/streaming/sources.py +846 -0
- truthound/validators/string/__init__.py +57 -0
- truthound/validators/string/casing.py +158 -0
- truthound/validators/string/charset.py +96 -0
- truthound/validators/string/format.py +501 -0
- truthound/validators/string/json.py +77 -0
- truthound/validators/string/json_schema.py +184 -0
- truthound/validators/string/length.py +104 -0
- truthound/validators/string/like_pattern.py +237 -0
- truthound/validators/string/regex.py +202 -0
- truthound/validators/string/regex_extended.py +435 -0
- truthound/validators/table/__init__.py +88 -0
- truthound/validators/table/base.py +78 -0
- truthound/validators/table/column_count.py +198 -0
- truthound/validators/table/freshness.py +362 -0
- truthound/validators/table/row_count.py +251 -0
- truthound/validators/table/schema.py +333 -0
- truthound/validators/table/size.py +285 -0
- truthound/validators/timeout/__init__.py +102 -0
- truthound/validators/timeout/advanced/__init__.py +247 -0
- truthound/validators/timeout/advanced/circuit_breaker.py +675 -0
- truthound/validators/timeout/advanced/prediction.py +773 -0
- truthound/validators/timeout/advanced/priority.py +618 -0
- truthound/validators/timeout/advanced/redis_backend.py +770 -0
- truthound/validators/timeout/advanced/retry.py +721 -0
- truthound/validators/timeout/advanced/sampling.py +788 -0
- truthound/validators/timeout/advanced/sla.py +661 -0
- truthound/validators/timeout/advanced/telemetry.py +804 -0
- truthound/validators/timeout/cascade.py +477 -0
- truthound/validators/timeout/deadline.py +657 -0
- truthound/validators/timeout/degradation.py +525 -0
- truthound/validators/timeout/distributed.py +597 -0
- truthound/validators/timeseries/__init__.py +89 -0
- truthound/validators/timeseries/base.py +326 -0
- truthound/validators/timeseries/completeness.py +617 -0
- truthound/validators/timeseries/gap.py +485 -0
- truthound/validators/timeseries/monotonic.py +310 -0
- truthound/validators/timeseries/seasonality.py +422 -0
- truthound/validators/timeseries/trend.py +510 -0
- truthound/validators/uniqueness/__init__.py +59 -0
- truthound/validators/uniqueness/approximate.py +475 -0
- truthound/validators/uniqueness/distinct_values.py +253 -0
- truthound/validators/uniqueness/duplicate.py +118 -0
- truthound/validators/uniqueness/primary_key.py +140 -0
- truthound/validators/uniqueness/unique.py +191 -0
- truthound/validators/uniqueness/within_record.py +599 -0
- truthound/validators/utils.py +756 -0
- truthound-1.0.8.dist-info/METADATA +474 -0
- truthound-1.0.8.dist-info/RECORD +877 -0
- truthound-1.0.8.dist-info/WHEEL +4 -0
- truthound-1.0.8.dist-info/entry_points.txt +2 -0
- truthound-1.0.8.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,1132 @@
|
|
|
1
|
+
"""Enterprise data encryption system for Truthound.
|
|
2
|
+
|
|
3
|
+
This module extends the base encryption system with enterprise features:
|
|
4
|
+
- At-rest encryption for validation results
|
|
5
|
+
- Field-level encryption for sensitive columns
|
|
6
|
+
- Cloud KMS integration (AWS, GCP, Azure, Vault)
|
|
7
|
+
- Key rotation and lifecycle management
|
|
8
|
+
|
|
9
|
+
Architecture:
|
|
10
|
+
AtRestEncryption
|
|
11
|
+
|
|
|
12
|
+
+---> Local encryption (AES-256-GCM)
|
|
13
|
+
+---> Cloud KMS wrapping
|
|
14
|
+
|
|
|
15
|
+
FieldLevelEncryption
|
|
16
|
+
|
|
|
17
|
+
+---> Per-column encryption policies
|
|
18
|
+
+---> Format-preserving encryption
|
|
19
|
+
|
|
|
20
|
+
KeyProvider
|
|
21
|
+
|
|
|
22
|
+
+---> VaultKeyProvider (HashiCorp Vault)
|
|
23
|
+
+---> AwsKmsProvider (AWS KMS)
|
|
24
|
+
+---> GcpKmsProvider (Google Cloud KMS)
|
|
25
|
+
+---> AzureKeyVaultProvider (Azure Key Vault)
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
>>> from truthound.infrastructure.encryption import (
|
|
29
|
+
... get_encryptor, configure_encryption,
|
|
30
|
+
... AtRestEncryption, FieldLevelEncryption,
|
|
31
|
+
... )
|
|
32
|
+
>>>
|
|
33
|
+
>>> # Configure encryption
|
|
34
|
+
>>> configure_encryption(
|
|
35
|
+
... provider="aws_kms",
|
|
36
|
+
... key_id="alias/truthound-data-key",
|
|
37
|
+
... region="us-east-1",
|
|
38
|
+
... )
|
|
39
|
+
>>>
|
|
40
|
+
>>> # Encrypt data at rest
|
|
41
|
+
>>> encryptor = get_encryptor()
|
|
42
|
+
>>> encrypted = encryptor.encrypt(sensitive_data)
|
|
43
|
+
>>>
|
|
44
|
+
>>> # Field-level encryption
|
|
45
|
+
>>> field_enc = FieldLevelEncryption(
|
|
46
|
+
... policies={
|
|
47
|
+
... "ssn": {"algorithm": "format_preserving"},
|
|
48
|
+
... "email": {"algorithm": "aes_gcm"},
|
|
49
|
+
... }
|
|
50
|
+
... )
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
import base64
|
|
56
|
+
import hashlib
|
|
57
|
+
import json
|
|
58
|
+
import os
|
|
59
|
+
import struct
|
|
60
|
+
import threading
|
|
61
|
+
import time
|
|
62
|
+
from abc import ABC, abstractmethod
|
|
63
|
+
from contextlib import contextmanager
|
|
64
|
+
from dataclasses import dataclass, field
|
|
65
|
+
from datetime import datetime, timedelta, timezone
|
|
66
|
+
from enum import Enum
|
|
67
|
+
from pathlib import Path
|
|
68
|
+
from typing import Any, Callable, Iterator, TypeVar
|
|
69
|
+
|
|
70
|
+
# Re-export base encryption components
|
|
71
|
+
from truthound.stores.encryption import (
|
|
72
|
+
EncryptionAlgorithm,
|
|
73
|
+
EncryptionConfig,
|
|
74
|
+
EncryptionError,
|
|
75
|
+
DecryptionError,
|
|
76
|
+
generate_key,
|
|
77
|
+
generate_nonce,
|
|
78
|
+
get_encryptor as get_base_encryptor,
|
|
79
|
+
AesGcmEncryptor,
|
|
80
|
+
ChaCha20Poly1305Encryptor,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# =============================================================================
|
|
85
|
+
# Key Provider Protocol
|
|
86
|
+
# =============================================================================
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class KeyProvider(ABC):
|
|
90
|
+
"""Abstract base class for key providers.
|
|
91
|
+
|
|
92
|
+
Key providers supply encryption keys from various sources:
|
|
93
|
+
- Cloud KMS (AWS, GCP, Azure)
|
|
94
|
+
- HashiCorp Vault
|
|
95
|
+
- Local key stores
|
|
96
|
+
|
|
97
|
+
The provider handles key retrieval, caching, and rotation.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
def get_key(self, key_id: str) -> bytes:
|
|
102
|
+
"""Get encryption key by ID.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
key_id: Key identifier.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Raw key bytes.
|
|
109
|
+
"""
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
@abstractmethod
|
|
113
|
+
def encrypt_data_key(self, data_key: bytes, key_id: str) -> bytes:
|
|
114
|
+
"""Encrypt a data key with a master key (envelope encryption).
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
data_key: Plain data key.
|
|
118
|
+
key_id: Master key ID.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Encrypted data key.
|
|
122
|
+
"""
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def decrypt_data_key(self, encrypted_key: bytes, key_id: str) -> bytes:
|
|
127
|
+
"""Decrypt a data key with a master key.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
encrypted_key: Encrypted data key.
|
|
131
|
+
key_id: Master key ID.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Plain data key.
|
|
135
|
+
"""
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
def generate_data_key(self, key_id: str, key_length: int = 32) -> tuple[bytes, bytes]:
|
|
139
|
+
"""Generate a new data key and encrypt it.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
key_id: Master key ID for wrapping.
|
|
143
|
+
key_length: Key length in bytes.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Tuple of (plain_key, encrypted_key).
|
|
147
|
+
"""
|
|
148
|
+
plain_key = os.urandom(key_length)
|
|
149
|
+
encrypted_key = self.encrypt_data_key(plain_key, key_id)
|
|
150
|
+
return plain_key, encrypted_key
|
|
151
|
+
|
|
152
|
+
def close(self) -> None:
|
|
153
|
+
"""Clean up provider resources."""
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# =============================================================================
|
|
158
|
+
# Cloud Key Providers
|
|
159
|
+
# =============================================================================
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class VaultKeyProvider(KeyProvider):
|
|
163
|
+
"""HashiCorp Vault key provider.
|
|
164
|
+
|
|
165
|
+
Uses Vault's Transit secrets engine for key management.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def __init__(
|
|
169
|
+
self,
|
|
170
|
+
url: str,
|
|
171
|
+
*,
|
|
172
|
+
token: str | None = None,
|
|
173
|
+
mount_point: str = "transit",
|
|
174
|
+
cache_ttl: float = 300.0,
|
|
175
|
+
) -> None:
|
|
176
|
+
"""Initialize Vault provider.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
url: Vault server URL.
|
|
180
|
+
token: Vault token (or VAULT_TOKEN env var).
|
|
181
|
+
mount_point: Transit engine mount point.
|
|
182
|
+
cache_ttl: Key cache TTL in seconds.
|
|
183
|
+
"""
|
|
184
|
+
self._url = url.rstrip("/")
|
|
185
|
+
self._token = token or os.getenv("VAULT_TOKEN", "")
|
|
186
|
+
self._mount_point = mount_point
|
|
187
|
+
self._cache_ttl = cache_ttl
|
|
188
|
+
self._cache: dict[str, tuple[bytes, float]] = {}
|
|
189
|
+
self._lock = threading.Lock()
|
|
190
|
+
|
|
191
|
+
def get_key(self, key_id: str) -> bytes:
|
|
192
|
+
"""Get key from Vault (uses transit for wrapping only)."""
|
|
193
|
+
# For Vault transit, we generate local data keys and use transit for wrapping
|
|
194
|
+
# This returns a locally generated key that will be wrapped by Vault
|
|
195
|
+
return os.urandom(32)
|
|
196
|
+
|
|
197
|
+
def encrypt_data_key(self, data_key: bytes, key_id: str) -> bytes:
|
|
198
|
+
"""Encrypt data key using Vault transit."""
|
|
199
|
+
try:
|
|
200
|
+
import urllib.request
|
|
201
|
+
|
|
202
|
+
# Base64 encode the data key
|
|
203
|
+
plaintext_b64 = base64.b64encode(data_key).decode("utf-8")
|
|
204
|
+
|
|
205
|
+
url = f"{self._url}/v1/{self._mount_point}/encrypt/{key_id}"
|
|
206
|
+
payload = json.dumps({"plaintext": plaintext_b64}).encode("utf-8")
|
|
207
|
+
|
|
208
|
+
request = urllib.request.Request(
|
|
209
|
+
url,
|
|
210
|
+
data=payload,
|
|
211
|
+
headers={
|
|
212
|
+
"X-Vault-Token": self._token,
|
|
213
|
+
"Content-Type": "application/json",
|
|
214
|
+
},
|
|
215
|
+
method="POST",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
219
|
+
data = json.loads(response.read().decode("utf-8"))
|
|
220
|
+
ciphertext = data["data"]["ciphertext"]
|
|
221
|
+
return ciphertext.encode("utf-8")
|
|
222
|
+
|
|
223
|
+
except Exception as e:
|
|
224
|
+
raise EncryptionError(f"Vault encrypt failed: {e}")
|
|
225
|
+
|
|
226
|
+
def decrypt_data_key(self, encrypted_key: bytes, key_id: str) -> bytes:
|
|
227
|
+
"""Decrypt data key using Vault transit."""
|
|
228
|
+
try:
|
|
229
|
+
import urllib.request
|
|
230
|
+
|
|
231
|
+
ciphertext = encrypted_key.decode("utf-8")
|
|
232
|
+
|
|
233
|
+
url = f"{self._url}/v1/{self._mount_point}/decrypt/{key_id}"
|
|
234
|
+
payload = json.dumps({"ciphertext": ciphertext}).encode("utf-8")
|
|
235
|
+
|
|
236
|
+
request = urllib.request.Request(
|
|
237
|
+
url,
|
|
238
|
+
data=payload,
|
|
239
|
+
headers={
|
|
240
|
+
"X-Vault-Token": self._token,
|
|
241
|
+
"Content-Type": "application/json",
|
|
242
|
+
},
|
|
243
|
+
method="POST",
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
247
|
+
data = json.loads(response.read().decode("utf-8"))
|
|
248
|
+
plaintext_b64 = data["data"]["plaintext"]
|
|
249
|
+
return base64.b64decode(plaintext_b64)
|
|
250
|
+
|
|
251
|
+
except Exception as e:
|
|
252
|
+
raise DecryptionError(f"Vault decrypt failed: {e}")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class AwsKmsProvider(KeyProvider):
|
|
256
|
+
"""AWS KMS key provider.
|
|
257
|
+
|
|
258
|
+
Uses AWS KMS for key management and envelope encryption.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
def __init__(
|
|
262
|
+
self,
|
|
263
|
+
key_id: str,
|
|
264
|
+
*,
|
|
265
|
+
region: str | None = None,
|
|
266
|
+
cache_ttl: float = 300.0,
|
|
267
|
+
) -> None:
|
|
268
|
+
"""Initialize AWS KMS provider.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
key_id: KMS key ID or alias (e.g., alias/my-key).
|
|
272
|
+
region: AWS region.
|
|
273
|
+
cache_ttl: Key cache TTL.
|
|
274
|
+
"""
|
|
275
|
+
self._key_id = key_id
|
|
276
|
+
self._region = region or os.getenv("AWS_REGION", "us-east-1")
|
|
277
|
+
self._cache_ttl = cache_ttl
|
|
278
|
+
self._client = None
|
|
279
|
+
self._lock = threading.Lock()
|
|
280
|
+
|
|
281
|
+
def _get_client(self) -> Any:
|
|
282
|
+
"""Get or create KMS client."""
|
|
283
|
+
if self._client is None:
|
|
284
|
+
try:
|
|
285
|
+
import boto3
|
|
286
|
+
|
|
287
|
+
self._client = boto3.client("kms", region_name=self._region)
|
|
288
|
+
except ImportError:
|
|
289
|
+
raise EncryptionError("boto3 not installed")
|
|
290
|
+
return self._client
|
|
291
|
+
|
|
292
|
+
def get_key(self, key_id: str) -> bytes:
|
|
293
|
+
"""Generate data key from KMS."""
|
|
294
|
+
plain_key, _ = self.generate_data_key(key_id)
|
|
295
|
+
return plain_key
|
|
296
|
+
|
|
297
|
+
def encrypt_data_key(self, data_key: bytes, key_id: str) -> bytes:
|
|
298
|
+
"""Encrypt data key with KMS."""
|
|
299
|
+
try:
|
|
300
|
+
client = self._get_client()
|
|
301
|
+
response = client.encrypt(
|
|
302
|
+
KeyId=key_id or self._key_id,
|
|
303
|
+
Plaintext=data_key,
|
|
304
|
+
)
|
|
305
|
+
return response["CiphertextBlob"]
|
|
306
|
+
except Exception as e:
|
|
307
|
+
raise EncryptionError(f"AWS KMS encrypt failed: {e}")
|
|
308
|
+
|
|
309
|
+
def decrypt_data_key(self, encrypted_key: bytes, key_id: str) -> bytes:
|
|
310
|
+
"""Decrypt data key with KMS."""
|
|
311
|
+
try:
|
|
312
|
+
client = self._get_client()
|
|
313
|
+
response = client.decrypt(
|
|
314
|
+
KeyId=key_id or self._key_id,
|
|
315
|
+
CiphertextBlob=encrypted_key,
|
|
316
|
+
)
|
|
317
|
+
return response["Plaintext"]
|
|
318
|
+
except Exception as e:
|
|
319
|
+
raise DecryptionError(f"AWS KMS decrypt failed: {e}")
|
|
320
|
+
|
|
321
|
+
def generate_data_key(self, key_id: str, key_length: int = 32) -> tuple[bytes, bytes]:
|
|
322
|
+
"""Generate data key using KMS GenerateDataKey."""
|
|
323
|
+
try:
|
|
324
|
+
client = self._get_client()
|
|
325
|
+
key_spec = "AES_256" if key_length == 32 else "AES_128"
|
|
326
|
+
response = client.generate_data_key(
|
|
327
|
+
KeyId=key_id or self._key_id,
|
|
328
|
+
KeySpec=key_spec,
|
|
329
|
+
)
|
|
330
|
+
return response["Plaintext"], response["CiphertextBlob"]
|
|
331
|
+
except Exception as e:
|
|
332
|
+
raise EncryptionError(f"AWS KMS generate data key failed: {e}")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class GcpKmsProvider(KeyProvider):
|
|
336
|
+
"""Google Cloud KMS key provider.
|
|
337
|
+
|
|
338
|
+
Uses Google Cloud KMS for key management.
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
def __init__(
|
|
342
|
+
self,
|
|
343
|
+
key_name: str,
|
|
344
|
+
*,
|
|
345
|
+
project_id: str | None = None,
|
|
346
|
+
location: str = "global",
|
|
347
|
+
key_ring: str = "truthound",
|
|
348
|
+
) -> None:
|
|
349
|
+
"""Initialize GCP KMS provider.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
key_name: KMS key name.
|
|
353
|
+
project_id: GCP project ID.
|
|
354
|
+
location: KMS location.
|
|
355
|
+
key_ring: Key ring name.
|
|
356
|
+
"""
|
|
357
|
+
self._key_name = key_name
|
|
358
|
+
self._project_id = project_id or os.getenv("GCP_PROJECT_ID", "")
|
|
359
|
+
self._location = location
|
|
360
|
+
self._key_ring = key_ring
|
|
361
|
+
self._client = None
|
|
362
|
+
|
|
363
|
+
def _get_client(self) -> Any:
|
|
364
|
+
"""Get or create KMS client."""
|
|
365
|
+
if self._client is None:
|
|
366
|
+
try:
|
|
367
|
+
from google.cloud import kms
|
|
368
|
+
|
|
369
|
+
self._client = kms.KeyManagementServiceClient()
|
|
370
|
+
except ImportError:
|
|
371
|
+
raise EncryptionError("google-cloud-kms not installed")
|
|
372
|
+
return self._client
|
|
373
|
+
|
|
374
|
+
def _get_key_path(self, key_id: str) -> str:
|
|
375
|
+
"""Get full key path."""
|
|
376
|
+
key = key_id or self._key_name
|
|
377
|
+
return f"projects/{self._project_id}/locations/{self._location}/keyRings/{self._key_ring}/cryptoKeys/{key}"
|
|
378
|
+
|
|
379
|
+
def get_key(self, key_id: str) -> bytes:
|
|
380
|
+
"""Generate data key."""
|
|
381
|
+
plain_key, _ = self.generate_data_key(key_id)
|
|
382
|
+
return plain_key
|
|
383
|
+
|
|
384
|
+
def encrypt_data_key(self, data_key: bytes, key_id: str) -> bytes:
|
|
385
|
+
"""Encrypt data key with GCP KMS."""
|
|
386
|
+
try:
|
|
387
|
+
client = self._get_client()
|
|
388
|
+
key_path = self._get_key_path(key_id)
|
|
389
|
+
response = client.encrypt(
|
|
390
|
+
request={"name": key_path, "plaintext": data_key}
|
|
391
|
+
)
|
|
392
|
+
return response.ciphertext
|
|
393
|
+
except Exception as e:
|
|
394
|
+
raise EncryptionError(f"GCP KMS encrypt failed: {e}")
|
|
395
|
+
|
|
396
|
+
def decrypt_data_key(self, encrypted_key: bytes, key_id: str) -> bytes:
|
|
397
|
+
"""Decrypt data key with GCP KMS."""
|
|
398
|
+
try:
|
|
399
|
+
client = self._get_client()
|
|
400
|
+
key_path = self._get_key_path(key_id)
|
|
401
|
+
response = client.decrypt(
|
|
402
|
+
request={"name": key_path, "ciphertext": encrypted_key}
|
|
403
|
+
)
|
|
404
|
+
return response.plaintext
|
|
405
|
+
except Exception as e:
|
|
406
|
+
raise DecryptionError(f"GCP KMS decrypt failed: {e}")
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class AzureKeyVaultProvider(KeyProvider):
|
|
410
|
+
"""Azure Key Vault key provider.
|
|
411
|
+
|
|
412
|
+
Uses Azure Key Vault for key management.
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
def __init__(
|
|
416
|
+
self,
|
|
417
|
+
vault_url: str,
|
|
418
|
+
key_name: str,
|
|
419
|
+
*,
|
|
420
|
+
credential: Any = None,
|
|
421
|
+
) -> None:
|
|
422
|
+
"""Initialize Azure Key Vault provider.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
vault_url: Key Vault URL.
|
|
426
|
+
key_name: Key name.
|
|
427
|
+
credential: Azure credential (DefaultAzureCredential if None).
|
|
428
|
+
"""
|
|
429
|
+
self._vault_url = vault_url
|
|
430
|
+
self._key_name = key_name
|
|
431
|
+
self._credential = credential
|
|
432
|
+
self._client = None
|
|
433
|
+
self._crypto_client = None
|
|
434
|
+
|
|
435
|
+
def _get_clients(self) -> tuple[Any, Any]:
|
|
436
|
+
"""Get or create Key Vault clients."""
|
|
437
|
+
if self._client is None:
|
|
438
|
+
try:
|
|
439
|
+
from azure.identity import DefaultAzureCredential
|
|
440
|
+
from azure.keyvault.keys import KeyClient
|
|
441
|
+
from azure.keyvault.keys.crypto import CryptographyClient
|
|
442
|
+
|
|
443
|
+
credential = self._credential or DefaultAzureCredential()
|
|
444
|
+
self._client = KeyClient(
|
|
445
|
+
vault_url=self._vault_url,
|
|
446
|
+
credential=credential,
|
|
447
|
+
)
|
|
448
|
+
key = self._client.get_key(self._key_name)
|
|
449
|
+
self._crypto_client = CryptographyClient(key, credential=credential)
|
|
450
|
+
except ImportError:
|
|
451
|
+
raise EncryptionError("azure-keyvault-keys not installed")
|
|
452
|
+
|
|
453
|
+
return self._client, self._crypto_client
|
|
454
|
+
|
|
455
|
+
def get_key(self, key_id: str) -> bytes:
|
|
456
|
+
"""Generate data key."""
|
|
457
|
+
return os.urandom(32)
|
|
458
|
+
|
|
459
|
+
def encrypt_data_key(self, data_key: bytes, key_id: str) -> bytes:
|
|
460
|
+
"""Encrypt data key with Azure Key Vault."""
|
|
461
|
+
try:
|
|
462
|
+
from azure.keyvault.keys.crypto import EncryptionAlgorithm as AzureAlgorithm
|
|
463
|
+
|
|
464
|
+
_, crypto_client = self._get_clients()
|
|
465
|
+
result = crypto_client.encrypt(AzureAlgorithm.rsa_oaep, data_key)
|
|
466
|
+
return result.ciphertext
|
|
467
|
+
except Exception as e:
|
|
468
|
+
raise EncryptionError(f"Azure Key Vault encrypt failed: {e}")
|
|
469
|
+
|
|
470
|
+
def decrypt_data_key(self, encrypted_key: bytes, key_id: str) -> bytes:
|
|
471
|
+
"""Decrypt data key with Azure Key Vault."""
|
|
472
|
+
try:
|
|
473
|
+
from azure.keyvault.keys.crypto import EncryptionAlgorithm as AzureAlgorithm
|
|
474
|
+
|
|
475
|
+
_, crypto_client = self._get_clients()
|
|
476
|
+
result = crypto_client.decrypt(AzureAlgorithm.rsa_oaep, encrypted_key)
|
|
477
|
+
return result.plaintext
|
|
478
|
+
except Exception as e:
|
|
479
|
+
raise DecryptionError(f"Azure Key Vault decrypt failed: {e}")
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
# =============================================================================
|
|
483
|
+
# Local Key Provider
|
|
484
|
+
# =============================================================================
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
class LocalKeyProvider(KeyProvider):
|
|
488
|
+
"""Local key provider for development/testing.
|
|
489
|
+
|
|
490
|
+
Stores keys locally using password-based encryption.
|
|
491
|
+
NOT recommended for production use.
|
|
492
|
+
"""
|
|
493
|
+
|
|
494
|
+
def __init__(
|
|
495
|
+
self,
|
|
496
|
+
key_file: str | Path = ".truthound_keys",
|
|
497
|
+
*,
|
|
498
|
+
master_password: str | None = None,
|
|
499
|
+
) -> None:
|
|
500
|
+
"""Initialize local key provider.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
key_file: Path to key storage file.
|
|
504
|
+
master_password: Master password (or TRUTHOUND_MASTER_KEY env).
|
|
505
|
+
"""
|
|
506
|
+
self._key_file = Path(key_file)
|
|
507
|
+
self._master_password = master_password or os.getenv("TRUTHOUND_MASTER_KEY", "")
|
|
508
|
+
self._keys: dict[str, bytes] = {}
|
|
509
|
+
self._lock = threading.Lock()
|
|
510
|
+
self._load_keys()
|
|
511
|
+
|
|
512
|
+
def _load_keys(self) -> None:
|
|
513
|
+
"""Load keys from file."""
|
|
514
|
+
if not self._key_file.exists():
|
|
515
|
+
return
|
|
516
|
+
|
|
517
|
+
try:
|
|
518
|
+
content = self._key_file.read_bytes()
|
|
519
|
+
# Simple XOR with master key hash for obfuscation (not secure!)
|
|
520
|
+
key_hash = hashlib.sha256(self._master_password.encode()).digest()
|
|
521
|
+
decrypted = bytes(b ^ key_hash[i % 32] for i, b in enumerate(content))
|
|
522
|
+
self._keys = json.loads(decrypted.decode("utf-8"))
|
|
523
|
+
# Convert hex strings back to bytes
|
|
524
|
+
self._keys = {k: bytes.fromhex(v) for k, v in self._keys.items()}
|
|
525
|
+
except Exception:
|
|
526
|
+
self._keys = {}
|
|
527
|
+
|
|
528
|
+
def _save_keys(self) -> None:
|
|
529
|
+
"""Save keys to file."""
|
|
530
|
+
try:
|
|
531
|
+
# Convert bytes to hex for JSON
|
|
532
|
+
keys_hex = {k: v.hex() for k, v in self._keys.items()}
|
|
533
|
+
content = json.dumps(keys_hex).encode("utf-8")
|
|
534
|
+
# Simple XOR with master key hash
|
|
535
|
+
key_hash = hashlib.sha256(self._master_password.encode()).digest()
|
|
536
|
+
encrypted = bytes(b ^ key_hash[i % 32] for i, b in enumerate(content))
|
|
537
|
+
self._key_file.write_bytes(encrypted)
|
|
538
|
+
except Exception:
|
|
539
|
+
pass
|
|
540
|
+
|
|
541
|
+
def get_key(self, key_id: str) -> bytes:
|
|
542
|
+
"""Get or generate key."""
|
|
543
|
+
with self._lock:
|
|
544
|
+
if key_id not in self._keys:
|
|
545
|
+
self._keys[key_id] = os.urandom(32)
|
|
546
|
+
self._save_keys()
|
|
547
|
+
return self._keys[key_id]
|
|
548
|
+
|
|
549
|
+
def encrypt_data_key(self, data_key: bytes, key_id: str) -> bytes:
|
|
550
|
+
"""Encrypt data key with master key."""
|
|
551
|
+
master_key = self.get_key(key_id)
|
|
552
|
+
encryptor = AesGcmEncryptor()
|
|
553
|
+
return encryptor.encrypt(data_key, master_key)
|
|
554
|
+
|
|
555
|
+
def decrypt_data_key(self, encrypted_key: bytes, key_id: str) -> bytes:
|
|
556
|
+
"""Decrypt data key with master key."""
|
|
557
|
+
master_key = self.get_key(key_id)
|
|
558
|
+
encryptor = AesGcmEncryptor()
|
|
559
|
+
return encryptor.decrypt(encrypted_key, master_key)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
# =============================================================================
|
|
563
|
+
# At-Rest Encryption
|
|
564
|
+
# =============================================================================
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
@dataclass
|
|
568
|
+
class EncryptedData:
|
|
569
|
+
"""Encrypted data with metadata.
|
|
570
|
+
|
|
571
|
+
Attributes:
|
|
572
|
+
ciphertext: Encrypted data.
|
|
573
|
+
encrypted_key: Wrapped data encryption key.
|
|
574
|
+
key_id: Master key ID used for wrapping.
|
|
575
|
+
algorithm: Encryption algorithm used.
|
|
576
|
+
nonce: Nonce/IV used for encryption.
|
|
577
|
+
timestamp: Encryption timestamp.
|
|
578
|
+
metadata: Additional metadata.
|
|
579
|
+
"""
|
|
580
|
+
|
|
581
|
+
ciphertext: bytes
|
|
582
|
+
encrypted_key: bytes
|
|
583
|
+
key_id: str
|
|
584
|
+
algorithm: str = "AES-256-GCM"
|
|
585
|
+
nonce: bytes = b""
|
|
586
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
587
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
588
|
+
|
|
589
|
+
def to_bytes(self) -> bytes:
|
|
590
|
+
"""Serialize to bytes."""
|
|
591
|
+
header = {
|
|
592
|
+
"key_id": self.key_id,
|
|
593
|
+
"algorithm": self.algorithm,
|
|
594
|
+
"timestamp": self.timestamp.isoformat(),
|
|
595
|
+
"metadata": self.metadata,
|
|
596
|
+
}
|
|
597
|
+
header_json = json.dumps(header).encode("utf-8")
|
|
598
|
+
|
|
599
|
+
# Format: header_len (4) | header | nonce_len (1) | nonce | key_len (2) | encrypted_key | ciphertext
|
|
600
|
+
parts = [
|
|
601
|
+
struct.pack(">I", len(header_json)),
|
|
602
|
+
header_json,
|
|
603
|
+
struct.pack(">B", len(self.nonce)),
|
|
604
|
+
self.nonce,
|
|
605
|
+
struct.pack(">H", len(self.encrypted_key)),
|
|
606
|
+
self.encrypted_key,
|
|
607
|
+
self.ciphertext,
|
|
608
|
+
]
|
|
609
|
+
return b"".join(parts)
|
|
610
|
+
|
|
611
|
+
@classmethod
|
|
612
|
+
def from_bytes(cls, data: bytes) -> "EncryptedData":
|
|
613
|
+
"""Deserialize from bytes."""
|
|
614
|
+
offset = 0
|
|
615
|
+
|
|
616
|
+
# Read header
|
|
617
|
+
header_len = struct.unpack(">I", data[offset : offset + 4])[0]
|
|
618
|
+
offset += 4
|
|
619
|
+
header = json.loads(data[offset : offset + header_len].decode("utf-8"))
|
|
620
|
+
offset += header_len
|
|
621
|
+
|
|
622
|
+
# Read nonce
|
|
623
|
+
nonce_len = struct.unpack(">B", data[offset : offset + 1])[0]
|
|
624
|
+
offset += 1
|
|
625
|
+
nonce = data[offset : offset + nonce_len]
|
|
626
|
+
offset += nonce_len
|
|
627
|
+
|
|
628
|
+
# Read encrypted key
|
|
629
|
+
key_len = struct.unpack(">H", data[offset : offset + 2])[0]
|
|
630
|
+
offset += 2
|
|
631
|
+
encrypted_key = data[offset : offset + key_len]
|
|
632
|
+
offset += key_len
|
|
633
|
+
|
|
634
|
+
# Rest is ciphertext
|
|
635
|
+
ciphertext = data[offset:]
|
|
636
|
+
|
|
637
|
+
return cls(
|
|
638
|
+
ciphertext=ciphertext,
|
|
639
|
+
encrypted_key=encrypted_key,
|
|
640
|
+
key_id=header["key_id"],
|
|
641
|
+
algorithm=header["algorithm"],
|
|
642
|
+
nonce=nonce,
|
|
643
|
+
timestamp=datetime.fromisoformat(header["timestamp"]),
|
|
644
|
+
metadata=header.get("metadata", {}),
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
class AtRestEncryption:
|
|
649
|
+
"""At-rest encryption for data files.
|
|
650
|
+
|
|
651
|
+
Provides envelope encryption using a cloud KMS or local key provider.
|
|
652
|
+
Data is encrypted with a unique data encryption key (DEK), which is
|
|
653
|
+
then wrapped with a key encryption key (KEK) from the provider.
|
|
654
|
+
|
|
655
|
+
Example:
|
|
656
|
+
>>> encryptor = AtRestEncryption(
|
|
657
|
+
... provider=AwsKmsProvider("alias/my-key"),
|
|
658
|
+
... key_id="alias/my-key",
|
|
659
|
+
... )
|
|
660
|
+
>>>
|
|
661
|
+
>>> # Encrypt
|
|
662
|
+
>>> encrypted = encryptor.encrypt(b"sensitive data")
|
|
663
|
+
>>>
|
|
664
|
+
>>> # Decrypt
|
|
665
|
+
>>> decrypted = encryptor.decrypt(encrypted)
|
|
666
|
+
"""
|
|
667
|
+
|
|
668
|
+
def __init__(
|
|
669
|
+
self,
|
|
670
|
+
provider: KeyProvider,
|
|
671
|
+
key_id: str,
|
|
672
|
+
*,
|
|
673
|
+
algorithm: EncryptionAlgorithm = EncryptionAlgorithm.AES_256_GCM,
|
|
674
|
+
) -> None:
|
|
675
|
+
"""Initialize at-rest encryption.
|
|
676
|
+
|
|
677
|
+
Args:
|
|
678
|
+
provider: Key provider.
|
|
679
|
+
key_id: Default key ID for encryption.
|
|
680
|
+
algorithm: Encryption algorithm.
|
|
681
|
+
"""
|
|
682
|
+
self._provider = provider
|
|
683
|
+
self._key_id = key_id
|
|
684
|
+
self._algorithm = algorithm
|
|
685
|
+
|
|
686
|
+
# Get encryptor for the algorithm
|
|
687
|
+
if algorithm == EncryptionAlgorithm.AES_256_GCM:
|
|
688
|
+
self._encryptor = AesGcmEncryptor()
|
|
689
|
+
elif algorithm == EncryptionAlgorithm.CHACHA20_POLY1305:
|
|
690
|
+
self._encryptor = ChaCha20Poly1305Encryptor()
|
|
691
|
+
else:
|
|
692
|
+
self._encryptor = AesGcmEncryptor()
|
|
693
|
+
|
|
694
|
+
def encrypt(
|
|
695
|
+
self,
|
|
696
|
+
data: bytes,
|
|
697
|
+
*,
|
|
698
|
+
key_id: str | None = None,
|
|
699
|
+
metadata: dict[str, Any] | None = None,
|
|
700
|
+
) -> EncryptedData:
|
|
701
|
+
"""Encrypt data.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
data: Data to encrypt.
|
|
705
|
+
key_id: Key ID (uses default if None).
|
|
706
|
+
metadata: Additional metadata.
|
|
707
|
+
|
|
708
|
+
Returns:
|
|
709
|
+
EncryptedData with ciphertext and wrapped key.
|
|
710
|
+
"""
|
|
711
|
+
key_id = key_id or self._key_id
|
|
712
|
+
|
|
713
|
+
# Generate data key
|
|
714
|
+
plain_key, encrypted_key = self._provider.generate_data_key(key_id)
|
|
715
|
+
|
|
716
|
+
# Generate nonce
|
|
717
|
+
nonce = generate_nonce(self._algorithm)
|
|
718
|
+
|
|
719
|
+
# Encrypt data
|
|
720
|
+
ciphertext = self._encryptor.encrypt(data, plain_key, nonce)
|
|
721
|
+
|
|
722
|
+
# Clear plain key from memory
|
|
723
|
+
plain_key = b"\x00" * len(plain_key)
|
|
724
|
+
|
|
725
|
+
return EncryptedData(
|
|
726
|
+
ciphertext=ciphertext,
|
|
727
|
+
encrypted_key=encrypted_key,
|
|
728
|
+
key_id=key_id,
|
|
729
|
+
algorithm=self._algorithm.value,
|
|
730
|
+
nonce=nonce,
|
|
731
|
+
metadata=metadata or {},
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
def decrypt(self, encrypted: EncryptedData) -> bytes:
|
|
735
|
+
"""Decrypt data.
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
encrypted: Encrypted data.
|
|
739
|
+
|
|
740
|
+
Returns:
|
|
741
|
+
Decrypted data.
|
|
742
|
+
"""
|
|
743
|
+
# Decrypt data key
|
|
744
|
+
plain_key = self._provider.decrypt_data_key(
|
|
745
|
+
encrypted.encrypted_key,
|
|
746
|
+
encrypted.key_id,
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
# Decrypt data
|
|
750
|
+
try:
|
|
751
|
+
return self._encryptor.decrypt(encrypted.ciphertext, plain_key)
|
|
752
|
+
finally:
|
|
753
|
+
# Clear plain key from memory
|
|
754
|
+
plain_key = b"\x00" * len(plain_key)
|
|
755
|
+
|
|
756
|
+
def encrypt_file(
|
|
757
|
+
self,
|
|
758
|
+
input_path: str | Path,
|
|
759
|
+
output_path: str | Path,
|
|
760
|
+
**kwargs: Any,
|
|
761
|
+
) -> None:
|
|
762
|
+
"""Encrypt a file.
|
|
763
|
+
|
|
764
|
+
Args:
|
|
765
|
+
input_path: Input file path.
|
|
766
|
+
output_path: Output file path.
|
|
767
|
+
**kwargs: Additional arguments for encrypt().
|
|
768
|
+
"""
|
|
769
|
+
data = Path(input_path).read_bytes()
|
|
770
|
+
encrypted = self.encrypt(data, **kwargs)
|
|
771
|
+
Path(output_path).write_bytes(encrypted.to_bytes())
|
|
772
|
+
|
|
773
|
+
def decrypt_file(
|
|
774
|
+
self,
|
|
775
|
+
input_path: str | Path,
|
|
776
|
+
output_path: str | Path,
|
|
777
|
+
) -> None:
|
|
778
|
+
"""Decrypt a file.
|
|
779
|
+
|
|
780
|
+
Args:
|
|
781
|
+
input_path: Encrypted file path.
|
|
782
|
+
output_path: Output file path.
|
|
783
|
+
"""
|
|
784
|
+
data = Path(input_path).read_bytes()
|
|
785
|
+
encrypted = EncryptedData.from_bytes(data)
|
|
786
|
+
decrypted = self.decrypt(encrypted)
|
|
787
|
+
Path(output_path).write_bytes(decrypted)
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
# =============================================================================
|
|
791
|
+
# Field-Level Encryption
|
|
792
|
+
# =============================================================================
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
@dataclass
|
|
796
|
+
class FieldEncryptionPolicy:
|
|
797
|
+
"""Encryption policy for a field/column.
|
|
798
|
+
|
|
799
|
+
Attributes:
|
|
800
|
+
algorithm: Encryption algorithm.
|
|
801
|
+
key_id: Key ID to use.
|
|
802
|
+
format_preserving: Use format-preserving encryption.
|
|
803
|
+
deterministic: Use deterministic encryption (for searching).
|
|
804
|
+
mask_format: Masking format for display.
|
|
805
|
+
"""
|
|
806
|
+
|
|
807
|
+
algorithm: str = "aes_gcm" # aes_gcm, chacha20, format_preserving
|
|
808
|
+
key_id: str = ""
|
|
809
|
+
format_preserving: bool = False
|
|
810
|
+
deterministic: bool = False
|
|
811
|
+
mask_format: str = "" # e.g., "***-**-{last4}" for SSN
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
class FieldLevelEncryption:
|
|
815
|
+
"""Field-level encryption for sensitive columns.
|
|
816
|
+
|
|
817
|
+
Encrypts individual fields/columns based on policies.
|
|
818
|
+
Supports format-preserving encryption for data that must
|
|
819
|
+
maintain its format (e.g., credit card numbers).
|
|
820
|
+
|
|
821
|
+
Example:
|
|
822
|
+
>>> fle = FieldLevelEncryption(
|
|
823
|
+
... provider=AwsKmsProvider("alias/my-key"),
|
|
824
|
+
... policies={
|
|
825
|
+
... "ssn": FieldEncryptionPolicy(format_preserving=True),
|
|
826
|
+
... "email": FieldEncryptionPolicy(algorithm="aes_gcm"),
|
|
827
|
+
... },
|
|
828
|
+
... )
|
|
829
|
+
>>>
|
|
830
|
+
>>> # Encrypt field
|
|
831
|
+
>>> encrypted_ssn = fle.encrypt_field("ssn", "123-45-6789")
|
|
832
|
+
>>>
|
|
833
|
+
>>> # Decrypt field
|
|
834
|
+
>>> ssn = fle.decrypt_field("ssn", encrypted_ssn)
|
|
835
|
+
"""
|
|
836
|
+
|
|
837
|
+
def __init__(
|
|
838
|
+
self,
|
|
839
|
+
provider: KeyProvider,
|
|
840
|
+
policies: dict[str, FieldEncryptionPolicy] | None = None,
|
|
841
|
+
*,
|
|
842
|
+
default_key_id: str = "",
|
|
843
|
+
) -> None:
|
|
844
|
+
"""Initialize field-level encryption.
|
|
845
|
+
|
|
846
|
+
Args:
|
|
847
|
+
provider: Key provider.
|
|
848
|
+
policies: Field encryption policies.
|
|
849
|
+
default_key_id: Default key ID.
|
|
850
|
+
"""
|
|
851
|
+
self._provider = provider
|
|
852
|
+
self._policies = policies or {}
|
|
853
|
+
self._default_key_id = default_key_id
|
|
854
|
+
self._encryptor = AesGcmEncryptor()
|
|
855
|
+
|
|
856
|
+
def add_policy(self, field_name: str, policy: FieldEncryptionPolicy) -> None:
|
|
857
|
+
"""Add encryption policy for a field.
|
|
858
|
+
|
|
859
|
+
Args:
|
|
860
|
+
field_name: Field name.
|
|
861
|
+
policy: Encryption policy.
|
|
862
|
+
"""
|
|
863
|
+
self._policies[field_name] = policy
|
|
864
|
+
|
|
865
|
+
def encrypt_field(self, field_name: str, value: str) -> str:
|
|
866
|
+
"""Encrypt a field value.
|
|
867
|
+
|
|
868
|
+
Args:
|
|
869
|
+
field_name: Field name.
|
|
870
|
+
value: Value to encrypt.
|
|
871
|
+
|
|
872
|
+
Returns:
|
|
873
|
+
Encrypted value (base64 encoded).
|
|
874
|
+
"""
|
|
875
|
+
policy = self._policies.get(field_name, FieldEncryptionPolicy())
|
|
876
|
+
key_id = policy.key_id or self._default_key_id
|
|
877
|
+
|
|
878
|
+
if policy.format_preserving:
|
|
879
|
+
return self._format_preserving_encrypt(value, key_id)
|
|
880
|
+
|
|
881
|
+
# Get key
|
|
882
|
+
key = self._provider.get_key(key_id)
|
|
883
|
+
|
|
884
|
+
if policy.deterministic:
|
|
885
|
+
# Use fixed nonce derived from value hash (for searching)
|
|
886
|
+
nonce = hashlib.sha256(value.encode()).digest()[:12]
|
|
887
|
+
else:
|
|
888
|
+
nonce = generate_nonce(EncryptionAlgorithm.AES_256_GCM)
|
|
889
|
+
|
|
890
|
+
# Encrypt
|
|
891
|
+
ciphertext = self._encryptor.encrypt(value.encode("utf-8"), key, nonce)
|
|
892
|
+
|
|
893
|
+
# Encode as base64
|
|
894
|
+
return base64.b64encode(nonce + ciphertext).decode("utf-8")
|
|
895
|
+
|
|
896
|
+
def decrypt_field(self, field_name: str, encrypted_value: str) -> str:
|
|
897
|
+
"""Decrypt a field value.
|
|
898
|
+
|
|
899
|
+
Args:
|
|
900
|
+
field_name: Field name.
|
|
901
|
+
encrypted_value: Encrypted value (base64 encoded).
|
|
902
|
+
|
|
903
|
+
Returns:
|
|
904
|
+
Decrypted value.
|
|
905
|
+
"""
|
|
906
|
+
policy = self._policies.get(field_name, FieldEncryptionPolicy())
|
|
907
|
+
key_id = policy.key_id or self._default_key_id
|
|
908
|
+
|
|
909
|
+
if policy.format_preserving:
|
|
910
|
+
return self._format_preserving_decrypt(encrypted_value, key_id)
|
|
911
|
+
|
|
912
|
+
# Decode base64
|
|
913
|
+
data = base64.b64decode(encrypted_value)
|
|
914
|
+
nonce = data[:12]
|
|
915
|
+
ciphertext = data[12:]
|
|
916
|
+
|
|
917
|
+
# Get key
|
|
918
|
+
key = self._provider.get_key(key_id)
|
|
919
|
+
|
|
920
|
+
# Decrypt
|
|
921
|
+
plaintext = self._encryptor.decrypt(ciphertext, key)
|
|
922
|
+
return plaintext.decode("utf-8")
|
|
923
|
+
|
|
924
|
+
def _format_preserving_encrypt(self, value: str, key_id: str) -> str:
|
|
925
|
+
"""Format-preserving encryption (simplified).
|
|
926
|
+
|
|
927
|
+
This is a simplified FPE implementation. For production,
|
|
928
|
+
use a proper FPE library like python-ff3.
|
|
929
|
+
"""
|
|
930
|
+
# Get key
|
|
931
|
+
key = self._provider.get_key(key_id)
|
|
932
|
+
|
|
933
|
+
# Simple FPE: XOR with key-derived stream
|
|
934
|
+
key_stream = hashlib.sha256(key + value.encode()).digest()
|
|
935
|
+
result = []
|
|
936
|
+
|
|
937
|
+
for i, char in enumerate(value):
|
|
938
|
+
if char.isdigit():
|
|
939
|
+
offset = key_stream[i % 32] % 10
|
|
940
|
+
new_digit = (int(char) + offset) % 10
|
|
941
|
+
result.append(str(new_digit))
|
|
942
|
+
elif char.isalpha():
|
|
943
|
+
offset = key_stream[i % 32] % 26
|
|
944
|
+
if char.isupper():
|
|
945
|
+
new_char = chr((ord(char) - ord("A") + offset) % 26 + ord("A"))
|
|
946
|
+
else:
|
|
947
|
+
new_char = chr((ord(char) - ord("a") + offset) % 26 + ord("a"))
|
|
948
|
+
result.append(new_char)
|
|
949
|
+
else:
|
|
950
|
+
result.append(char)
|
|
951
|
+
|
|
952
|
+
return "".join(result)
|
|
953
|
+
|
|
954
|
+
def _format_preserving_decrypt(self, value: str, key_id: str) -> str:
|
|
955
|
+
"""Format-preserving decryption."""
|
|
956
|
+
key = self._provider.get_key(key_id)
|
|
957
|
+
|
|
958
|
+
# Reverse the FPE
|
|
959
|
+
key_stream = hashlib.sha256(key + value.encode()).digest()
|
|
960
|
+
result = []
|
|
961
|
+
|
|
962
|
+
for i, char in enumerate(value):
|
|
963
|
+
if char.isdigit():
|
|
964
|
+
offset = key_stream[i % 32] % 10
|
|
965
|
+
new_digit = (int(char) - offset) % 10
|
|
966
|
+
result.append(str(new_digit))
|
|
967
|
+
elif char.isalpha():
|
|
968
|
+
offset = key_stream[i % 32] % 26
|
|
969
|
+
if char.isupper():
|
|
970
|
+
new_char = chr((ord(char) - ord("A") - offset) % 26 + ord("A"))
|
|
971
|
+
else:
|
|
972
|
+
new_char = chr((ord(char) - ord("a") - offset) % 26 + ord("a"))
|
|
973
|
+
result.append(new_char)
|
|
974
|
+
else:
|
|
975
|
+
result.append(char)
|
|
976
|
+
|
|
977
|
+
return "".join(result)
|
|
978
|
+
|
|
979
|
+
def mask_field(self, field_name: str, value: str) -> str:
|
|
980
|
+
"""Mask a field value for display.
|
|
981
|
+
|
|
982
|
+
Args:
|
|
983
|
+
field_name: Field name.
|
|
984
|
+
value: Value to mask.
|
|
985
|
+
|
|
986
|
+
Returns:
|
|
987
|
+
Masked value.
|
|
988
|
+
"""
|
|
989
|
+
policy = self._policies.get(field_name, FieldEncryptionPolicy())
|
|
990
|
+
|
|
991
|
+
if policy.mask_format:
|
|
992
|
+
# Apply mask format
|
|
993
|
+
if "{last4}" in policy.mask_format:
|
|
994
|
+
last4 = value[-4:] if len(value) >= 4 else value
|
|
995
|
+
return policy.mask_format.replace("{last4}", last4)
|
|
996
|
+
return policy.mask_format
|
|
997
|
+
|
|
998
|
+
# Default masking
|
|
999
|
+
if len(value) <= 4:
|
|
1000
|
+
return "*" * len(value)
|
|
1001
|
+
return value[:2] + "*" * (len(value) - 4) + value[-2:]
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
# =============================================================================
|
|
1005
|
+
# Encryption Configuration
|
|
1006
|
+
# =============================================================================
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
@dataclass
|
|
1010
|
+
class EnterpriseEncryptionConfig:
|
|
1011
|
+
"""Enterprise encryption configuration.
|
|
1012
|
+
|
|
1013
|
+
Example:
|
|
1014
|
+
>>> config = EnterpriseEncryptionConfig(
|
|
1015
|
+
... provider="aws_kms",
|
|
1016
|
+
... key_id="alias/truthound-data-key",
|
|
1017
|
+
... region="us-east-1",
|
|
1018
|
+
... )
|
|
1019
|
+
"""
|
|
1020
|
+
|
|
1021
|
+
enabled: bool = True
|
|
1022
|
+
provider: str = "local" # local, vault, aws_kms, gcp_kms, azure_keyvault
|
|
1023
|
+
|
|
1024
|
+
# Provider-specific settings
|
|
1025
|
+
key_id: str = ""
|
|
1026
|
+
|
|
1027
|
+
# Vault settings
|
|
1028
|
+
vault_url: str = ""
|
|
1029
|
+
vault_token: str = ""
|
|
1030
|
+
vault_mount_point: str = "transit"
|
|
1031
|
+
|
|
1032
|
+
# AWS settings
|
|
1033
|
+
aws_region: str = ""
|
|
1034
|
+
|
|
1035
|
+
# GCP settings
|
|
1036
|
+
gcp_project_id: str = ""
|
|
1037
|
+
gcp_location: str = "global"
|
|
1038
|
+
gcp_key_ring: str = "truthound"
|
|
1039
|
+
|
|
1040
|
+
# Azure settings
|
|
1041
|
+
azure_vault_url: str = ""
|
|
1042
|
+
|
|
1043
|
+
# Local settings
|
|
1044
|
+
local_key_file: str = ".truthound_keys"
|
|
1045
|
+
local_master_password: str = ""
|
|
1046
|
+
|
|
1047
|
+
# Field-level encryption
|
|
1048
|
+
field_policies: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
# =============================================================================
|
|
1052
|
+
# Global Encryption
|
|
1053
|
+
# =============================================================================
|
|
1054
|
+
|
|
1055
|
+
_global_provider: KeyProvider | None = None
|
|
1056
|
+
_global_encryptor: AtRestEncryption | None = None
|
|
1057
|
+
_lock = threading.Lock()
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def configure_encryption(
|
|
1061
|
+
*,
|
|
1062
|
+
provider: str = "local",
|
|
1063
|
+
key_id: str = "",
|
|
1064
|
+
vault_url: str = "",
|
|
1065
|
+
aws_region: str = "",
|
|
1066
|
+
gcp_project_id: str = "",
|
|
1067
|
+
azure_vault_url: str = "",
|
|
1068
|
+
**kwargs: Any,
|
|
1069
|
+
) -> AtRestEncryption:
|
|
1070
|
+
"""Configure global encryption.
|
|
1071
|
+
|
|
1072
|
+
Args:
|
|
1073
|
+
provider: Key provider type.
|
|
1074
|
+
key_id: Default key ID.
|
|
1075
|
+
vault_url: Vault URL (if using Vault).
|
|
1076
|
+
aws_region: AWS region (if using AWS KMS).
|
|
1077
|
+
gcp_project_id: GCP project ID (if using GCP KMS).
|
|
1078
|
+
azure_vault_url: Azure Key Vault URL.
|
|
1079
|
+
**kwargs: Additional provider configuration.
|
|
1080
|
+
|
|
1081
|
+
Returns:
|
|
1082
|
+
Configured AtRestEncryption instance.
|
|
1083
|
+
"""
|
|
1084
|
+
global _global_provider, _global_encryptor
|
|
1085
|
+
|
|
1086
|
+
with _lock:
|
|
1087
|
+
# Create provider
|
|
1088
|
+
if provider == "vault":
|
|
1089
|
+
_global_provider = VaultKeyProvider(
|
|
1090
|
+
vault_url,
|
|
1091
|
+
token=kwargs.get("vault_token"),
|
|
1092
|
+
mount_point=kwargs.get("vault_mount_point", "transit"),
|
|
1093
|
+
)
|
|
1094
|
+
elif provider == "aws_kms":
|
|
1095
|
+
_global_provider = AwsKmsProvider(
|
|
1096
|
+
key_id,
|
|
1097
|
+
region=aws_region,
|
|
1098
|
+
)
|
|
1099
|
+
elif provider == "gcp_kms":
|
|
1100
|
+
_global_provider = GcpKmsProvider(
|
|
1101
|
+
key_id,
|
|
1102
|
+
project_id=gcp_project_id,
|
|
1103
|
+
location=kwargs.get("gcp_location", "global"),
|
|
1104
|
+
key_ring=kwargs.get("gcp_key_ring", "truthound"),
|
|
1105
|
+
)
|
|
1106
|
+
elif provider == "azure_keyvault":
|
|
1107
|
+
_global_provider = AzureKeyVaultProvider(
|
|
1108
|
+
azure_vault_url,
|
|
1109
|
+
key_id,
|
|
1110
|
+
)
|
|
1111
|
+
else: # local
|
|
1112
|
+
_global_provider = LocalKeyProvider(
|
|
1113
|
+
key_file=kwargs.get("local_key_file", ".truthound_keys"),
|
|
1114
|
+
master_password=kwargs.get("local_master_password"),
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
_global_encryptor = AtRestEncryption(_global_provider, key_id)
|
|
1118
|
+
return _global_encryptor
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def get_encryptor() -> AtRestEncryption:
|
|
1122
|
+
"""Get the global encryptor.
|
|
1123
|
+
|
|
1124
|
+
Returns:
|
|
1125
|
+
AtRestEncryption instance.
|
|
1126
|
+
"""
|
|
1127
|
+
global _global_encryptor
|
|
1128
|
+
|
|
1129
|
+
with _lock:
|
|
1130
|
+
if _global_encryptor is None:
|
|
1131
|
+
_global_encryptor = configure_encryption(provider="local", key_id="default")
|
|
1132
|
+
return _global_encryptor
|