znyx-core 1.0.0__tar.gz

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.
Files changed (113) hide show
  1. znyx_core-1.0.0/PKG-INFO +54 -0
  2. znyx_core-1.0.0/README.md +23 -0
  3. znyx_core-1.0.0/pyproject.toml +49 -0
  4. znyx_core-1.0.0/setup.cfg +4 -0
  5. znyx_core-1.0.0/src/znyx_core/__init__.py +0 -0
  6. znyx_core-1.0.0/src/znyx_core/config/__init__.py +0 -0
  7. znyx_core-1.0.0/src/znyx_core/config/tunables.py +194 -0
  8. znyx_core-1.0.0/src/znyx_core/core/__init__.py +1 -0
  9. znyx_core-1.0.0/src/znyx_core/core/decision.py +68 -0
  10. znyx_core-1.0.0/src/znyx_core/core/fingerprint.py +48 -0
  11. znyx_core-1.0.0/src/znyx_core/core/labels.py +125 -0
  12. znyx_core-1.0.0/src/znyx_core/core/models.py +409 -0
  13. znyx_core-1.0.0/src/znyx_core/core/risk.py +26 -0
  14. znyx_core-1.0.0/src/znyx_core/core/text_normalize.py +70 -0
  15. znyx_core-1.0.0/src/znyx_core/detectors/__init__.py +1 -0
  16. znyx_core-1.0.0/src/znyx_core/detectors/_injection_patterns.py +103 -0
  17. znyx_core-1.0.0/src/znyx_core/detectors/abuse.py +332 -0
  18. znyx_core-1.0.0/src/znyx_core/detectors/adapters/__init__.py +34 -0
  19. znyx_core-1.0.0/src/znyx_core/detectors/adapters/aws_bedrock_guardrails.py +215 -0
  20. znyx_core-1.0.0/src/znyx_core/detectors/adapters/azure_content_safety.py +134 -0
  21. znyx_core-1.0.0/src/znyx_core/detectors/adapters/openai_moderation.py +97 -0
  22. znyx_core-1.0.0/src/znyx_core/detectors/bias.py +293 -0
  23. znyx_core-1.0.0/src/znyx_core/detectors/citation_integrity.py +219 -0
  24. znyx_core-1.0.0/src/znyx_core/detectors/code_safety.py +317 -0
  25. znyx_core-1.0.0/src/znyx_core/detectors/competitor.py +361 -0
  26. znyx_core-1.0.0/src/znyx_core/detectors/compliance.py +354 -0
  27. znyx_core-1.0.0/src/znyx_core/detectors/copyright.py +285 -0
  28. znyx_core-1.0.0/src/znyx_core/detectors/document_metadata_leakage.py +82 -0
  29. znyx_core-1.0.0/src/znyx_core/detectors/embedding_integrity.py +118 -0
  30. znyx_core-1.0.0/src/znyx_core/detectors/excessive_agency.py +155 -0
  31. znyx_core-1.0.0/src/znyx_core/detectors/exfiltration.py +157 -0
  32. znyx_core-1.0.0/src/znyx_core/detectors/gibberish.py +277 -0
  33. znyx_core-1.0.0/src/znyx_core/detectors/hallucination.py +275 -0
  34. znyx_core-1.0.0/src/znyx_core/detectors/jailbreak.py +1353 -0
  35. znyx_core-1.0.0/src/znyx_core/detectors/language.py +268 -0
  36. znyx_core-1.0.0/src/znyx_core/detectors/malicious_url.py +381 -0
  37. znyx_core-1.0.0/src/znyx_core/detectors/mcp_manifest_scanner.py +390 -0
  38. znyx_core-1.0.0/src/znyx_core/detectors/memory_write_poisoning.py +67 -0
  39. znyx_core-1.0.0/src/znyx_core/detectors/numerical_consistency.py +93 -0
  40. znyx_core-1.0.0/src/znyx_core/detectors/pii.py +1373 -0
  41. znyx_core-1.0.0/src/znyx_core/detectors/plugin.py +302 -0
  42. znyx_core-1.0.0/src/znyx_core/detectors/remote.py +366 -0
  43. znyx_core-1.0.0/src/znyx_core/detectors/retrieval_chunk_injection.py +64 -0
  44. znyx_core-1.0.0/src/znyx_core/detectors/secrets.py +393 -0
  45. znyx_core-1.0.0/src/znyx_core/detectors/sensitive_business_data.py +135 -0
  46. znyx_core-1.0.0/src/znyx_core/detectors/sentiment.py +258 -0
  47. znyx_core-1.0.0/src/znyx_core/detectors/structure.py +384 -0
  48. znyx_core-1.0.0/src/znyx_core/detectors/system_prompt_leakage.py +76 -0
  49. znyx_core-1.0.0/src/znyx_core/detectors/tool_output_injection.py +68 -0
  50. znyx_core-1.0.0/src/znyx_core/detectors/tools.py +331 -0
  51. znyx_core-1.0.0/src/znyx_core/detectors/topic.py +1647 -0
  52. znyx_core-1.0.0/src/znyx_core/detectors/toxicity.py +1707 -0
  53. znyx_core-1.0.0/src/znyx_core/detectors/unbounded_consumption.py +182 -0
  54. znyx_core-1.0.0/src/znyx_core/engine/__init__.py +1 -0
  55. znyx_core-1.0.0/src/znyx_core/engine/backends.py +230 -0
  56. znyx_core-1.0.0/src/znyx_core/engine/consensus.py +91 -0
  57. znyx_core-1.0.0/src/znyx_core/engine/detector_registry.py +122 -0
  58. znyx_core-1.0.0/src/znyx_core/engine/egress.py +270 -0
  59. znyx_core-1.0.0/src/znyx_core/engine/escalation.py +322 -0
  60. znyx_core-1.0.0/src/znyx_core/engine/eval_metrics.py +216 -0
  61. znyx_core-1.0.0/src/znyx_core/engine/evaluator.py +509 -0
  62. znyx_core-1.0.0/src/znyx_core/engine/judge_catalog.py +127 -0
  63. znyx_core-1.0.0/src/znyx_core/engine/judge_escalation.py +180 -0
  64. znyx_core-1.0.0/src/znyx_core/engine/judge_runtime.py +212 -0
  65. znyx_core-1.0.0/src/znyx_core/engine/ml_catalog.py +307 -0
  66. znyx_core-1.0.0/src/znyx_core/engine/orchestrator.py +595 -0
  67. znyx_core-1.0.0/src/znyx_core/engine/owasp_map.py +274 -0
  68. znyx_core-1.0.0/src/znyx_core/engine/quality/__init__.py +1 -0
  69. znyx_core-1.0.0/src/znyx_core/engine/quality/coherence.py +108 -0
  70. znyx_core-1.0.0/src/znyx_core/engine/quality/fluency.py +130 -0
  71. znyx_core-1.0.0/src/znyx_core/engine/quality/groundedness.py +174 -0
  72. znyx_core-1.0.0/src/znyx_core/engine/quality/intent_resolution.py +141 -0
  73. znyx_core-1.0.0/src/znyx_core/engine/quality/judge_evaluator.py +247 -0
  74. znyx_core-1.0.0/src/znyx_core/engine/quality/nli_client.py +245 -0
  75. znyx_core-1.0.0/src/znyx_core/engine/quality/relevance.py +101 -0
  76. znyx_core-1.0.0/src/znyx_core/engine/quality/task_adherence.py +157 -0
  77. znyx_core-1.0.0/src/znyx_core/engine/quality/tool_accuracy.py +125 -0
  78. znyx_core-1.0.0/src/znyx_core/engine/quality_scorer.py +198 -0
  79. znyx_core-1.0.0/src/znyx_core/engine/remediation.py +318 -0
  80. znyx_core-1.0.0/src/znyx_core/engine/scorecard_gate.py +217 -0
  81. znyx_core-1.0.0/src/znyx_core/engine/scorecard_stamp.py +98 -0
  82. znyx_core-1.0.0/src/znyx_core/engine/streaming.py +162 -0
  83. znyx_core-1.0.0/src/znyx_core/engine/tool_registration_hook.py +22 -0
  84. znyx_core-1.0.0/src/znyx_core/llm/__init__.py +0 -0
  85. znyx_core-1.0.0/src/znyx_core/llm/judge.py +280 -0
  86. znyx_core-1.0.0/src/znyx_core/llm/providers.py +510 -0
  87. znyx_core-1.0.0/src/znyx_core/llm/round_trip.py +375 -0
  88. znyx_core-1.0.0/src/znyx_core/middleware/__init__.py +1 -0
  89. znyx_core-1.0.0/src/znyx_core/middleware/auth.py +178 -0
  90. znyx_core-1.0.0/src/znyx_core/middleware/csrf.py +148 -0
  91. znyx_core-1.0.0/src/znyx_core/middleware/metrics.py +283 -0
  92. znyx_core-1.0.0/src/znyx_core/middleware/otel.py +172 -0
  93. znyx_core-1.0.0/src/znyx_core/middleware/rate_limit.py +253 -0
  94. znyx_core-1.0.0/src/znyx_core/net_guard.py +146 -0
  95. znyx_core-1.0.0/src/znyx_core/policy/__init__.py +1 -0
  96. znyx_core-1.0.0/src/znyx_core/policy/bundle.py +266 -0
  97. znyx_core-1.0.0/src/znyx_core/policy/db_resolver_registry.py +34 -0
  98. znyx_core-1.0.0/src/znyx_core/policy/lint.py +66 -0
  99. znyx_core-1.0.0/src/znyx_core/policy/loader.py +83 -0
  100. znyx_core-1.0.0/src/znyx_core/policy/resolver.py +49 -0
  101. znyx_core-1.0.0/src/znyx_core/policy/schema.py +672 -0
  102. znyx_core-1.0.0/src/znyx_core/services/__init__.py +0 -0
  103. znyx_core-1.0.0/src/znyx_core/utils/__init__.py +1 -0
  104. znyx_core-1.0.0/src/znyx_core/utils/checksums.py +147 -0
  105. znyx_core-1.0.0/src/znyx_core/utils/env.py +39 -0
  106. znyx_core-1.0.0/src/znyx_core/utils/hash.py +23 -0
  107. znyx_core-1.0.0/src/znyx_core/utils/luhn.py +39 -0
  108. znyx_core-1.0.0/src/znyx_core/validation.py +18 -0
  109. znyx_core-1.0.0/src/znyx_core.egg-info/PKG-INFO +54 -0
  110. znyx_core-1.0.0/src/znyx_core.egg-info/SOURCES.txt +111 -0
  111. znyx_core-1.0.0/src/znyx_core.egg-info/dependency_links.txt +1 -0
  112. znyx_core-1.0.0/src/znyx_core.egg-info/requires.txt +10 -0
  113. znyx_core-1.0.0/src/znyx_core.egg-info/top_level.txt +1 -0
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: znyx-core
3
+ Version: 1.0.0
4
+ Summary: Znyx guardrails detection engine: detectors, policy resolution, and orchestration for LLM safety, usable in-process.
5
+ Author: Zitrino
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/zitrino-oss/znyx-runtime
8
+ Project-URL: Source, https://github.com/zitrino-oss/znyx-runtime
9
+ Project-URL: Issues, https://github.com/zitrino-oss/znyx-runtime/issues
10
+ Keywords: llm,guardrails,safety,ai-security,prompt-injection,pii,content-moderation
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Security
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: pydantic>=2.5.0
23
+ Requires-Dist: pyyaml>=6.0
24
+ Requires-Dist: rapidfuzz>=3.6.0
25
+ Requires-Dist: httpx>=0.26.0
26
+ Requires-Dist: cryptography>=41.0.0
27
+ Requires-Dist: defusedxml>=0.7.1
28
+ Requires-Dist: fastapi>=0.109.0
29
+ Provides-Extra: semantic
30
+ Requires-Dist: sentence-transformers>=2.2.0; extra == "semantic"
31
+
32
+ # znyx-core
33
+
34
+ The Znyx guardrails detection engine: detectors, policy resolution, scoring, and
35
+ orchestration for LLM safety. Importable in-process, with no server or HTTP hop.
36
+
37
+ ```bash
38
+ pip install znyx-core
39
+ ```
40
+
41
+ `znyx-core` is the engine that powers the [`znyx-runtime`](https://github.com/zitrino-oss/znyx-runtime)
42
+ service. Install it directly when you want to run detection inside your own Python
43
+ process rather than calling the runtime over HTTP.
44
+
45
+ It runs rules-only by default. Model-backed (ML) detection is an optional layer
46
+ served by a separate Znyx inference sidecar over HTTP; without it, every detector
47
+ has a deterministic rules path.
48
+
49
+ See the [repository README](https://github.com/zitrino-oss/znyx-runtime) for
50
+ configuration, the policy schema, and usage examples.
51
+
52
+ ## License
53
+
54
+ Apache-2.0
@@ -0,0 +1,23 @@
1
+ # znyx-core
2
+
3
+ The Znyx guardrails detection engine: detectors, policy resolution, scoring, and
4
+ orchestration for LLM safety. Importable in-process, with no server or HTTP hop.
5
+
6
+ ```bash
7
+ pip install znyx-core
8
+ ```
9
+
10
+ `znyx-core` is the engine that powers the [`znyx-runtime`](https://github.com/zitrino-oss/znyx-runtime)
11
+ service. Install it directly when you want to run detection inside your own Python
12
+ process rather than calling the runtime over HTTP.
13
+
14
+ It runs rules-only by default. Model-backed (ML) detection is an optional layer
15
+ served by a separate Znyx inference sidecar over HTTP; without it, every detector
16
+ has a deterministic rules path.
17
+
18
+ See the [repository README](https://github.com/zitrino-oss/znyx-runtime) for
19
+ configuration, the policy schema, and usage examples.
20
+
21
+ ## License
22
+
23
+ Apache-2.0
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "znyx-core"
7
+ version = "1.0.0"
8
+ description = "Znyx guardrails detection engine: detectors, policy resolution, and orchestration for LLM safety, usable in-process."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "Zitrino" }]
13
+ keywords = ["llm", "guardrails", "safety", "ai-security", "prompt-injection", "pii", "content-moderation"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Security",
23
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
24
+ ]
25
+ dependencies = [
26
+ "pydantic>=2.5.0",
27
+ "pyyaml>=6.0",
28
+ "rapidfuzz>=3.6.0",
29
+ "httpx>=0.26.0",
30
+ "cryptography>=41.0.0",
31
+ "defusedxml>=0.7.1",
32
+ "fastapi>=0.109.0",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ # The hallucination / groundedness detector uses sentence-transformers when
37
+ # present, and falls back to a token-overlap heuristic when it is not installed.
38
+ semantic = ["sentence-transformers>=2.2.0"]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/zitrino-oss/znyx-runtime"
42
+ Source = "https://github.com/zitrino-oss/znyx-runtime"
43
+ Issues = "https://github.com/zitrino-oss/znyx-runtime/issues"
44
+
45
+ [tool.setuptools.packages.find]
46
+ where = ["src"]
47
+
48
+ [tool.setuptools.package-data]
49
+ znyx_core = ["**/*.yaml", "**/*.yml", "**/*.json"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
File without changes
@@ -0,0 +1,194 @@
1
+ """Centralised numeric tunables — single source of truth for thresholds,
2
+ batch sizes, retry counts, and timeout windows.
3
+
4
+ Adding a new constant here is preferable to inlining a number in service
5
+ code for three reasons:
6
+
7
+ 1. **Discoverability.** A new engineer can grep one file to learn what
8
+ knobs exist and what their defaults are.
9
+ 2. **Override surface.** Every value reads from an env var with a sensible
10
+ default — ops never has to redeploy to retune a quota.
11
+ 3. **Type/range safety.** The casts (``int(os.getenv(..., default))``) live
12
+ in one place; adding a min/max guard is one edit, not a campaign.
13
+
14
+ Convention: every setting is ``UPPER_SNAKE_CASE``, exposed as a module-level
15
+ constant. Group by subsystem with a comment header. When a service consumes
16
+ one, prefer ``from znyx_core.config.tunables import RETRY_DELAYS_SECONDS`` over
17
+ duplicating the literal value.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ from typing import Tuple
23
+
24
+
25
+ def _int(env: str, default: int) -> int:
26
+ try:
27
+ return int(os.getenv(env, str(default)))
28
+ except (TypeError, ValueError):
29
+ return default
30
+
31
+
32
+ def _float(env: str, default: float) -> float:
33
+ try:
34
+ return float(os.getenv(env, str(default)))
35
+ except (TypeError, ValueError):
36
+ return default
37
+
38
+
39
+ def _str(env: str, default: str = "") -> str:
40
+ return os.getenv(env, default)
41
+
42
+
43
+ def _bool(env: str, default: bool = False) -> bool:
44
+ raw = os.getenv(env)
45
+ if raw is None:
46
+ return default
47
+ return raw.strip().lower() in ("1", "true", "yes")
48
+
49
+
50
+ def _csv_floats(env: str, default: Tuple[float, ...]) -> Tuple[float, ...]:
51
+ raw = os.getenv(env)
52
+ if not raw:
53
+ return default
54
+ try:
55
+ return tuple(float(x.strip()) for x in raw.split(",") if x.strip())
56
+ except (TypeError, ValueError):
57
+ return default
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Webhook delivery
62
+ # ---------------------------------------------------------------------------
63
+ WEBHOOK_MAX_RETRIES: int = _int("ZNYX_WEBHOOK_MAX_RETRIES", 3)
64
+ WEBHOOK_RETRY_DELAYS_SECONDS: Tuple[float, ...] = _csv_floats(
65
+ "ZNYX_WEBHOOK_RETRY_DELAYS", (1.0, 5.0, 30.0)
66
+ )
67
+ WEBHOOK_REQUEST_TIMEOUT_SECONDS: float = _float("ZNYX_WEBHOOK_TIMEOUT", 10.0)
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Runtime telemetry batching
72
+ # ---------------------------------------------------------------------------
73
+ TELEMETRY_BATCH_SIZE: int = _int("ZNYX_TELEMETRY_BATCH_SIZE", 100)
74
+ TELEMETRY_FLUSH_INTERVAL_SECONDS: int = _int("ZNYX_TELEMETRY_FLUSH_INTERVAL", 10)
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Bundle manager
79
+ # ---------------------------------------------------------------------------
80
+ BUNDLE_BOOT_RETRY_DELAYS_SECONDS: Tuple[float, ...] = _csv_floats(
81
+ "ZNYX_BUNDLE_BOOT_RETRY_DELAYS", (1.0, 3.0, 5.0)
82
+ )
83
+ BUNDLE_POLL_INTERVAL_SECONDS: float = _float("ZNYX_BUNDLE_POLL_INTERVAL", 30.0)
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Billing — subscription state machine
88
+ # ---------------------------------------------------------------------------
89
+ BILLING_GRACE_DAYS: int = _int("ZNYX_BILLING_GRACE_DAYS", 7)
90
+ # DEPRECATED: trials are removed — no plan grants a trial and nothing reads
91
+ # this value anymore. Kept only so existing env/config files don't error.
92
+ # The ``trialing`` status is left inert for any pre-existing trialing orgs.
93
+ BILLING_TRIAL_DAYS: int = _int("ZNYX_BILLING_TRIAL_DAYS", 14)
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Retention defaults (per plan, in days). Keep alongside other tunables so
98
+ # ops can override per-tier without editing the service body.
99
+ # ---------------------------------------------------------------------------
100
+ RETENTION_FREE_DAYS: int = _int("ZNYX_RETENTION_FREE_DAYS", 30)
101
+ RETENTION_GROWTH_DAYS: int = _int("ZNYX_RETENTION_GROWTH_DAYS", 90)
102
+ RETENTION_ENTERPRISE_DAYS: int = _int("ZNYX_RETENTION_ENTERPRISE_DAYS", 365)
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Auth / brute-force protection
107
+ # ---------------------------------------------------------------------------
108
+ AUTH_MAX_FAILED_ATTEMPTS: int = _int("MAX_FAILED_LOGIN_ATTEMPTS", 10)
109
+ AUTH_LOCKOUT_DURATION_MINUTES: int = _int("LOGIN_LOCKOUT_MINUTES", 15)
110
+ AUTH_LOGIN_RATE_LIMIT_PER_MINUTE: int = _int("LOGIN_RATE_LIMIT_PER_MINUTE", 10)
111
+ AUTH_REGISTER_RATE_LIMIT_PER_MINUTE: int = _int("REGISTER_RATE_LIMIT_PER_MINUTE", 5)
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # Remote detector HTTP client
116
+ # ---------------------------------------------------------------------------
117
+ REMOTE_DETECTOR_TIMEOUT_SECONDS: float = _float("ZNYX_REMOTE_DETECTOR_TIMEOUT", 5.0)
118
+ REMOTE_DETECTOR_MAX_RETRIES: int = _int("ZNYX_REMOTE_DETECTOR_MAX_RETRIES", 2)
119
+ REMOTE_DETECTOR_CIRCUIT_THRESHOLD: int = _int(
120
+ "ZNYX_REMOTE_DETECTOR_CIRCUIT_THRESHOLD", 5
121
+ )
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # Scheduler intervals (seconds)
126
+ # ---------------------------------------------------------------------------
127
+ SCHEDULER_RETENTION_INTERVAL_SECONDS: int = _int("SCHEDULER_RETENTION_INTERVAL", 3600)
128
+ SCHEDULER_ALERT_INTERVAL_SECONDS: int = _int("SCHEDULER_ALERT_INTERVAL", 300)
129
+ SCHEDULER_SUBSCRIPTION_INTERVAL_SECONDS: int = _int(
130
+ "SCHEDULER_SUBSCRIPTION_INTERVAL", 3600
131
+ )
132
+ # Auto-invoice generation: how often to check for ended billing periods.
133
+ SCHEDULER_BILLING_INTERVAL_SECONDS: int = _int("SCHEDULER_BILLING_INTERVAL", 3600)
134
+ # egress audit: how often to fold the durable egress spool into egress_events rows
135
+ # (read endpoints also drain on demand; this keeps the table fresh without a read).
136
+ SCHEDULER_EGRESS_DRAIN_INTERVAL_SECONDS: int = _int("SCHEDULER_EGRESS_DRAIN_INTERVAL", 60)
137
+
138
+
139
+ # ---------------------------------------------------------------------------
140
+ # Pagination defaults
141
+ # ---------------------------------------------------------------------------
142
+ DEFAULT_PAGE_SIZE: int = _int("ZNYX_DEFAULT_PAGE_SIZE", 50)
143
+ MAX_PAGE_SIZE: int = _int("ZNYX_MAX_PAGE_SIZE", 200)
144
+
145
+
146
+ # ---------------------------------------------------------------------------
147
+ # Database connection pool
148
+ # ---------------------------------------------------------------------------
149
+ DB_POOL_SIZE: int = _int("DB_POOL_SIZE", 20)
150
+ DB_MAX_OVERFLOW: int = _int("DB_MAX_OVERFLOW", 40)
151
+
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # HTTP rate limiter (per-IP / per-key sliding window)
155
+ # ---------------------------------------------------------------------------
156
+ RATE_LIMIT_REQUESTS_PER_MINUTE: int = _int("RATE_LIMIT_REQUESTS_PER_MINUTE", 60)
157
+ RATE_LIMIT_BURST: int = _int("RATE_LIMIT_BURST", 10)
158
+
159
+
160
+ # ---------------------------------------------------------------------------
161
+ # Server ports
162
+ # ---------------------------------------------------------------------------
163
+ CONTROL_PLANE_PORT: int = _int("CONTROL_PLANE_PORT", _int("PORT", 8000))
164
+ RUNTIME_PORT: int = _int("RUNTIME_PORT", _int("PORT", 8080))
165
+
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # Email — Microsoft Graph API
169
+ # ---------------------------------------------------------------------------
170
+ MS_GRAPH_TENANT_ID: str = _str("MICROSOFT_TENANT_ID", "")
171
+ MS_GRAPH_CLIENT_ID: str = _str("MICROSOFT_CLIENT_ID", "")
172
+ MS_GRAPH_CLIENT_SECRET: str = _str("MICROSOFT_CLIENT_SECRET", "")
173
+ MS_GRAPH_SENDER_EMAIL: str = _str("MICROSOFT_SENDER_EMAIL", "")
174
+ EMAIL_FROM_NAME: str = _str("EMAIL_FROM_NAME", "ZNYX Guardrails")
175
+ # Inbox that website contact-form enquiries (POST /v1/contact) are delivered to.
176
+ # Mirrors the website's NEXT_PUBLIC_ENQUIRY_EMAIL default.
177
+ CONTACT_ENQUIRY_EMAIL: str = _str("CONTACT_ENQUIRY_EMAIL", "").strip()
178
+ # NB: a *present but empty* env var (e.g. an unset GitHub Actions variable
179
+ # expanded into a ConfigMap as CONSOLE_BASE_URL='') makes os.getenv return ""
180
+ # rather than the default, which would yield relative links like
181
+ # "/console/verify-email?..." in emails — broken in Outlook and other clients.
182
+ # Strip + `or default` so an empty value still resolves to an absolute URL.
183
+ CONSOLE_BASE_URL: str = _str("CONSOLE_BASE_URL", "").strip().rstrip("/") or "http://localhost:3701"
184
+ PASSWORD_RESET_EXPIRES_MINUTES: int = _int("PASSWORD_RESET_EXPIRES_MINUTES", 60)
185
+
186
+
187
+ # ---------------------------------------------------------------------------
188
+ # Azure Storage
189
+ # ---------------------------------------------------------------------------
190
+ AZURE_STORAGE_ACCOUNT_NAME: str = _str("AZURE_STORAGE_ACCOUNT_NAME", "")
191
+ AZURE_STORAGE_ACCOUNT_KEY: str = _str("AZURE_STORAGE_ACCOUNT_KEY", "")
192
+ AZURE_STORAGE_CONTAINER_NAME: str = _str("AZURE_STORAGE_CONTAINER_NAME", "")
193
+ AZURE_PUBLIC_STORAGE_CONTAINER: str = _str("AZURE_PUBLIC_STORAGE_CONTAINER", "publicmailtemplates")
194
+ KEY_VAULT_NAME: str = _str("KEY_VAULT_NAME", "")
@@ -0,0 +1 @@
1
+ # Core module
@@ -0,0 +1,68 @@
1
+ from typing import List
2
+ from znyx_core.core.models import Decision, DetectorResult, RuleHit
3
+ from znyx_core.core.labels import DECISION_PRECEDENCE
4
+
5
+
6
+ class DecisionAggregator:
7
+ """Aggregates detector results into a final decision"""
8
+
9
+ # Canonical "worst wins" ranking (BLOCK > REDACT > TRANSFORM > WARN > ALLOW),
10
+ # defined once in core.labels so the aggregator, trace UI, and docs agree.
11
+ DECISION_PRIORITY = DECISION_PRECEDENCE
12
+
13
+ @staticmethod
14
+ def aggregate(results: List[DetectorResult]) -> DetectorResult:
15
+ """
16
+ Aggregate multiple detector results into a single result.
17
+ Uses highest priority decision and combines all rule hits.
18
+ """
19
+ if not results:
20
+ return DetectorResult(
21
+ decision=Decision.ALLOW,
22
+ risk_score=0,
23
+ rule_hits=[],
24
+ sanitized_text=None,
25
+ user_message=None,
26
+ developer_message=None
27
+ )
28
+
29
+ # Combine all rule hits
30
+ all_rule_hits: List[RuleHit] = []
31
+ for result in results:
32
+ all_rule_hits.extend(result.rule_hits)
33
+
34
+ # Calculate max risk score
35
+ max_risk_score = max((r.risk_score for r in results), default=0)
36
+
37
+ # Find highest priority decision
38
+ highest_decision = Decision.ALLOW
39
+ highest_priority = 0
40
+ selected_result = None
41
+
42
+ for result in results:
43
+ if result.decision:
44
+ priority = DecisionAggregator.DECISION_PRIORITY.get(result.decision, 0)
45
+ if priority > highest_priority:
46
+ highest_priority = priority
47
+ highest_decision = result.decision
48
+ selected_result = result
49
+
50
+ # Use sanitized/transformed text and messages from the selected result
51
+ sanitized_text = selected_result.sanitized_text if selected_result else None
52
+ user_message = selected_result.user_message if selected_result else None
53
+ developer_message = selected_result.developer_message if selected_result else None
54
+
55
+ # If we have multiple results with text transformations, use the last one
56
+ # (this handles chaining of transformations)
57
+ for result in results:
58
+ if result.sanitized_text and result.decision in [Decision.REDACT, Decision.TRANSFORM]:
59
+ sanitized_text = result.sanitized_text
60
+
61
+ return DetectorResult(
62
+ decision=highest_decision,
63
+ risk_score=max_risk_score,
64
+ rule_hits=all_rule_hits,
65
+ sanitized_text=sanitized_text,
66
+ user_message=user_message,
67
+ developer_message=developer_message
68
+ )
@@ -0,0 +1,48 @@
1
+ """Keyed shingle fingerprinting (OWASP LLM07 system-prompt leakage).
2
+
3
+ Privacy-preserving fingerprints: a system prompt is reduced to a set of **keyed**
4
+ hashes of its overlapping token n-grams ("shingles"). The raw prompt is never stored —
5
+ only ``HMAC-SHA256(per_org_key, shingle)`` digests. Detection HMACs the candidate
6
+ output's shingles with the SAME per-org key and counts how many match.
7
+
8
+ A per-org key (pepper) means digests are not comparable across orgs and resist offline
9
+ dictionary attack; the ``min_shingle_tokens`` floor (≥8) means trivially-guessable short
10
+ fragments are never fingerprinted. This module is dependency-free (stdlib only) so both
11
+ the control plane (builds fingerprints) and the runtime detector (matches) share it.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import hmac
16
+ import re
17
+ from hashlib import sha256
18
+ from typing import List, Set
19
+
20
+ DEFAULT_MIN_SHINGLE_TOKENS = 8
21
+ _DIGEST_HEX = 32 # truncated 128-bit digest — compact for JSONB, ample collision margin
22
+ _TOKEN_RE = re.compile(r"[a-z0-9]+")
23
+
24
+
25
+ def tokenize(text: str) -> List[str]:
26
+ return _TOKEN_RE.findall((text or "").lower())
27
+
28
+
29
+ def _shingles(tokens: List[str], n: int) -> List[str]:
30
+ if n <= 0 or len(tokens) < n:
31
+ return []
32
+ return [" ".join(tokens[i:i + n]) for i in range(len(tokens) - n + 1)]
33
+
34
+
35
+ def shingle_hashes(text: str, key: bytes, min_shingle_tokens: int = DEFAULT_MIN_SHINGLE_TOKENS) -> Set[str]:
36
+ """Keyed digests of every ``min_shingle_tokens``-token shingle in ``text``.
37
+ Returns an empty set when the text is too short to fingerprint."""
38
+ out: Set[str] = set()
39
+ for sh in _shingles(tokenize(text), min_shingle_tokens):
40
+ out.add(hmac.new(key, sh.encode("utf-8"), sha256).hexdigest()[:_DIGEST_HEX])
41
+ return out
42
+
43
+
44
+ def overlap_count(text: str, key: bytes, min_shingle_tokens: int, known: Set[str]) -> int:
45
+ """How many of ``text``'s keyed shingle digests are in the ``known`` set."""
46
+ if not known:
47
+ return 0
48
+ return len(shingle_hashes(text, key, min_shingle_tokens) & set(known))
@@ -0,0 +1,125 @@
1
+ """Canonical label / score semantics.
2
+
3
+ The single source of truth for how ZNYX labels things, so every layer, the aggregator,
4
+ the trace UI, scorecards, model cards, and benchmark scoring agree:
5
+
6
+ * **Decision set + precedence** — the 5 decisions and their "worst wins" ranking.
7
+ * **Severity ↔ risk bands** — the 3 severities and their 0–100 risk bands.
8
+ * **Confidence bands** — coarse 0–1 confidence buckets.
9
+ * **Score normalization** — mapping each layer's native score onto one 0–100 scale.
10
+
11
+ Documented for humans in ``docs/label-semantics.md`` (keep the two in sync — the doc
12
+ quotes these constants). Answers the research question "how to normalize scores
13
+ across layers".
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from enum import Enum
18
+
19
+ from znyx_core.core.models import Decision, Severity
20
+
21
+
22
+ class ConfidenceBand(str, Enum):
23
+ """Coarse confidence buckets for display/triage. Thresholds are inclusive-low."""
24
+ LOW = "low"
25
+ MEDIUM = "medium"
26
+ HIGH = "high"
27
+
28
+
29
+ # ── Decision set + precedence ────────────────────────────────────────────────
30
+ # The canonical "worst wins" ranking used when aggregating detector results
31
+ # (BLOCK > REDACT > TRANSFORM > WARN > ALLOW). The DecisionAggregator imports this so
32
+ # there is exactly one ordering in the system.
33
+ DECISION_PRECEDENCE = {
34
+ Decision.BLOCK: 5,
35
+ Decision.REDACT: 4,
36
+ Decision.TRANSFORM: 3,
37
+ Decision.WARN: 2,
38
+ Decision.ALLOW: 1,
39
+ }
40
+
41
+ # Human-facing meaning of each decision (model cards / docs / UI tooltips).
42
+ DECISION_SEMANTICS = {
43
+ Decision.ALLOW: "No action — content passes unchanged.",
44
+ Decision.WARN: "Allowed, but flagged for observability/telemetry; does not modify or stop.",
45
+ Decision.TRANSFORM: "Content is rewritten (e.g. reformatted to a contract) before passing.",
46
+ Decision.REDACT: "Sensitive spans are masked; the redacted content passes.",
47
+ Decision.BLOCK: "Content is stopped; it does not pass.",
48
+ }
49
+
50
+
51
+ def decision_rank(decision: Decision | None) -> int:
52
+ """Precedence rank of a decision (higher = more severe). None/unknown → 0."""
53
+ return DECISION_PRECEDENCE.get(decision, 0) if decision else 0
54
+
55
+
56
+ def most_severe_decision(decisions) -> Decision:
57
+ """The highest-precedence decision in an iterable (empty → ALLOW)."""
58
+ best, best_rank = Decision.ALLOW, 0
59
+ for d in decisions:
60
+ r = decision_rank(d)
61
+ if r > best_rank:
62
+ best, best_rank = d, r
63
+ return best
64
+
65
+
66
+ # ── Severity ↔ risk bands ────────────────────────────────────────────────────
67
+ # Representative 0-100 risk for each severity (used when a layer reports only a
68
+ # severity, e.g. a deterministic rule hit, and a numeric risk is needed).
69
+ SEVERITY_TO_RISK = {
70
+ Severity.LOW: 25,
71
+ Severity.MEDIUM: 60,
72
+ Severity.HIGH: 90,
73
+ }
74
+
75
+ # Inclusive-low boundaries that bucket a 0-100 risk score back into a severity.
76
+ RISK_BAND_MEDIUM_MIN = 40
77
+ RISK_BAND_HIGH_MIN = 80
78
+
79
+ # Inclusive-low boundaries that bucket a 0-1 confidence into a ConfidenceBand.
80
+ CONFIDENCE_BAND_MEDIUM_MIN = 0.5
81
+ CONFIDENCE_BAND_HIGH_MIN = 0.8
82
+
83
+
84
+ def confidence_band(confidence: float | None) -> ConfidenceBand | None:
85
+ """Bucket a 0..1 confidence. None → None (no model confidence available)."""
86
+ if confidence is None:
87
+ return None
88
+ c = min(1.0, max(0.0, float(confidence)))
89
+ if c < CONFIDENCE_BAND_MEDIUM_MIN:
90
+ return ConfidenceBand.LOW
91
+ if c < CONFIDENCE_BAND_HIGH_MIN:
92
+ return ConfidenceBand.MEDIUM
93
+ return ConfidenceBand.HIGH
94
+
95
+
96
+ def severity_to_risk(severity: Severity) -> int:
97
+ return SEVERITY_TO_RISK.get(severity, 60)
98
+
99
+
100
+ def risk_to_severity(risk: float) -> Severity:
101
+ r = min(100.0, max(0.0, float(risk)))
102
+ if r >= RISK_BAND_HIGH_MIN:
103
+ return Severity.HIGH
104
+ if r >= RISK_BAND_MEDIUM_MIN:
105
+ return Severity.MEDIUM
106
+ return Severity.LOW
107
+
108
+
109
+ def normalize_risk(value: float, kind: str = "deterministic") -> int:
110
+ """Map a layer's native score onto the common 0..100 risk scale.
111
+
112
+ - ``deterministic`` / ``judge``: already a 0..100 risk score — clamp.
113
+ - ``ml`` / ``embedding`` / ``probability``: a 0..1 probability — ×100 then clamp
114
+ ( "ML probability ×100"). Probabilities are clamped to [0,1] upstream.
115
+
116
+ Keeps the aggregator's "worst decision wins" intact while letting each layer
117
+ record both its native and normalized score for divergence analysis.
118
+ """
119
+ if value is None:
120
+ return 0
121
+ v = float(value)
122
+ kind = (kind or "deterministic").lower()
123
+ if kind in ("ml", "embedding", "probability"):
124
+ v *= 100.0
125
+ return int(round(min(100.0, max(0.0, v))))