sentinelsup 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.
- sentinelsup-0.1.0/.gitignore +7 -0
- sentinelsup-0.1.0/LICENSE +21 -0
- sentinelsup-0.1.0/PKG-INFO +177 -0
- sentinelsup-0.1.0/README.md +148 -0
- sentinelsup-0.1.0/examples/django_middleware.py +48 -0
- sentinelsup-0.1.0/examples/flask_signup_guard.py +46 -0
- sentinelsup-0.1.0/pyproject.toml +48 -0
- sentinelsup-0.1.0/sentinel/__init__.py +156 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sentinel Edge Networks LTD
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sentinelsup
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sentinel — real-time fraud, VPN, proxy, and bot detection API. Free tier, sub-40ms response.
|
|
5
|
+
Project-URL: Homepage, https://sntlhq.com
|
|
6
|
+
Project-URL: Documentation, https://sntlhq.com/api
|
|
7
|
+
Project-URL: Repository, https://github.com/sentinelsup/sentinel-python
|
|
8
|
+
Project-URL: Issues, https://github.com/sentinelsup/sentinel-python/issues
|
|
9
|
+
Author-email: Sentinel Edge Networks LTD <support@sntlhq.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: antidetect-browser,api,bot-detection,device-fingerprinting,fraud-detection,proxy-detection,sentinel,vpn-detection
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
25
|
+
Classifier: Topic :: Security
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.8
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Sentinel Python SDK
|
|
31
|
+
|
|
32
|
+
Real-time fraud, VPN, proxy, and bot detection — free tier, sub-40ms global response.
|
|
33
|
+
|
|
34
|
+
Zero dependencies. Just the standard library.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install sentinelsup
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import os
|
|
46
|
+
from sentinel import Sentinel
|
|
47
|
+
|
|
48
|
+
s = Sentinel(api_key=os.environ["SENTINEL_API_KEY"])
|
|
49
|
+
|
|
50
|
+
result = s.evaluate(token=request.json["sentinelToken"])
|
|
51
|
+
|
|
52
|
+
if result.is_suspicious:
|
|
53
|
+
return abort(403, "Sentinel flagged this session")
|
|
54
|
+
|
|
55
|
+
print(result.decision) # 'allow' | 'review' | 'block'
|
|
56
|
+
print(result.risk_score) # 0..100
|
|
57
|
+
print(result.network) # {'vpn': True, 'proxy': False, 'datacenter': True, ...}
|
|
58
|
+
print(result.reasons) # ['ip_in_known_vpn_range', 'datacenter_asn', ...]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Get your API key
|
|
62
|
+
|
|
63
|
+
Sign up at [sntlhq.com/signup](https://sntlhq.com/signup) — free, no credit card.
|
|
64
|
+
|
|
65
|
+
## Flask example — block VPN/proxy signups
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from flask import Flask, request, abort, jsonify
|
|
69
|
+
from sentinel import Sentinel, SentinelError
|
|
70
|
+
|
|
71
|
+
app = Flask(__name__)
|
|
72
|
+
sentinel = Sentinel() # reads SENTINEL_API_KEY from env
|
|
73
|
+
|
|
74
|
+
@app.route("/signup", methods=["POST"])
|
|
75
|
+
def signup():
|
|
76
|
+
data = request.get_json()
|
|
77
|
+
try:
|
|
78
|
+
result = sentinel.evaluate(token=data["sentinelToken"])
|
|
79
|
+
except SentinelError as e:
|
|
80
|
+
# Fail open OR fail closed — your call. Logged either way.
|
|
81
|
+
app.logger.warning("Sentinel error: %s", e)
|
|
82
|
+
result = None
|
|
83
|
+
|
|
84
|
+
if result and result.is_blocked:
|
|
85
|
+
abort(403, "Signup blocked")
|
|
86
|
+
|
|
87
|
+
# ... your normal signup flow
|
|
88
|
+
return jsonify({"ok": True})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Django example — middleware for high-value endpoints
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from django.http import JsonResponse
|
|
95
|
+
from sentinel import Sentinel
|
|
96
|
+
|
|
97
|
+
sentinel = Sentinel() # reads SENTINEL_API_KEY from env
|
|
98
|
+
|
|
99
|
+
class FraudCheckMiddleware:
|
|
100
|
+
def __init__(self, get_response):
|
|
101
|
+
self.get_response = get_response
|
|
102
|
+
|
|
103
|
+
def __call__(self, request):
|
|
104
|
+
if request.path.startswith("/api/checkout"):
|
|
105
|
+
token = request.META.get("HTTP_X_SENTINEL_TOKEN")
|
|
106
|
+
if token:
|
|
107
|
+
try:
|
|
108
|
+
result = sentinel.evaluate(token=token)
|
|
109
|
+
if result.is_blocked:
|
|
110
|
+
return JsonResponse({"error": "blocked"}, status=403)
|
|
111
|
+
except Exception:
|
|
112
|
+
pass # fail open
|
|
113
|
+
return self.get_response(request)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
Sentinel(
|
|
120
|
+
api_key="sk_live_...", # required (or via SENTINEL_API_KEY env var)
|
|
121
|
+
endpoint="https://sntlhq.com", # override for testing
|
|
122
|
+
timeout=5.0, # seconds
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Response shape
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
@dataclass
|
|
130
|
+
class EvaluateResult:
|
|
131
|
+
decision: str | None # 'allow' | 'review' | 'block'
|
|
132
|
+
risk_score: int | None # 0..100
|
|
133
|
+
ip: str | None
|
|
134
|
+
country: str | None # ISO-2
|
|
135
|
+
asn: str | None # 'AS16509'
|
|
136
|
+
asn_org: str | None # 'Amazon.com, Inc.'
|
|
137
|
+
network: dict # {vpn, proxy, datacenter, anonymous, residential}
|
|
138
|
+
device: dict # {headless, automation, fingerprint_age_days}
|
|
139
|
+
reasons: list[str]
|
|
140
|
+
raw: dict # full upstream response
|
|
141
|
+
|
|
142
|
+
is_suspicious: bool # True if decision != 'allow'
|
|
143
|
+
is_blocked: bool # True if decision == 'block'
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Errors
|
|
147
|
+
|
|
148
|
+
All failures raise `SentinelError`. The exception carries `.status` (HTTP code) and `.body` (parsed error body) when available.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from sentinel import Sentinel, SentinelError
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
result = sentinel.evaluate(token=tok)
|
|
155
|
+
except SentinelError as e:
|
|
156
|
+
if e.status == 429:
|
|
157
|
+
# back off
|
|
158
|
+
pass
|
|
159
|
+
elif e.status and 400 <= e.status < 500:
|
|
160
|
+
# bad input, won't recover by retrying
|
|
161
|
+
pass
|
|
162
|
+
else:
|
|
163
|
+
# transient — retry once or fail open
|
|
164
|
+
pass
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Try the API without signing up
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
curl https://sntlhq.com/v1/evaluate/sample?scenario=vpn
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Or use the [interactive playground](https://sntlhq.com/api#playground).
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Sentinel Python SDK
|
|
2
|
+
|
|
3
|
+
Real-time fraud, VPN, proxy, and bot detection — free tier, sub-40ms global response.
|
|
4
|
+
|
|
5
|
+
Zero dependencies. Just the standard library.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install sentinelsup
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import os
|
|
17
|
+
from sentinel import Sentinel
|
|
18
|
+
|
|
19
|
+
s = Sentinel(api_key=os.environ["SENTINEL_API_KEY"])
|
|
20
|
+
|
|
21
|
+
result = s.evaluate(token=request.json["sentinelToken"])
|
|
22
|
+
|
|
23
|
+
if result.is_suspicious:
|
|
24
|
+
return abort(403, "Sentinel flagged this session")
|
|
25
|
+
|
|
26
|
+
print(result.decision) # 'allow' | 'review' | 'block'
|
|
27
|
+
print(result.risk_score) # 0..100
|
|
28
|
+
print(result.network) # {'vpn': True, 'proxy': False, 'datacenter': True, ...}
|
|
29
|
+
print(result.reasons) # ['ip_in_known_vpn_range', 'datacenter_asn', ...]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Get your API key
|
|
33
|
+
|
|
34
|
+
Sign up at [sntlhq.com/signup](https://sntlhq.com/signup) — free, no credit card.
|
|
35
|
+
|
|
36
|
+
## Flask example — block VPN/proxy signups
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from flask import Flask, request, abort, jsonify
|
|
40
|
+
from sentinel import Sentinel, SentinelError
|
|
41
|
+
|
|
42
|
+
app = Flask(__name__)
|
|
43
|
+
sentinel = Sentinel() # reads SENTINEL_API_KEY from env
|
|
44
|
+
|
|
45
|
+
@app.route("/signup", methods=["POST"])
|
|
46
|
+
def signup():
|
|
47
|
+
data = request.get_json()
|
|
48
|
+
try:
|
|
49
|
+
result = sentinel.evaluate(token=data["sentinelToken"])
|
|
50
|
+
except SentinelError as e:
|
|
51
|
+
# Fail open OR fail closed — your call. Logged either way.
|
|
52
|
+
app.logger.warning("Sentinel error: %s", e)
|
|
53
|
+
result = None
|
|
54
|
+
|
|
55
|
+
if result and result.is_blocked:
|
|
56
|
+
abort(403, "Signup blocked")
|
|
57
|
+
|
|
58
|
+
# ... your normal signup flow
|
|
59
|
+
return jsonify({"ok": True})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Django example — middleware for high-value endpoints
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from django.http import JsonResponse
|
|
66
|
+
from sentinel import Sentinel
|
|
67
|
+
|
|
68
|
+
sentinel = Sentinel() # reads SENTINEL_API_KEY from env
|
|
69
|
+
|
|
70
|
+
class FraudCheckMiddleware:
|
|
71
|
+
def __init__(self, get_response):
|
|
72
|
+
self.get_response = get_response
|
|
73
|
+
|
|
74
|
+
def __call__(self, request):
|
|
75
|
+
if request.path.startswith("/api/checkout"):
|
|
76
|
+
token = request.META.get("HTTP_X_SENTINEL_TOKEN")
|
|
77
|
+
if token:
|
|
78
|
+
try:
|
|
79
|
+
result = sentinel.evaluate(token=token)
|
|
80
|
+
if result.is_blocked:
|
|
81
|
+
return JsonResponse({"error": "blocked"}, status=403)
|
|
82
|
+
except Exception:
|
|
83
|
+
pass # fail open
|
|
84
|
+
return self.get_response(request)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Configuration
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
Sentinel(
|
|
91
|
+
api_key="sk_live_...", # required (or via SENTINEL_API_KEY env var)
|
|
92
|
+
endpoint="https://sntlhq.com", # override for testing
|
|
93
|
+
timeout=5.0, # seconds
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Response shape
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
@dataclass
|
|
101
|
+
class EvaluateResult:
|
|
102
|
+
decision: str | None # 'allow' | 'review' | 'block'
|
|
103
|
+
risk_score: int | None # 0..100
|
|
104
|
+
ip: str | None
|
|
105
|
+
country: str | None # ISO-2
|
|
106
|
+
asn: str | None # 'AS16509'
|
|
107
|
+
asn_org: str | None # 'Amazon.com, Inc.'
|
|
108
|
+
network: dict # {vpn, proxy, datacenter, anonymous, residential}
|
|
109
|
+
device: dict # {headless, automation, fingerprint_age_days}
|
|
110
|
+
reasons: list[str]
|
|
111
|
+
raw: dict # full upstream response
|
|
112
|
+
|
|
113
|
+
is_suspicious: bool # True if decision != 'allow'
|
|
114
|
+
is_blocked: bool # True if decision == 'block'
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Errors
|
|
118
|
+
|
|
119
|
+
All failures raise `SentinelError`. The exception carries `.status` (HTTP code) and `.body` (parsed error body) when available.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from sentinel import Sentinel, SentinelError
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
result = sentinel.evaluate(token=tok)
|
|
126
|
+
except SentinelError as e:
|
|
127
|
+
if e.status == 429:
|
|
128
|
+
# back off
|
|
129
|
+
pass
|
|
130
|
+
elif e.status and 400 <= e.status < 500:
|
|
131
|
+
# bad input, won't recover by retrying
|
|
132
|
+
pass
|
|
133
|
+
else:
|
|
134
|
+
# transient — retry once or fail open
|
|
135
|
+
pass
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Try the API without signing up
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
curl https://sntlhq.com/v1/evaluate/sample?scenario=vpn
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Or use the [interactive playground](https://sntlhq.com/api#playground).
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Django middleware example — score every request to high-value endpoints.
|
|
2
|
+
|
|
3
|
+
Add to settings.py MIDDLEWARE:
|
|
4
|
+
"yourapp.middleware.SentinelMiddleware",
|
|
5
|
+
|
|
6
|
+
Then ensure your frontend forwards the Sentinel token via the
|
|
7
|
+
X-Sentinel-Token header (set after the SDK injects it on the client).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from django.http import JsonResponse
|
|
13
|
+
|
|
14
|
+
from sentinel import Sentinel, SentinelError
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
17
|
+
_GUARDED_PATHS = ("/api/checkout", "/api/withdraw", "/api/transfer")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SentinelMiddleware:
|
|
21
|
+
def __init__(self, get_response):
|
|
22
|
+
self.get_response = get_response
|
|
23
|
+
self.sentinel = Sentinel() # reads SENTINEL_API_KEY
|
|
24
|
+
|
|
25
|
+
def __call__(self, request):
|
|
26
|
+
if not request.path.startswith(_GUARDED_PATHS):
|
|
27
|
+
return self.get_response(request)
|
|
28
|
+
|
|
29
|
+
token = request.META.get("HTTP_X_SENTINEL_TOKEN")
|
|
30
|
+
if not token:
|
|
31
|
+
return JsonResponse({"error": "missing X-Sentinel-Token"}, status=400)
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
result = self.sentinel.evaluate(token=token)
|
|
35
|
+
except SentinelError as e:
|
|
36
|
+
log.warning("Sentinel error: %s", e)
|
|
37
|
+
# Fail open on infra problems; switch to fail-closed for finance flows.
|
|
38
|
+
return self.get_response(request)
|
|
39
|
+
|
|
40
|
+
if result.is_blocked:
|
|
41
|
+
return JsonResponse(
|
|
42
|
+
{"error": "blocked", "risk_score": result.risk_score, "reasons": result.reasons},
|
|
43
|
+
status=403,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Stash on request so the view can read decision/score
|
|
47
|
+
request.sentinel = result
|
|
48
|
+
return self.get_response(request)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Flask example — block VPN/proxy signups using Sentinel.
|
|
2
|
+
|
|
3
|
+
Run:
|
|
4
|
+
pip install flask sentinelsup
|
|
5
|
+
export SENTINEL_API_KEY=sk_live_...
|
|
6
|
+
python flask_signup_guard.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from flask import Flask, abort, jsonify, request
|
|
11
|
+
|
|
12
|
+
from sentinel import Sentinel, SentinelError
|
|
13
|
+
|
|
14
|
+
app = Flask(__name__)
|
|
15
|
+
sentinel = Sentinel() # reads SENTINEL_API_KEY
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.route("/signup", methods=["POST"])
|
|
19
|
+
def signup() -> object:
|
|
20
|
+
payload = request.get_json(force=True) or {}
|
|
21
|
+
email = (payload.get("email") or "").strip().lower()
|
|
22
|
+
token = payload.get("sentinelToken")
|
|
23
|
+
|
|
24
|
+
if not email or not token:
|
|
25
|
+
abort(400, "missing email or sentinelToken")
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
result = sentinel.evaluate(token=token)
|
|
29
|
+
except SentinelError as e:
|
|
30
|
+
# Fail open if Sentinel is down — log and continue.
|
|
31
|
+
app.logger.warning("Sentinel unavailable: %s", e)
|
|
32
|
+
result = None
|
|
33
|
+
|
|
34
|
+
if result and result.is_blocked:
|
|
35
|
+
return jsonify({"error": "Signup blocked", "reasons": result.reasons}), 403
|
|
36
|
+
|
|
37
|
+
if result and result.decision == "review":
|
|
38
|
+
# Soft challenge: email verification, manual review, slower onboarding, etc.
|
|
39
|
+
return jsonify({"ok": True, "needs_verification": True})
|
|
40
|
+
|
|
41
|
+
# Normal signup flow
|
|
42
|
+
return jsonify({"ok": True})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
app.run(port=int(os.environ.get("PORT", 5000)), debug=False)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
# NOTE: "sentinel-sdk" on PyPI is squatted by an unrelated third party
|
|
7
|
+
# ("NAST0R"). Never publish or document that name — users following it
|
|
8
|
+
# would install a stranger's code. "sentinelsup" matches the npm scope
|
|
9
|
+
# (@sentinelsup/sdk).
|
|
10
|
+
name = "sentinelsup"
|
|
11
|
+
version = "0.1.0"
|
|
12
|
+
description = "Sentinel — real-time fraud, VPN, proxy, and bot detection API. Free tier, sub-40ms response."
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
license = { text = "MIT" }
|
|
16
|
+
authors = [
|
|
17
|
+
{ name = "Sentinel Edge Networks LTD", email = "support@sntlhq.com" }
|
|
18
|
+
]
|
|
19
|
+
keywords = [
|
|
20
|
+
"fraud-detection", "vpn-detection", "proxy-detection", "bot-detection",
|
|
21
|
+
"antidetect-browser", "device-fingerprinting", "sentinel", "api"
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
29
|
+
"Programming Language :: Python :: 3.8",
|
|
30
|
+
"Programming Language :: Python :: 3.9",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3.13",
|
|
35
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
36
|
+
"Topic :: Security",
|
|
37
|
+
"Typing :: Typed",
|
|
38
|
+
]
|
|
39
|
+
dependencies = []
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://sntlhq.com"
|
|
43
|
+
Documentation = "https://sntlhq.com/api"
|
|
44
|
+
Repository = "https://github.com/sentinelsup/sentinel-python"
|
|
45
|
+
Issues = "https://github.com/sentinelsup/sentinel-python/issues"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["sentinel"]
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sentinel Python SDK — thin, dependency-free wrapper around the Sentinel
|
|
3
|
+
fraud detection API at https://sntlhq.com/v1/evaluate.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from sentinel import Sentinel
|
|
7
|
+
s = Sentinel(api_key=os.environ["SENTINEL_KEY"])
|
|
8
|
+
result = s.evaluate(token=request.json["sentinelToken"])
|
|
9
|
+
if result.is_suspicious:
|
|
10
|
+
abort(403)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Any, Dict, Optional
|
|
17
|
+
from urllib import error, request
|
|
18
|
+
|
|
19
|
+
DEFAULT_ENDPOINT = "https://sntlhq.com"
|
|
20
|
+
DEFAULT_TIMEOUT = 5.0
|
|
21
|
+
__version__ = "0.1.0"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SentinelError(Exception):
|
|
25
|
+
"""Raised on any Sentinel API or transport failure."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
message: str,
|
|
30
|
+
status: Optional[int] = None,
|
|
31
|
+
body: Optional[Dict[str, Any]] = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
super().__init__(message)
|
|
34
|
+
self.status = status
|
|
35
|
+
self.body = body or {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class EvaluateResult:
|
|
40
|
+
"""Structured response from /v1/evaluate."""
|
|
41
|
+
|
|
42
|
+
decision: Optional[str] = None
|
|
43
|
+
risk_score: Optional[int] = None
|
|
44
|
+
ip: Optional[str] = None
|
|
45
|
+
country: Optional[str] = None
|
|
46
|
+
asn: Optional[str] = None
|
|
47
|
+
asn_org: Optional[str] = None
|
|
48
|
+
network: Dict[str, Any] = field(default_factory=dict)
|
|
49
|
+
device: Dict[str, Any] = field(default_factory=dict)
|
|
50
|
+
reasons: list = field(default_factory=list)
|
|
51
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def is_suspicious(self) -> bool:
|
|
55
|
+
"""True if the decision is anything other than 'allow'."""
|
|
56
|
+
return self.decision is not None and self.decision != "allow"
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def is_blocked(self) -> bool:
|
|
60
|
+
return self.decision == "block"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Sentinel:
|
|
64
|
+
"""Sentinel API client. Pass an API key from https://sntlhq.com/dashboard."""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
api_key: Optional[str] = None,
|
|
69
|
+
endpoint: str = DEFAULT_ENDPOINT,
|
|
70
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
71
|
+
) -> None:
|
|
72
|
+
api_key = api_key or os.environ.get("SENTINEL_API_KEY")
|
|
73
|
+
if not api_key or not isinstance(api_key, str):
|
|
74
|
+
raise SentinelError(
|
|
75
|
+
"Sentinel: api_key is required. "
|
|
76
|
+
"Pass it explicitly or set SENTINEL_API_KEY. "
|
|
77
|
+
"Get one free at https://sntlhq.com/signup"
|
|
78
|
+
)
|
|
79
|
+
self.api_key = api_key
|
|
80
|
+
self.endpoint = endpoint.rstrip("/")
|
|
81
|
+
self.timeout = timeout
|
|
82
|
+
|
|
83
|
+
def evaluate(
|
|
84
|
+
self,
|
|
85
|
+
token: str,
|
|
86
|
+
fingerprint_event_id: Optional[str] = None,
|
|
87
|
+
) -> EvaluateResult:
|
|
88
|
+
"""Evaluate a visitor session for fraud signals.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
token: Sentinel client-side token from the frontend SDK.
|
|
92
|
+
fingerprint_event_id: Optional Fingerprint event id for device signals.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
EvaluateResult with decision, risk_score, network, device, and reasons.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
SentinelError: on network failure, timeout, or non-2xx response.
|
|
99
|
+
"""
|
|
100
|
+
if not token or not isinstance(token, str):
|
|
101
|
+
raise SentinelError(
|
|
102
|
+
"Sentinel.evaluate: token (client-side Sentinel token) is required"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
payload: Dict[str, Any] = {"token": token}
|
|
106
|
+
if fingerprint_event_id:
|
|
107
|
+
payload["fingerprintEventId"] = fingerprint_event_id
|
|
108
|
+
|
|
109
|
+
body = json.dumps(payload).encode("utf-8")
|
|
110
|
+
req = request.Request(
|
|
111
|
+
f"{self.endpoint}/v1/evaluate",
|
|
112
|
+
data=body,
|
|
113
|
+
method="POST",
|
|
114
|
+
headers={
|
|
115
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
116
|
+
"Content-Type": "application/json",
|
|
117
|
+
"User-Agent": f"sentinel-python/{__version__}",
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
with request.urlopen(req, timeout=self.timeout) as resp:
|
|
123
|
+
raw = resp.read().decode("utf-8")
|
|
124
|
+
data = json.loads(raw) if raw else {}
|
|
125
|
+
except error.HTTPError as e:
|
|
126
|
+
try:
|
|
127
|
+
err_body = json.loads(e.read().decode("utf-8"))
|
|
128
|
+
except Exception:
|
|
129
|
+
err_body = {}
|
|
130
|
+
msg = err_body.get("error") if isinstance(err_body, dict) else None
|
|
131
|
+
raise SentinelError(
|
|
132
|
+
f"Sentinel: API returned {e.code}"
|
|
133
|
+
+ (f" — {msg}" if msg else ""),
|
|
134
|
+
status=e.code,
|
|
135
|
+
body=err_body,
|
|
136
|
+
) from None
|
|
137
|
+
except error.URLError as e:
|
|
138
|
+
raise SentinelError(f"Sentinel: network error — {e.reason}") from None
|
|
139
|
+
except Exception as e:
|
|
140
|
+
raise SentinelError(f"Sentinel: unexpected error — {e}") from None
|
|
141
|
+
|
|
142
|
+
return EvaluateResult(
|
|
143
|
+
decision=data.get("decision"),
|
|
144
|
+
risk_score=data.get("risk_score"),
|
|
145
|
+
ip=data.get("ip"),
|
|
146
|
+
country=data.get("country"),
|
|
147
|
+
asn=data.get("asn"),
|
|
148
|
+
asn_org=data.get("asn_org"),
|
|
149
|
+
network=data.get("network") or {},
|
|
150
|
+
device=data.get("device") or {},
|
|
151
|
+
reasons=data.get("reasons") or [],
|
|
152
|
+
raw=data,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
__all__ = ["Sentinel", "SentinelError", "EvaluateResult", "__version__"]
|