zikriyon-prism 0.1.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.
- zikriyon_prism-0.1.0/PKG-INFO +65 -0
- zikriyon_prism-0.1.0/README.md +39 -0
- zikriyon_prism-0.1.0/pyproject.toml +49 -0
- zikriyon_prism-0.1.0/setup.cfg +4 -0
- zikriyon_prism-0.1.0/tests/test_engine.py +94 -0
- zikriyon_prism-0.1.0/zikriyon_prism/__init__.py +35 -0
- zikriyon_prism-0.1.0/zikriyon_prism/cloud_client.py +101 -0
- zikriyon_prism-0.1.0/zikriyon_prism/engine.py +236 -0
- zikriyon_prism-0.1.0/zikriyon_prism/license.py +57 -0
- zikriyon_prism-0.1.0/zikriyon_prism.egg-info/PKG-INFO +65 -0
- zikriyon_prism-0.1.0/zikriyon_prism.egg-info/SOURCES.txt +12 -0
- zikriyon_prism-0.1.0/zikriyon_prism.egg-info/dependency_links.txt +1 -0
- zikriyon_prism-0.1.0/zikriyon_prism.egg-info/requires.txt +7 -0
- zikriyon_prism-0.1.0/zikriyon_prism.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zikriyon-prism
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Security, moderation and trust scoring engine — local rule engine plus optional cloud ML tier.
|
|
5
|
+
Author: ZEAIPC (Zikr-e-Ameen Innovations & Programming Corporation)
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://zikriyonprism.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/zeaipc/zikriyon-prism
|
|
9
|
+
Keywords: security,moderation,trust-score,spam-detection,anomaly-detection
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: PyYAML>=6.0
|
|
21
|
+
Requires-Dist: httpx>=0.25
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
24
|
+
Requires-Dist: mypy>=1.5; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# zikriyon-prism (Python SDK)
|
|
28
|
+
|
|
29
|
+
Security, moderation and trust-scoring engine — local rule engine
|
|
30
|
+
(free, fully offline) plus an optional cloud ML tier (trust score,
|
|
31
|
+
anomaly detection, phishing NLP).
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install zikriyon-prism
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from zikriyon_prism import RuleEngine
|
|
43
|
+
|
|
44
|
+
engine = RuleEngine.from_file("rules/spam_abuse.yaml")
|
|
45
|
+
result = engine.evaluate(
|
|
46
|
+
{"message": {"text": "click here to unlock your account"}},
|
|
47
|
+
subject_id="user_123",
|
|
48
|
+
)
|
|
49
|
+
print(result.action) # "block"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Paid tier
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from zikriyon_prism import CloudClient
|
|
56
|
+
|
|
57
|
+
with CloudClient(license_key="ZP-PRO-<32 hex chars>") as client:
|
|
58
|
+
score = client.get_trust_score("user_123", signals={"login_count": 5})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
License keys are validated for *shape* client-side only. Real
|
|
62
|
+
entitlement and quota checks always happen server-side.
|
|
63
|
+
|
|
64
|
+
Full project docs, the Node.js SDK, and the cloud service:
|
|
65
|
+
https://github.com/zeaipc/zikriyon-prism
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# zikriyon-prism (Python SDK)
|
|
2
|
+
|
|
3
|
+
Security, moderation and trust-scoring engine — local rule engine
|
|
4
|
+
(free, fully offline) plus an optional cloud ML tier (trust score,
|
|
5
|
+
anomaly detection, phishing NLP).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install zikriyon-prism
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from zikriyon_prism import RuleEngine
|
|
17
|
+
|
|
18
|
+
engine = RuleEngine.from_file("rules/spam_abuse.yaml")
|
|
19
|
+
result = engine.evaluate(
|
|
20
|
+
{"message": {"text": "click here to unlock your account"}},
|
|
21
|
+
subject_id="user_123",
|
|
22
|
+
)
|
|
23
|
+
print(result.action) # "block"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Paid tier
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from zikriyon_prism import CloudClient
|
|
30
|
+
|
|
31
|
+
with CloudClient(license_key="ZP-PRO-<32 hex chars>") as client:
|
|
32
|
+
score = client.get_trust_score("user_123", signals={"login_count": 5})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
License keys are validated for *shape* client-side only. Real
|
|
36
|
+
entitlement and quota checks always happen server-side.
|
|
37
|
+
|
|
38
|
+
Full project docs, the Node.js SDK, and the cloud service:
|
|
39
|
+
https://github.com/zeaipc/zikriyon-prism
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zikriyon-prism"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Security, moderation and trust scoring engine — local rule engine plus optional cloud ML tier."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "LicenseRef-Proprietary"
|
|
12
|
+
authors = [{ name = "ZEAIPC (Zikr-e-Ameen Innovations & Programming Corporation)" }]
|
|
13
|
+
keywords = ["security", "moderation", "trust-score", "spam-detection", "anomaly-detection"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Security",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"PyYAML>=6.0",
|
|
26
|
+
"httpx>=0.25",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=7.4",
|
|
32
|
+
"mypy>=1.5",
|
|
33
|
+
"ruff>=0.1",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://zikriyonprism.dev"
|
|
38
|
+
Repository = "https://github.com/zeaipc/zikriyon-prism"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
include = ["zikriyon_prism*"]
|
|
42
|
+
|
|
43
|
+
[tool.mypy]
|
|
44
|
+
strict = true
|
|
45
|
+
python_version = "3.9"
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
line-length = 100
|
|
49
|
+
target-version = "py39"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from zikriyon_prism.engine import RuleEngine, RuleSpecError
|
|
6
|
+
|
|
7
|
+
SPEC = """
|
|
8
|
+
version: 1
|
|
9
|
+
spec_id: test_spec
|
|
10
|
+
settings:
|
|
11
|
+
case_sensitive: false
|
|
12
|
+
default_action: allow
|
|
13
|
+
max_severity_wins: true
|
|
14
|
+
severity_levels:
|
|
15
|
+
low: { weight: 1, action: flag }
|
|
16
|
+
high: { weight: 3, action: block }
|
|
17
|
+
rules:
|
|
18
|
+
- id: phishing_test
|
|
19
|
+
type: keyword_list
|
|
20
|
+
target: message.text
|
|
21
|
+
list_ref: keyword_lists.phishing
|
|
22
|
+
severity: high
|
|
23
|
+
reason: "phishing phrase"
|
|
24
|
+
- id: flood_test
|
|
25
|
+
type: rate
|
|
26
|
+
target: user.message_count
|
|
27
|
+
threshold: 3
|
|
28
|
+
window_seconds: 5
|
|
29
|
+
severity: low
|
|
30
|
+
reason: "too many messages"
|
|
31
|
+
keyword_lists:
|
|
32
|
+
phishing:
|
|
33
|
+
- "verify your account"
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def engine() -> RuleEngine:
|
|
39
|
+
return RuleEngine.from_string(SPEC)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_no_match_returns_default_action(engine: RuleEngine) -> None:
|
|
43
|
+
result = engine.evaluate({"message": {"text": "hello there"}}, subject_id="u1")
|
|
44
|
+
assert result.matched is False
|
|
45
|
+
assert result.action == "allow"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_keyword_match_blocks(engine: RuleEngine) -> None:
|
|
49
|
+
result = engine.evaluate(
|
|
50
|
+
{"message": {"text": "please verify your account now"}}, subject_id="u1"
|
|
51
|
+
)
|
|
52
|
+
assert result.matched is True
|
|
53
|
+
assert result.action == "block"
|
|
54
|
+
assert result.matches[0].rule_id == "phishing_test"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_case_insensitive_by_default(engine: RuleEngine) -> None:
|
|
58
|
+
result = engine.evaluate(
|
|
59
|
+
{"message": {"text": "VERIFY YOUR ACCOUNT"}}, subject_id="u1"
|
|
60
|
+
)
|
|
61
|
+
assert result.matched is True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_rate_rule_triggers_after_threshold(engine: RuleEngine) -> None:
|
|
65
|
+
subject = "flood_user"
|
|
66
|
+
results = [
|
|
67
|
+
engine.evaluate({"message": {"text": "hi"}}, subject_id=subject)
|
|
68
|
+
for _ in range(3)
|
|
69
|
+
]
|
|
70
|
+
assert results[-1].matched is True
|
|
71
|
+
assert results[-1].action == "flag"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_rate_rule_resets_outside_window() -> None:
|
|
75
|
+
engine = RuleEngine.from_string(SPEC.replace("window_seconds: 5", "window_seconds: 0.2"))
|
|
76
|
+
subj = "u2"
|
|
77
|
+
engine.evaluate({}, subject_id=subj)
|
|
78
|
+
time.sleep(0.3)
|
|
79
|
+
result = engine.evaluate({}, subject_id=subj)
|
|
80
|
+
assert result.matched is False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_invalid_spec_rejected() -> None:
|
|
84
|
+
with pytest.raises(RuleSpecError):
|
|
85
|
+
RuleEngine.from_string("rules: []") # missing severity_levels
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_malformed_rule_does_not_crash_evaluation() -> None:
|
|
89
|
+
bad_spec = SPEC + "\n - id: broken\n type: pattern\n severity: high\n"
|
|
90
|
+
engine = RuleEngine.from_string(bad_spec)
|
|
91
|
+
# 'pattern' rule missing required 'pattern'/'target' keys should be
|
|
92
|
+
# skipped, not raise, per production error-isolation requirement.
|
|
93
|
+
result = engine.evaluate({"message": {"text": "safe text"}}, subject_id="u3")
|
|
94
|
+
assert result.matched is False
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zikriyon Prism — security & moderation engine for QalbConverfy and beyond.
|
|
3
|
+
|
|
4
|
+
Free tier (no license needed):
|
|
5
|
+
from zikriyon_prism import RuleEngine
|
|
6
|
+
engine = RuleEngine.from_file("rules/spam_abuse.yaml")
|
|
7
|
+
result = engine.evaluate({"message": {"text": "..."}}, subject_id="user_1")
|
|
8
|
+
|
|
9
|
+
Paid tier (license key required):
|
|
10
|
+
from zikriyon_prism import CloudClient
|
|
11
|
+
with CloudClient(license_key="ZP-PRO-...") as client:
|
|
12
|
+
score = client.get_trust_score("user_1", signals={...})
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .cloud_client import CloudClient, CloudServiceError, LicenseRequiredError, TrustScoreResult
|
|
16
|
+
from .engine import EvaluationResult, RuleEngine, RuleMatch, RuleSpecError
|
|
17
|
+
from .license import InvalidLicenseKeyError, LicenseKey, load_license, parse_license
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"RuleEngine",
|
|
23
|
+
"EvaluationResult",
|
|
24
|
+
"RuleMatch",
|
|
25
|
+
"RuleSpecError",
|
|
26
|
+
"CloudClient",
|
|
27
|
+
"CloudServiceError",
|
|
28
|
+
"LicenseRequiredError",
|
|
29
|
+
"TrustScoreResult",
|
|
30
|
+
"LicenseKey",
|
|
31
|
+
"InvalidLicenseKeyError",
|
|
32
|
+
"load_license",
|
|
33
|
+
"parse_license",
|
|
34
|
+
"__version__",
|
|
35
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zikriyon Prism — cloud client (paid tier).
|
|
3
|
+
|
|
4
|
+
Thin HTTP client for the hosted ML endpoints (trust score, anomaly
|
|
5
|
+
detection, phishing NLP). Requires a valid license key. Never used
|
|
6
|
+
by the free local rule engine.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any, Dict, Optional
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
from .license import LicenseKey, load_license
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("zikriyon_prism.cloud_client")
|
|
20
|
+
|
|
21
|
+
DEFAULT_BASE_URL = "https://api.zikriyonprism.dev/v1"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CloudServiceError(RuntimeError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LicenseRequiredError(RuntimeError):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class TrustScoreResult:
|
|
34
|
+
user_id: str
|
|
35
|
+
score: float # 0.0 (untrusted) - 1.0 (fully trusted)
|
|
36
|
+
factors: Dict[str, float]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CloudClient:
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
license_key: Optional[str] = None,
|
|
43
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
44
|
+
timeout_seconds: float = 5.0,
|
|
45
|
+
) -> None:
|
|
46
|
+
resolved = load_license(license_key)
|
|
47
|
+
if resolved is None:
|
|
48
|
+
raise LicenseRequiredError(
|
|
49
|
+
"CloudClient requires a license key (pass explicitly or set "
|
|
50
|
+
"ZIKRIYON_PRISM_KEY). The free local RuleEngine does not need one."
|
|
51
|
+
)
|
|
52
|
+
self._license: LicenseKey = resolved
|
|
53
|
+
self._base_url = base_url.rstrip("/")
|
|
54
|
+
self._client = httpx.Client(timeout=timeout_seconds)
|
|
55
|
+
|
|
56
|
+
def close(self) -> None:
|
|
57
|
+
self._client.close()
|
|
58
|
+
|
|
59
|
+
def __enter__(self) -> "CloudClient":
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def __exit__(self, *_exc: object) -> None:
|
|
63
|
+
self.close()
|
|
64
|
+
|
|
65
|
+
def _headers(self) -> Dict[str, str]:
|
|
66
|
+
return {
|
|
67
|
+
"Authorization": f"Bearer {self._license.raw}",
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def get_trust_score(self, user_id: str, signals: Dict[str, Any]) -> TrustScoreResult:
|
|
72
|
+
try:
|
|
73
|
+
resp = self._client.post(
|
|
74
|
+
f"{self._base_url}/trust-score",
|
|
75
|
+
headers=self._headers(),
|
|
76
|
+
json={"user_id": user_id, "signals": signals},
|
|
77
|
+
)
|
|
78
|
+
resp.raise_for_status()
|
|
79
|
+
except httpx.HTTPStatusError as exc:
|
|
80
|
+
raise CloudServiceError(
|
|
81
|
+
f"Trust score request failed: {exc.response.status_code} {exc.response.text}"
|
|
82
|
+
) from exc
|
|
83
|
+
except httpx.HTTPError as exc:
|
|
84
|
+
raise CloudServiceError(f"Trust score request failed: {exc}") from exc
|
|
85
|
+
|
|
86
|
+
data = resp.json()
|
|
87
|
+
return TrustScoreResult(
|
|
88
|
+
user_id=user_id, score=data["score"], factors=data.get("factors", {})
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def check_anomaly(self, subject_id: str, features: Dict[str, Any]) -> Dict[str, Any]:
|
|
92
|
+
try:
|
|
93
|
+
resp = self._client.post(
|
|
94
|
+
f"{self._base_url}/anomaly",
|
|
95
|
+
headers=self._headers(),
|
|
96
|
+
json={"subject_id": subject_id, "features": features},
|
|
97
|
+
)
|
|
98
|
+
resp.raise_for_status()
|
|
99
|
+
except httpx.HTTPError as exc:
|
|
100
|
+
raise CloudServiceError(f"Anomaly check failed: {exc}") from exc
|
|
101
|
+
return resp.json()
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zikriyon Prism — local rule engine (free tier).
|
|
3
|
+
|
|
4
|
+
Loads the shared, language-agnostic YAML rule spec and evaluates events
|
|
5
|
+
against it. Fully local, no network calls, no data leaves the process.
|
|
6
|
+
|
|
7
|
+
This module intentionally mirrors node-sdk/src/engine.ts rule-for-rule.
|
|
8
|
+
If you change matching semantics here, update the TypeScript port too —
|
|
9
|
+
the YAML spec is the shared contract, this file is the Python side of it.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import re
|
|
16
|
+
import time
|
|
17
|
+
from collections import deque
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Deque, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
import yaml
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("zikriyon_prism.engine")
|
|
25
|
+
|
|
26
|
+
_VALID_RULE_TYPES = {"pattern", "pattern_count", "keyword_list", "rate", "variance_check"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RuleSpecError(ValueError):
|
|
30
|
+
"""Raised when the YAML rule spec is malformed or internally inconsistent."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class RuleMatch:
|
|
35
|
+
rule_id: str
|
|
36
|
+
severity: str
|
|
37
|
+
action: str
|
|
38
|
+
reason: str
|
|
39
|
+
weight: int
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class EvaluationResult:
|
|
44
|
+
matched: bool
|
|
45
|
+
action: str
|
|
46
|
+
matches: List[RuleMatch] = field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def highest_severity_match(self) -> Optional[RuleMatch]:
|
|
50
|
+
if not self.matches:
|
|
51
|
+
return None
|
|
52
|
+
return max(self.matches, key=lambda m: m.weight)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _SlidingWindowCounter:
|
|
56
|
+
"""Tracks event timestamps per (rule_id, subject) for rate-based rules."""
|
|
57
|
+
|
|
58
|
+
def __init__(self) -> None:
|
|
59
|
+
self._windows: Dict[str, Deque[float]] = {}
|
|
60
|
+
|
|
61
|
+
def record_and_count(self, key: str, window_seconds: float, now: Optional[float] = None) -> int:
|
|
62
|
+
now = now if now is not None else time.time()
|
|
63
|
+
dq = self._windows.setdefault(key, deque())
|
|
64
|
+
dq.append(now)
|
|
65
|
+
cutoff = now - window_seconds
|
|
66
|
+
while dq and dq[0] < cutoff:
|
|
67
|
+
dq.popleft()
|
|
68
|
+
return len(dq)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class RuleEngine:
|
|
72
|
+
"""
|
|
73
|
+
Loads a Zikriyon Prism YAML rule spec and evaluates events against it.
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
engine = RuleEngine.from_file("core-engine/rules/spam_abuse.yaml")
|
|
77
|
+
result = engine.evaluate({
|
|
78
|
+
"message": {"text": "click here to unlock your account"},
|
|
79
|
+
}, subject_id="user_123")
|
|
80
|
+
if result.matched:
|
|
81
|
+
print(result.action, [m.reason for m in result.matches])
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, spec: Dict[str, Any]) -> None:
|
|
85
|
+
self._validate_spec(spec)
|
|
86
|
+
self.spec = spec
|
|
87
|
+
self.settings = spec.get("settings", {})
|
|
88
|
+
self.severity_levels: Dict[str, Dict[str, Any]] = spec["severity_levels"]
|
|
89
|
+
self.rules: List[Dict[str, Any]] = spec["rules"]
|
|
90
|
+
self.keyword_lists: Dict[str, List[str]] = spec.get("keyword_lists", {})
|
|
91
|
+
self._case_sensitive: bool = bool(self.settings.get("case_sensitive", False))
|
|
92
|
+
self._default_action: str = self.settings.get("default_action", "allow")
|
|
93
|
+
self._counters = _SlidingWindowCounter()
|
|
94
|
+
self._compiled_patterns: Dict[str, "re.Pattern[str]"] = {}
|
|
95
|
+
|
|
96
|
+
# ---- construction -----------------------------------------------------
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_file(cls, path: str | Path) -> "RuleEngine":
|
|
100
|
+
path = Path(path)
|
|
101
|
+
with path.open("r", encoding="utf-8") as f:
|
|
102
|
+
spec = yaml.safe_load(f)
|
|
103
|
+
logger.info("Loaded rule spec '%s' from %s", spec.get("spec_id", "?"), path)
|
|
104
|
+
return cls(spec)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_string(cls, yaml_text: str) -> "RuleEngine":
|
|
108
|
+
spec = yaml.safe_load(yaml_text)
|
|
109
|
+
return cls(spec)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def _validate_spec(spec: Dict[str, Any]) -> None:
|
|
113
|
+
if not isinstance(spec, dict):
|
|
114
|
+
raise RuleSpecError("Rule spec must be a mapping at the top level")
|
|
115
|
+
for required in ("severity_levels", "rules"):
|
|
116
|
+
if required not in spec:
|
|
117
|
+
raise RuleSpecError(f"Rule spec missing required key: '{required}'")
|
|
118
|
+
for rule in spec["rules"]:
|
|
119
|
+
if rule.get("type") not in _VALID_RULE_TYPES:
|
|
120
|
+
raise RuleSpecError(
|
|
121
|
+
f"Rule '{rule.get('id', '?')}' has invalid type "
|
|
122
|
+
f"'{rule.get('type')}'. Must be one of {_VALID_RULE_TYPES}"
|
|
123
|
+
)
|
|
124
|
+
if rule.get("severity") not in spec["severity_levels"]:
|
|
125
|
+
raise RuleSpecError(
|
|
126
|
+
f"Rule '{rule.get('id', '?')}' references unknown severity "
|
|
127
|
+
f"'{rule.get('severity')}'"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# ---- evaluation ---------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
def evaluate(self, event: Dict[str, Any], subject_id: str = "default") -> EvaluationResult:
|
|
133
|
+
"""
|
|
134
|
+
Evaluate a single event (e.g. an incoming message or auth attempt)
|
|
135
|
+
against every rule in the spec. `subject_id` scopes rate/variance
|
|
136
|
+
counters per user/session — pass a stable ID (user id, IP, etc).
|
|
137
|
+
"""
|
|
138
|
+
matches: List[RuleMatch] = []
|
|
139
|
+
|
|
140
|
+
for rule in self.rules:
|
|
141
|
+
try:
|
|
142
|
+
if self._rule_matches(rule, event, subject_id):
|
|
143
|
+
sev = rule["severity"]
|
|
144
|
+
level = self.severity_levels[sev]
|
|
145
|
+
matches.append(
|
|
146
|
+
RuleMatch(
|
|
147
|
+
rule_id=rule["id"],
|
|
148
|
+
severity=sev,
|
|
149
|
+
action=level["action"],
|
|
150
|
+
reason=rule.get("reason", ""),
|
|
151
|
+
weight=level["weight"],
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
except Exception:
|
|
155
|
+
# A single malformed/failing rule must never take down
|
|
156
|
+
# the whole evaluation pass in production.
|
|
157
|
+
logger.exception("Rule '%s' raised during evaluation; skipping", rule.get("id"))
|
|
158
|
+
|
|
159
|
+
if not matches:
|
|
160
|
+
return EvaluationResult(matched=False, action=self._default_action)
|
|
161
|
+
|
|
162
|
+
if self.settings.get("max_severity_wins", True):
|
|
163
|
+
top = max(matches, key=lambda m: m.weight)
|
|
164
|
+
return EvaluationResult(matched=True, action=top.action, matches=matches)
|
|
165
|
+
|
|
166
|
+
return EvaluationResult(matched=True, action=matches[0].action, matches=matches)
|
|
167
|
+
|
|
168
|
+
# ---- rule type handlers -------------------------------------------------
|
|
169
|
+
|
|
170
|
+
def _rule_matches(self, rule: Dict[str, Any], event: Dict[str, Any], subject_id: str) -> bool:
|
|
171
|
+
rtype = rule["type"]
|
|
172
|
+
if rtype == "pattern":
|
|
173
|
+
return self._match_pattern(rule, event)
|
|
174
|
+
if rtype == "pattern_count":
|
|
175
|
+
return self._match_pattern_count(rule, event)
|
|
176
|
+
if rtype == "keyword_list":
|
|
177
|
+
return self._match_keyword_list(rule, event)
|
|
178
|
+
if rtype == "rate":
|
|
179
|
+
return self._match_rate(rule, event, subject_id)
|
|
180
|
+
if rtype == "variance_check":
|
|
181
|
+
return self._match_variance(rule, event)
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
def _get_target(self, target_path: str, event: Dict[str, Any]) -> Any:
|
|
185
|
+
node: Any = event
|
|
186
|
+
for part in target_path.split("."):
|
|
187
|
+
if not isinstance(node, dict) or part not in node:
|
|
188
|
+
return None
|
|
189
|
+
node = node[part]
|
|
190
|
+
return node
|
|
191
|
+
|
|
192
|
+
def _compile(self, pattern: str) -> "re.Pattern[str]":
|
|
193
|
+
if pattern not in self._compiled_patterns:
|
|
194
|
+
flags = 0 if self._case_sensitive else re.IGNORECASE
|
|
195
|
+
self._compiled_patterns[pattern] = re.compile(pattern, flags)
|
|
196
|
+
return self._compiled_patterns[pattern]
|
|
197
|
+
|
|
198
|
+
def _match_pattern(self, rule: Dict[str, Any], event: Dict[str, Any]) -> bool:
|
|
199
|
+
text = self._get_target(rule["target"], event)
|
|
200
|
+
if not isinstance(text, str):
|
|
201
|
+
return False
|
|
202
|
+
return self._compile(rule["pattern"]).search(text) is not None
|
|
203
|
+
|
|
204
|
+
def _match_pattern_count(self, rule: Dict[str, Any], event: Dict[str, Any]) -> bool:
|
|
205
|
+
text = self._get_target(rule["target"], event)
|
|
206
|
+
if not isinstance(text, str):
|
|
207
|
+
return False
|
|
208
|
+
count = len(self._compile(rule["pattern"]).findall(text))
|
|
209
|
+
return count >= rule["threshold"]
|
|
210
|
+
|
|
211
|
+
def _match_keyword_list(self, rule: Dict[str, Any], event: Dict[str, Any]) -> bool:
|
|
212
|
+
text = self._get_target(rule["target"], event)
|
|
213
|
+
if not isinstance(text, str):
|
|
214
|
+
return False
|
|
215
|
+
haystack = text if self._case_sensitive else text.lower()
|
|
216
|
+
list_ref = rule["list_ref"].split(".", 1)[1] # "keyword_lists.X" -> "X"
|
|
217
|
+
keywords = self.keyword_lists.get(list_ref, [])
|
|
218
|
+
for kw in keywords:
|
|
219
|
+
needle = kw if self._case_sensitive else kw.lower()
|
|
220
|
+
if needle and needle in haystack:
|
|
221
|
+
return True
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
def _match_rate(self, rule: Dict[str, Any], event: Dict[str, Any], subject_id: str) -> bool:
|
|
225
|
+
# `target` here identifies the counter stream, not a literal value lookup.
|
|
226
|
+
key = f"{rule['id']}:{subject_id}"
|
|
227
|
+
count = self._counters.record_and_count(key, rule["window_seconds"])
|
|
228
|
+
return count >= rule["threshold"]
|
|
229
|
+
|
|
230
|
+
def _match_variance(self, rule: Dict[str, Any], event: Dict[str, Any]) -> bool:
|
|
231
|
+
samples = self._get_target(rule["target"], event)
|
|
232
|
+
if not isinstance(samples, list) or len(samples) < rule.get("min_samples", 5):
|
|
233
|
+
return False
|
|
234
|
+
mean = sum(samples) / len(samples)
|
|
235
|
+
variance = sum((x - mean) ** 2 for x in samples) / len(samples)
|
|
236
|
+
return variance <= rule.get("max_variance_ms", 50) ** 2
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zikriyon Prism — license key handling.
|
|
3
|
+
|
|
4
|
+
The local rule engine (engine.py) never requires a license — it is fully
|
|
5
|
+
free and offline. A license key is only required when using cloud_client.py
|
|
6
|
+
to call the hosted ML tier (trust score, anomaly detection, phishing NLP).
|
|
7
|
+
|
|
8
|
+
Key format: ZP-<plan>-<32 hex chars>, e.g. ZP-PRO-9f1c2e4a7b3d4f5e6a7b8c9d0e1f2a3b
|
|
9
|
+
This module only validates *shape* locally; real entitlement/quota checks
|
|
10
|
+
always happen server-side in cloud-service — never trust a client-side
|
|
11
|
+
"valid" result for authorization decisions.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
_KEY_PATTERN = re.compile(r"^ZP-(FREE|PRO|ENTERPRISE)-[0-9a-fA-F]{32}$")
|
|
22
|
+
|
|
23
|
+
ENV_VAR = "ZIKRIYON_PRISM_KEY"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InvalidLicenseKeyError(ValueError):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class LicenseKey:
|
|
32
|
+
raw: str
|
|
33
|
+
plan: str
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str: # never leak the full key in logs/repr
|
|
36
|
+
return f"LicenseKey(plan={self.plan!r}, key='ZP-{self.plan}-****')"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_license(explicit_key: Optional[str] = None) -> Optional[LicenseKey]:
|
|
40
|
+
"""
|
|
41
|
+
Resolve a license key from an explicit argument or the
|
|
42
|
+
ZIKRIYON_PRISM_KEY environment variable. Returns None for
|
|
43
|
+
unauthenticated/free-tier usage (local engine only).
|
|
44
|
+
"""
|
|
45
|
+
raw = explicit_key or os.environ.get(ENV_VAR)
|
|
46
|
+
if not raw:
|
|
47
|
+
return None
|
|
48
|
+
return parse_license(raw)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_license(raw: str) -> LicenseKey:
|
|
52
|
+
match = _KEY_PATTERN.match(raw.strip())
|
|
53
|
+
if not match:
|
|
54
|
+
raise InvalidLicenseKeyError(
|
|
55
|
+
"License key must match ZP-<FREE|PRO|ENTERPRISE>-<32 hex chars>"
|
|
56
|
+
)
|
|
57
|
+
return LicenseKey(raw=raw.strip(), plan=match.group(1))
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zikriyon-prism
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Security, moderation and trust scoring engine — local rule engine plus optional cloud ML tier.
|
|
5
|
+
Author: ZEAIPC (Zikr-e-Ameen Innovations & Programming Corporation)
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://zikriyonprism.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/zeaipc/zikriyon-prism
|
|
9
|
+
Keywords: security,moderation,trust-score,spam-detection,anomaly-detection
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: PyYAML>=6.0
|
|
21
|
+
Requires-Dist: httpx>=0.25
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
24
|
+
Requires-Dist: mypy>=1.5; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# zikriyon-prism (Python SDK)
|
|
28
|
+
|
|
29
|
+
Security, moderation and trust-scoring engine — local rule engine
|
|
30
|
+
(free, fully offline) plus an optional cloud ML tier (trust score,
|
|
31
|
+
anomaly detection, phishing NLP).
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install zikriyon-prism
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from zikriyon_prism import RuleEngine
|
|
43
|
+
|
|
44
|
+
engine = RuleEngine.from_file("rules/spam_abuse.yaml")
|
|
45
|
+
result = engine.evaluate(
|
|
46
|
+
{"message": {"text": "click here to unlock your account"}},
|
|
47
|
+
subject_id="user_123",
|
|
48
|
+
)
|
|
49
|
+
print(result.action) # "block"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Paid tier
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from zikriyon_prism import CloudClient
|
|
56
|
+
|
|
57
|
+
with CloudClient(license_key="ZP-PRO-<32 hex chars>") as client:
|
|
58
|
+
score = client.get_trust_score("user_123", signals={"login_count": 5})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
License keys are validated for *shape* client-side only. Real
|
|
62
|
+
entitlement and quota checks always happen server-side.
|
|
63
|
+
|
|
64
|
+
Full project docs, the Node.js SDK, and the cloud service:
|
|
65
|
+
https://github.com/zeaipc/zikriyon-prism
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
tests/test_engine.py
|
|
4
|
+
zikriyon_prism/__init__.py
|
|
5
|
+
zikriyon_prism/cloud_client.py
|
|
6
|
+
zikriyon_prism/engine.py
|
|
7
|
+
zikriyon_prism/license.py
|
|
8
|
+
zikriyon_prism.egg-info/PKG-INFO
|
|
9
|
+
zikriyon_prism.egg-info/SOURCES.txt
|
|
10
|
+
zikriyon_prism.egg-info/dependency_links.txt
|
|
11
|
+
zikriyon_prism.egg-info/requires.txt
|
|
12
|
+
zikriyon_prism.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
zikriyon_prism
|