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.
- znyx_core-1.0.0/PKG-INFO +54 -0
- znyx_core-1.0.0/README.md +23 -0
- znyx_core-1.0.0/pyproject.toml +49 -0
- znyx_core-1.0.0/setup.cfg +4 -0
- znyx_core-1.0.0/src/znyx_core/__init__.py +0 -0
- znyx_core-1.0.0/src/znyx_core/config/__init__.py +0 -0
- znyx_core-1.0.0/src/znyx_core/config/tunables.py +194 -0
- znyx_core-1.0.0/src/znyx_core/core/__init__.py +1 -0
- znyx_core-1.0.0/src/znyx_core/core/decision.py +68 -0
- znyx_core-1.0.0/src/znyx_core/core/fingerprint.py +48 -0
- znyx_core-1.0.0/src/znyx_core/core/labels.py +125 -0
- znyx_core-1.0.0/src/znyx_core/core/models.py +409 -0
- znyx_core-1.0.0/src/znyx_core/core/risk.py +26 -0
- znyx_core-1.0.0/src/znyx_core/core/text_normalize.py +70 -0
- znyx_core-1.0.0/src/znyx_core/detectors/__init__.py +1 -0
- znyx_core-1.0.0/src/znyx_core/detectors/_injection_patterns.py +103 -0
- znyx_core-1.0.0/src/znyx_core/detectors/abuse.py +332 -0
- znyx_core-1.0.0/src/znyx_core/detectors/adapters/__init__.py +34 -0
- znyx_core-1.0.0/src/znyx_core/detectors/adapters/aws_bedrock_guardrails.py +215 -0
- znyx_core-1.0.0/src/znyx_core/detectors/adapters/azure_content_safety.py +134 -0
- znyx_core-1.0.0/src/znyx_core/detectors/adapters/openai_moderation.py +97 -0
- znyx_core-1.0.0/src/znyx_core/detectors/bias.py +293 -0
- znyx_core-1.0.0/src/znyx_core/detectors/citation_integrity.py +219 -0
- znyx_core-1.0.0/src/znyx_core/detectors/code_safety.py +317 -0
- znyx_core-1.0.0/src/znyx_core/detectors/competitor.py +361 -0
- znyx_core-1.0.0/src/znyx_core/detectors/compliance.py +354 -0
- znyx_core-1.0.0/src/znyx_core/detectors/copyright.py +285 -0
- znyx_core-1.0.0/src/znyx_core/detectors/document_metadata_leakage.py +82 -0
- znyx_core-1.0.0/src/znyx_core/detectors/embedding_integrity.py +118 -0
- znyx_core-1.0.0/src/znyx_core/detectors/excessive_agency.py +155 -0
- znyx_core-1.0.0/src/znyx_core/detectors/exfiltration.py +157 -0
- znyx_core-1.0.0/src/znyx_core/detectors/gibberish.py +277 -0
- znyx_core-1.0.0/src/znyx_core/detectors/hallucination.py +275 -0
- znyx_core-1.0.0/src/znyx_core/detectors/jailbreak.py +1353 -0
- znyx_core-1.0.0/src/znyx_core/detectors/language.py +268 -0
- znyx_core-1.0.0/src/znyx_core/detectors/malicious_url.py +381 -0
- znyx_core-1.0.0/src/znyx_core/detectors/mcp_manifest_scanner.py +390 -0
- znyx_core-1.0.0/src/znyx_core/detectors/memory_write_poisoning.py +67 -0
- znyx_core-1.0.0/src/znyx_core/detectors/numerical_consistency.py +93 -0
- znyx_core-1.0.0/src/znyx_core/detectors/pii.py +1373 -0
- znyx_core-1.0.0/src/znyx_core/detectors/plugin.py +302 -0
- znyx_core-1.0.0/src/znyx_core/detectors/remote.py +366 -0
- znyx_core-1.0.0/src/znyx_core/detectors/retrieval_chunk_injection.py +64 -0
- znyx_core-1.0.0/src/znyx_core/detectors/secrets.py +393 -0
- znyx_core-1.0.0/src/znyx_core/detectors/sensitive_business_data.py +135 -0
- znyx_core-1.0.0/src/znyx_core/detectors/sentiment.py +258 -0
- znyx_core-1.0.0/src/znyx_core/detectors/structure.py +384 -0
- znyx_core-1.0.0/src/znyx_core/detectors/system_prompt_leakage.py +76 -0
- znyx_core-1.0.0/src/znyx_core/detectors/tool_output_injection.py +68 -0
- znyx_core-1.0.0/src/znyx_core/detectors/tools.py +331 -0
- znyx_core-1.0.0/src/znyx_core/detectors/topic.py +1647 -0
- znyx_core-1.0.0/src/znyx_core/detectors/toxicity.py +1707 -0
- znyx_core-1.0.0/src/znyx_core/detectors/unbounded_consumption.py +182 -0
- znyx_core-1.0.0/src/znyx_core/engine/__init__.py +1 -0
- znyx_core-1.0.0/src/znyx_core/engine/backends.py +230 -0
- znyx_core-1.0.0/src/znyx_core/engine/consensus.py +91 -0
- znyx_core-1.0.0/src/znyx_core/engine/detector_registry.py +122 -0
- znyx_core-1.0.0/src/znyx_core/engine/egress.py +270 -0
- znyx_core-1.0.0/src/znyx_core/engine/escalation.py +322 -0
- znyx_core-1.0.0/src/znyx_core/engine/eval_metrics.py +216 -0
- znyx_core-1.0.0/src/znyx_core/engine/evaluator.py +509 -0
- znyx_core-1.0.0/src/znyx_core/engine/judge_catalog.py +127 -0
- znyx_core-1.0.0/src/znyx_core/engine/judge_escalation.py +180 -0
- znyx_core-1.0.0/src/znyx_core/engine/judge_runtime.py +212 -0
- znyx_core-1.0.0/src/znyx_core/engine/ml_catalog.py +307 -0
- znyx_core-1.0.0/src/znyx_core/engine/orchestrator.py +595 -0
- znyx_core-1.0.0/src/znyx_core/engine/owasp_map.py +274 -0
- znyx_core-1.0.0/src/znyx_core/engine/quality/__init__.py +1 -0
- znyx_core-1.0.0/src/znyx_core/engine/quality/coherence.py +108 -0
- znyx_core-1.0.0/src/znyx_core/engine/quality/fluency.py +130 -0
- znyx_core-1.0.0/src/znyx_core/engine/quality/groundedness.py +174 -0
- znyx_core-1.0.0/src/znyx_core/engine/quality/intent_resolution.py +141 -0
- znyx_core-1.0.0/src/znyx_core/engine/quality/judge_evaluator.py +247 -0
- znyx_core-1.0.0/src/znyx_core/engine/quality/nli_client.py +245 -0
- znyx_core-1.0.0/src/znyx_core/engine/quality/relevance.py +101 -0
- znyx_core-1.0.0/src/znyx_core/engine/quality/task_adherence.py +157 -0
- znyx_core-1.0.0/src/znyx_core/engine/quality/tool_accuracy.py +125 -0
- znyx_core-1.0.0/src/znyx_core/engine/quality_scorer.py +198 -0
- znyx_core-1.0.0/src/znyx_core/engine/remediation.py +318 -0
- znyx_core-1.0.0/src/znyx_core/engine/scorecard_gate.py +217 -0
- znyx_core-1.0.0/src/znyx_core/engine/scorecard_stamp.py +98 -0
- znyx_core-1.0.0/src/znyx_core/engine/streaming.py +162 -0
- znyx_core-1.0.0/src/znyx_core/engine/tool_registration_hook.py +22 -0
- znyx_core-1.0.0/src/znyx_core/llm/__init__.py +0 -0
- znyx_core-1.0.0/src/znyx_core/llm/judge.py +280 -0
- znyx_core-1.0.0/src/znyx_core/llm/providers.py +510 -0
- znyx_core-1.0.0/src/znyx_core/llm/round_trip.py +375 -0
- znyx_core-1.0.0/src/znyx_core/middleware/__init__.py +1 -0
- znyx_core-1.0.0/src/znyx_core/middleware/auth.py +178 -0
- znyx_core-1.0.0/src/znyx_core/middleware/csrf.py +148 -0
- znyx_core-1.0.0/src/znyx_core/middleware/metrics.py +283 -0
- znyx_core-1.0.0/src/znyx_core/middleware/otel.py +172 -0
- znyx_core-1.0.0/src/znyx_core/middleware/rate_limit.py +253 -0
- znyx_core-1.0.0/src/znyx_core/net_guard.py +146 -0
- znyx_core-1.0.0/src/znyx_core/policy/__init__.py +1 -0
- znyx_core-1.0.0/src/znyx_core/policy/bundle.py +266 -0
- znyx_core-1.0.0/src/znyx_core/policy/db_resolver_registry.py +34 -0
- znyx_core-1.0.0/src/znyx_core/policy/lint.py +66 -0
- znyx_core-1.0.0/src/znyx_core/policy/loader.py +83 -0
- znyx_core-1.0.0/src/znyx_core/policy/resolver.py +49 -0
- znyx_core-1.0.0/src/znyx_core/policy/schema.py +672 -0
- znyx_core-1.0.0/src/znyx_core/services/__init__.py +0 -0
- znyx_core-1.0.0/src/znyx_core/utils/__init__.py +1 -0
- znyx_core-1.0.0/src/znyx_core/utils/checksums.py +147 -0
- znyx_core-1.0.0/src/znyx_core/utils/env.py +39 -0
- znyx_core-1.0.0/src/znyx_core/utils/hash.py +23 -0
- znyx_core-1.0.0/src/znyx_core/utils/luhn.py +39 -0
- znyx_core-1.0.0/src/znyx_core/validation.py +18 -0
- znyx_core-1.0.0/src/znyx_core.egg-info/PKG-INFO +54 -0
- znyx_core-1.0.0/src/znyx_core.egg-info/SOURCES.txt +111 -0
- znyx_core-1.0.0/src/znyx_core.egg-info/dependency_links.txt +1 -0
- znyx_core-1.0.0/src/znyx_core.egg-info/requires.txt +10 -0
- znyx_core-1.0.0/src/znyx_core.egg-info/top_level.txt +1 -0
znyx_core-1.0.0/PKG-INFO
ADDED
|
@@ -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"]
|
|
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))))
|