analytics-platform 0.3.8__tar.gz → 0.3.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/PKG-INFO +5 -1
  2. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/core.py +14 -9
  3. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/proxy.py +17 -2
  4. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform.egg-info/PKG-INFO +5 -1
  5. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform.egg-info/SOURCES.txt +2 -0
  6. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/pyproject.toml +7 -1
  7. analytics_platform-0.3.9/tests/test_headers.py +28 -0
  8. analytics_platform-0.3.9/tests/test_proxy_answer.py +27 -0
  9. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/LICENSE +0 -0
  10. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/README.md +0 -0
  11. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/__init__.py +0 -0
  12. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/_env.py +0 -0
  13. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/analytics.py +0 -0
  14. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/audit.py +0 -0
  15. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/calls.py +0 -0
  16. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/celery.py +0 -0
  17. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/challenge.py +0 -0
  18. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/crawlers.py +0 -0
  19. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/django.py +0 -0
  20. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/dramatiq.py +0 -0
  21. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/errors.py +0 -0
  22. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/firewall.py +0 -0
  23. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/flags.py +0 -0
  24. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/forms.py +0 -0
  25. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/inventory.py +0 -0
  26. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/maintenance.py +0 -0
  27. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/performance.py +0 -0
  28. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/posture.py +0 -0
  29. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/queries.py +0 -0
  30. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/queues.py +0 -0
  31. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/request_rules.py +0 -0
  32. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/resources.py +0 -0
  33. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/rq.py +0 -0
  34. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform/start.py +0 -0
  35. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform.egg-info/dependency_links.txt +0 -0
  36. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/analytics_platform.egg-info/top_level.txt +0 -0
  37. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/setup.cfg +0 -0
  38. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/tests/test_calls_queues.py +0 -0
  39. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/tests/test_celery.py +0 -0
  40. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/tests/test_challenge.py +0 -0
  41. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/tests/test_checkin.py +0 -0
  42. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/tests/test_crawlers.py +0 -0
  43. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/tests/test_errors.py +0 -0
  44. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/tests/test_firewall.py +0 -0
  45. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/tests/test_hardening.py +0 -0
  46. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/tests/test_limits.py +0 -0
  47. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/tests/test_page_limits.py +0 -0
  48. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/tests/test_proxy.py +0 -0
  49. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/tests/test_queries.py +0 -0
  50. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/tests/test_redis.py +0 -0
  51. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/tests/test_request_rules.py +0 -0
  52. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/tests/test_resources.py +0 -0
  53. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/tests/test_rq_dramatiq.py +0 -0
  54. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/tests/test_start.py +0 -0
  55. {analytics_platform-0.3.8 → analytics_platform-0.3.9}/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.8
3
+ Version: 0.3.9
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.8"
26
+ VERSION = "0.3.9"
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 well formed: a token name that
289
- # isn't one of the headers that frame the response (a Content-Length or
290
- # Transfer-Encoding of ours would desync it, a Set-Cookie or Content-Type
291
- # would change what it is), and a value of visible ASCII, spaces and tabs,
292
- # so no line break, control character or byte a server rejects gets through.
293
- FRAMING = frozenset(("content-length", "transfer-encoding", "connection", "set-cookie", "content-type"))
294
- _HEADER_NAME = re.compile(r"[A-Za-z][A-Za-z0-9-]{0,63}")
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 _HEADER_NAME.fullmatch(pair[0]) is not None and pair[0].lower() not in FRAMING
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:
@@ -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 ("Content-Type", "Cache-Control", "Retry-After"):
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.8
3
+ Version: 0.3.9
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
@@ -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.8"
13
+ version = "0.3.9"
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()