analytics-platform 0.3.8__tar.gz → 0.3.10__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.
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/PKG-INFO +5 -1
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/core.py +31 -9
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/firewall.py +3 -1
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/proxy.py +17 -2
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform.egg-info/PKG-INFO +5 -1
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform.egg-info/SOURCES.txt +2 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/pyproject.toml +7 -1
- analytics_platform-0.3.10/tests/test_headers.py +28 -0
- analytics_platform-0.3.10/tests/test_proxy_answer.py +27 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/LICENSE +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/README.md +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/__init__.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/_env.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/analytics.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/audit.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/calls.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/celery.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/challenge.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/crawlers.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/django.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/dramatiq.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/errors.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/flags.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/forms.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/inventory.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/maintenance.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/performance.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/posture.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/queries.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/queues.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/request_rules.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/resources.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/rq.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform/start.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform.egg-info/dependency_links.txt +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform.egg-info/top_level.txt +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/setup.cfg +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_calls_queues.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_celery.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_challenge.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_checkin.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_crawlers.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_errors.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_firewall.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_hardening.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_limits.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_page_limits.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_proxy.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_queries.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_redis.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_request_rules.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_resources.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_rq_dramatiq.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_start.py +0 -0
- {analytics_platform-0.3.8 → analytics_platform-0.3.10}/tests/test_static_late.py +0 -0
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: analytics-platform
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.10
|
|
4
4
|
Summary: Firewall, error monitoring and server analytics for your Python server, run from your analytics dashboard.
|
|
5
5
|
Author: steviec87
|
|
6
6
|
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://sitegoose.com
|
|
8
|
+
Project-URL: Source, https://github.com/StevieC87/Sitegoose-Packages/tree/main/platform-python
|
|
9
|
+
Project-URL: Issues, https://github.com/StevieC87/Sitegoose-Packages/issues
|
|
10
|
+
Project-URL: Security, https://sitegoose.com/security
|
|
7
11
|
Keywords: firewall,waf,rate-limit,error-monitoring,analytics,server-monitoring,django,flask,fastapi,celery,rq,dramatiq,wsgi,asgi
|
|
8
12
|
Classifier: Development Status :: 4 - Beta
|
|
9
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -23,7 +23,7 @@ import urllib.error
|
|
|
23
23
|
import urllib.parse
|
|
24
24
|
import urllib.request
|
|
25
25
|
|
|
26
|
-
VERSION = "0.3.
|
|
26
|
+
VERSION = "0.3.10"
|
|
27
27
|
TIMEOUT = 5
|
|
28
28
|
MIN_SYNC = 30
|
|
29
29
|
RETRY = 60
|
|
@@ -285,19 +285,24 @@ CLIENTS = ("python", "django")
|
|
|
285
285
|
|
|
286
286
|
|
|
287
287
|
|
|
288
|
-
# A dashboard header is added only when it is
|
|
289
|
-
#
|
|
290
|
-
#
|
|
291
|
-
#
|
|
292
|
-
#
|
|
293
|
-
|
|
294
|
-
|
|
288
|
+
# A dashboard header is added only when it is one of the nine the dashboard
|
|
289
|
+
# can produce (expresscollect/Analytics/platform.js, headersOf), in any case,
|
|
290
|
+
# with a value of visible ASCII, spaces and tabs, so no line break, control
|
|
291
|
+
# character or byte a server rejects gets through. A list of what's allowed,
|
|
292
|
+
# not of what isn't: whoever got into our servers still couldn't make this
|
|
293
|
+
# app grant another site its users' data (Access-Control-Allow-*), send every
|
|
294
|
+
# page elsewhere (Refresh), log everyone out (Clear-Site-Data) or change what
|
|
295
|
+
# a response is. Shared cases: sdk/protocols/header-cases.json.
|
|
296
|
+
SECURITY_HEADERS = frozenset((
|
|
297
|
+
"strict-transport-security", "x-content-type-options", "x-frame-options", "referrer-policy",
|
|
298
|
+
"permissions-policy", "cross-origin-opener-policy", "content-security-policy",
|
|
299
|
+
"content-security-policy-report-only", "reporting-endpoints"))
|
|
295
300
|
_HEADER_VALUE = re.compile(r"[\x20-\x7e\t]*")
|
|
296
301
|
|
|
297
302
|
|
|
298
303
|
def safe_header(pair):
|
|
299
304
|
return (isinstance(pair, (list, tuple)) and len(pair) == 2 and isinstance(pair[0], str)
|
|
300
|
-
and
|
|
305
|
+
and pair[0].lower() in SECURITY_HEADERS
|
|
301
306
|
and isinstance(pair[1], str) and len(pair[1]) <= 4000 and _HEADER_VALUE.fullmatch(pair[1]) is not None)
|
|
302
307
|
|
|
303
308
|
class Core:
|
|
@@ -442,6 +447,23 @@ def header(headers, name):
|
|
|
442
447
|
_PATH_SAFE = "/:@!$&'()*+,;=-._~"
|
|
443
448
|
|
|
444
449
|
|
|
450
|
+
_ABSOLUTE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.-]*://[^/?#]*")
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def origin_form(target):
|
|
454
|
+
"""The request target as the app routes it: an absolute-form one ("GET
|
|
455
|
+
http://host/admin", sent as if to a proxy) is routed by its path, so it is
|
|
456
|
+
cut to "/admin"; matched as written it began "http:" and missed every rule
|
|
457
|
+
on "/admin". "*" and a target already "/..." are kept as they are."""
|
|
458
|
+
text = str(target or "")
|
|
459
|
+
if not text or text.startswith("/") or text == "*":
|
|
460
|
+
return text
|
|
461
|
+
rest, count = _ABSOLUTE.subn("", text, count=1)
|
|
462
|
+
if not count:
|
|
463
|
+
return text
|
|
464
|
+
return rest if rest.startswith("/") else "/" + rest
|
|
465
|
+
|
|
466
|
+
|
|
445
467
|
def request_target(path, query="", raw=None):
|
|
446
468
|
"""The request target as the visitor sent it, query string and all, which
|
|
447
469
|
is what the request rules match (sdk/protocols/firewall.md): the Node and
|
|
@@ -19,7 +19,7 @@ import time
|
|
|
19
19
|
from datetime import datetime, timezone
|
|
20
20
|
|
|
21
21
|
from . import challenge as browser_check
|
|
22
|
-
from .core import Periodic, RETRY, header
|
|
22
|
+
from .core import Periodic, RETRY, header, origin_form
|
|
23
23
|
from .request_rules import MAX_TEXT, _decode_once, compile_limits, compile_rules, match_limits, match_request, normalize_target, prepare
|
|
24
24
|
|
|
25
25
|
PROTOCOL = 1
|
|
@@ -572,6 +572,7 @@ class Firewall:
|
|
|
572
572
|
request's headers) are for the browser check: rule "challenge",
|
|
573
573
|
status 503, answered with challenge_page()."""
|
|
574
574
|
allow = {"action": "allow", "rule": "", "status": 200}
|
|
575
|
+
path = origin_form(path)
|
|
575
576
|
rules = self.rules
|
|
576
577
|
if not rules or not rules["enabled"]:
|
|
577
578
|
return allow
|
|
@@ -694,6 +695,7 @@ class Firewall:
|
|
|
694
695
|
beacons are never rate-limited, checked or matched against request
|
|
695
696
|
rules. -> the same shape as check()."""
|
|
696
697
|
allow = {"action": "allow", "rule": "", "status": 200}
|
|
698
|
+
path = origin_form(path)
|
|
697
699
|
rules = self.rules
|
|
698
700
|
if not rules or not rules["enabled"]:
|
|
699
701
|
return allow
|
|
@@ -49,6 +49,21 @@ PASS_HEADERS = ("content-type", "user-agent", "accept-language", "referer", "ori
|
|
|
49
49
|
"sec-fetch-site", "sec-ch-ua", "sec-ch-ua-mobile", "sec-ch-ua-platform")
|
|
50
50
|
SAFE_PATH = re.compile(r"[A-Za-z0-9/_.-]{1,120}\Z")
|
|
51
51
|
FILE_PATH = re.compile(r"v(\d+(?:\.\d+\.\d+)?)/([a-z]+\.js)\Z")
|
|
52
|
+
# What a collector answer may be served as from the site's own domain. Its
|
|
53
|
+
# type comes from the collector, so anything but these three becomes plain
|
|
54
|
+
# text, and nosniff stops a browser guessing: a server that is no longer ours
|
|
55
|
+
# still can't answer <prefix>/api/... with a page that runs as the customer's
|
|
56
|
+
# own site. Shared cases: sdk/protocols/proxy-answer-cases.json.
|
|
57
|
+
ANSWER_TYPES = frozenset(("application/json", "text/plain", "image/webp"))
|
|
58
|
+
_VISIBLE = re.compile(r"[\x20-\x7e]*")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def answer_type(value):
|
|
62
|
+
value = value if isinstance(value, str) else ""
|
|
63
|
+
kind = value.split(";", 1)[0].strip().lower()
|
|
64
|
+
return value if kind in ANSWER_TYPES and _VISIBLE.fullmatch(value) else "text/plain; charset=utf-8"
|
|
65
|
+
|
|
66
|
+
|
|
52
67
|
NOT_FOUND = (404, [("Content-Type", "text/plain; charset=utf-8")], b"Not found")
|
|
53
68
|
|
|
54
69
|
|
|
@@ -236,8 +251,8 @@ class Proxy:
|
|
|
236
251
|
|
|
237
252
|
@staticmethod
|
|
238
253
|
def _answer_headers(headers):
|
|
239
|
-
out = []
|
|
240
|
-
for name in ("
|
|
254
|
+
out = [("Content-Type", answer_type(headers.get("Content-Type"))), ("X-Content-Type-Options", "nosniff")]
|
|
255
|
+
for name in ("Cache-Control", "Retry-After"):
|
|
241
256
|
value = headers.get(name)
|
|
242
257
|
if value:
|
|
243
258
|
out.append((name, value))
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: analytics-platform
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.10
|
|
4
4
|
Summary: Firewall, error monitoring and server analytics for your Python server, run from your analytics dashboard.
|
|
5
5
|
Author: steviec87
|
|
6
6
|
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://sitegoose.com
|
|
8
|
+
Project-URL: Source, https://github.com/StevieC87/Sitegoose-Packages/tree/main/platform-python
|
|
9
|
+
Project-URL: Issues, https://github.com/StevieC87/Sitegoose-Packages/issues
|
|
10
|
+
Project-URL: Security, https://sitegoose.com/security
|
|
7
11
|
Keywords: firewall,waf,rate-limit,error-monitoring,analytics,server-monitoring,django,flask,fastapi,celery,rq,dramatiq,wsgi,asgi
|
|
8
12
|
Classifier: Development Status :: 4 - Beta
|
|
9
13
|
Classifier: Intended Audience :: Developers
|
{analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform.egg-info/SOURCES.txt
RENAMED
|
@@ -39,9 +39,11 @@ tests/test_crawlers.py
|
|
|
39
39
|
tests/test_errors.py
|
|
40
40
|
tests/test_firewall.py
|
|
41
41
|
tests/test_hardening.py
|
|
42
|
+
tests/test_headers.py
|
|
42
43
|
tests/test_limits.py
|
|
43
44
|
tests/test_page_limits.py
|
|
44
45
|
tests/test_proxy.py
|
|
46
|
+
tests/test_proxy_answer.py
|
|
45
47
|
tests/test_queries.py
|
|
46
48
|
tests/test_redis.py
|
|
47
49
|
tests/test_request_rules.py
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
requires = ["setuptools>=77"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
|
+
[project.urls]
|
|
6
|
+
Homepage = "https://sitegoose.com"
|
|
7
|
+
Source = "https://github.com/StevieC87/Sitegoose-Packages/tree/main/platform-python"
|
|
8
|
+
Issues = "https://github.com/StevieC87/Sitegoose-Packages/issues"
|
|
9
|
+
Security = "https://sitegoose.com/security"
|
|
10
|
+
|
|
5
11
|
[project]
|
|
6
12
|
name = "analytics-platform"
|
|
7
|
-
version = "0.3.
|
|
13
|
+
version = "0.3.10"
|
|
8
14
|
description = "Firewall, error monitoring and server analytics for your Python server, run from your analytics dashboard."
|
|
9
15
|
readme = "README.md"
|
|
10
16
|
requires-python = ">=3.8"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""The security headers taken from the platform config, against the cases every
|
|
2
|
+
client shares: sdk/protocols/header-cases.json. Only the nine the dashboard can
|
|
3
|
+
produce get through, so all the packages add the same ones."""
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
from analytics_platform.core import safe_header
|
|
9
|
+
|
|
10
|
+
CASES = os.path.join(os.path.dirname(__file__), "..", "..", "protocols", "header-cases.json")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HeaderCases(unittest.TestCase):
|
|
14
|
+
def test_shared_cases(self):
|
|
15
|
+
with open(CASES, encoding="utf-8") as f:
|
|
16
|
+
cases = json.load(f)["cases"]
|
|
17
|
+
for case in cases:
|
|
18
|
+
value = case["value"] * case.get("repeat", 1)
|
|
19
|
+
with self.subTest(name=case["name"], value=case["value"][:40], repeat=case.get("repeat")):
|
|
20
|
+
self.assertEqual(safe_header([case["name"], value]), case["ok"])
|
|
21
|
+
|
|
22
|
+
def test_only_pairs_of_strings(self):
|
|
23
|
+
for pair in (None, "X-Frame-Options", ["X-Frame-Options"], ["X-Frame-Options", 1], ["X-Frame-Options", "DENY", "x"]):
|
|
24
|
+
self.assertFalse(safe_header(pair))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
unittest.main()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""The type a collector answer is served with from the site's own domain,
|
|
2
|
+
against the cases every client shares: sdk/protocols/proxy-answer-cases.json."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from analytics_platform.proxy import Proxy, answer_type
|
|
8
|
+
|
|
9
|
+
CASES = os.path.join(os.path.dirname(__file__), "..", "..", "protocols", "proxy-answer-cases.json")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ProxyAnswerCases(unittest.TestCase):
|
|
13
|
+
def test_shared_cases(self):
|
|
14
|
+
with open(CASES, encoding="utf-8") as f:
|
|
15
|
+
cases = json.load(f)["cases"]
|
|
16
|
+
for case in cases:
|
|
17
|
+
with self.subTest(type=case["type"]):
|
|
18
|
+
self.assertEqual(answer_type(case["type"]), case["served"])
|
|
19
|
+
|
|
20
|
+
def test_nosniff_and_only_cache_and_retry(self):
|
|
21
|
+
upstream = {"Content-Type": "text/html", "Cache-Control": "no-store", "Set-Cookie": "a=b", "Refresh": "0;url=x"}
|
|
22
|
+
self.assertEqual(Proxy._answer_headers(upstream), [
|
|
23
|
+
("Content-Type", "text/plain; charset=utf-8"), ("X-Content-Type-Options", "nosniff"), ("Cache-Control", "no-store")])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{analytics_platform-0.3.8 → analytics_platform-0.3.10}/analytics_platform.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|