deploy-checkup 0.1.0__tar.gz → 0.1.1__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.
- {deploy_checkup-0.1.0/src/deploy_checkup.egg-info → deploy_checkup-0.1.1}/PKG-INFO +1 -1
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/pyproject.toml +1 -1
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup/checks/cors.py +3 -4
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup/checks/exposed_files.py +3 -4
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup/checks/headers.py +3 -4
- deploy_checkup-0.1.1/src/deploy_checkup/models.py +31 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1/src/deploy_checkup.egg-info}/PKG-INFO +1 -1
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/tests/test_cors.py +16 -1
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/tests/test_exposed_files.py +13 -1
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/tests/test_headers.py +4 -2
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/tests/test_models.py +8 -1
- deploy_checkup-0.1.0/src/deploy_checkup/models.py +0 -19
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/LICENSE +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/README.md +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/setup.cfg +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup/__init__.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup/__main__.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup/checks/__init__.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup/checks/cold_start.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup/checks/health.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup/checks/https.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup/checks/reachable.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup/cli.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup/hosts.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup/report.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup/runner.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup.egg-info/SOURCES.txt +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup.egg-info/dependency_links.txt +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup.egg-info/entry_points.txt +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup.egg-info/requires.txt +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup.egg-info/top_level.txt +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/tests/test_cli.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/tests/test_cold_start.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/tests/test_health.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/tests/test_hosts.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/tests/test_https.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/tests/test_reachable.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/tests/test_report.py +0 -0
- {deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/tests/test_runner.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "deploy-checkup"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.1"
|
|
8
8
|
description = "Check whether your deployed app is set up safely, in plain language, with fixes for your host."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import httpx
|
|
2
2
|
|
|
3
|
-
from deploy_checkup.models import Finding, Level
|
|
3
|
+
from deploy_checkup.models import Finding, Level, could_not_finish
|
|
4
4
|
|
|
5
5
|
CHECK = "cors"
|
|
6
6
|
MADE_UP_SITE = "https://deploy-checkup-test.invalid" # .invalid can never be a real website
|
|
@@ -12,9 +12,8 @@ def check_cors(client: httpx.Client, api_url: str, frontend_url: str) -> list[Fi
|
|
|
12
12
|
try:
|
|
13
13
|
as_frontend = client.get(api_url, headers={"Origin": frontend})
|
|
14
14
|
as_stranger = client.get(api_url, headers={"Origin": MADE_UP_SITE})
|
|
15
|
-
except httpx.RequestError:
|
|
16
|
-
return []
|
|
17
|
-
|
|
15
|
+
except httpx.RequestError as error:
|
|
16
|
+
return [could_not_finish(CHECK, error)]
|
|
18
17
|
return [
|
|
19
18
|
_check_own_frontend(as_frontend, frontend),
|
|
20
19
|
_check_other_sites(as_stranger),
|
|
@@ -2,8 +2,7 @@ import re
|
|
|
2
2
|
|
|
3
3
|
import httpx
|
|
4
4
|
|
|
5
|
-
from deploy_checkup.models import Finding, Level
|
|
6
|
-
|
|
5
|
+
from deploy_checkup.models import Finding, Level, could_not_finish
|
|
7
6
|
CHECK = "exposed_files"
|
|
8
7
|
|
|
9
8
|
|
|
@@ -28,8 +27,8 @@ def check_exposed_files(client: httpx.Client, url: str) -> list[Finding]:
|
|
|
28
27
|
for path, looks_real in PATHS.items():
|
|
29
28
|
try:
|
|
30
29
|
response = client.get(httpx.URL(url).join(path))
|
|
31
|
-
except httpx.RequestError:
|
|
32
|
-
return
|
|
30
|
+
except httpx.RequestError as error:
|
|
31
|
+
return findings + [could_not_finish(CHECK, error)]
|
|
33
32
|
|
|
34
33
|
is_html = "text/html" in response.headers.get("content-type", "")
|
|
35
34
|
if response.status_code == 200 and not is_html and looks_real(response.text):
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import httpx
|
|
2
|
-
from deploy_checkup.models import Finding, Level
|
|
3
|
-
|
|
2
|
+
from deploy_checkup.models import Finding, Level, could_not_finish
|
|
4
3
|
|
|
5
4
|
CHECK = "headers"
|
|
6
5
|
SIX_MONTHS= 15_552_000 #seconds
|
|
@@ -10,8 +9,8 @@ def check_headers(client: httpx.Client , url:str)->list[Finding]:
|
|
|
10
9
|
try:
|
|
11
10
|
response = client.get(url, follow_redirects=True)
|
|
12
11
|
|
|
13
|
-
except httpx.RequestError:
|
|
14
|
-
return []
|
|
12
|
+
except httpx.RequestError as error:
|
|
13
|
+
return [could_not_finish(CHECK, error)]
|
|
15
14
|
|
|
16
15
|
headers = response.headers
|
|
17
16
|
return [
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Level(str, Enum):
|
|
6
|
+
PASS = "pass"
|
|
7
|
+
WARNING = "warning"
|
|
8
|
+
FAIL = "fail"
|
|
9
|
+
INFO = "info"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class Finding:
|
|
14
|
+
check: str
|
|
15
|
+
level: Level
|
|
16
|
+
title: str
|
|
17
|
+
why: str = ""
|
|
18
|
+
fix: str = "" # works on any host
|
|
19
|
+
render_fix: str = "" # replaces fix when the site is on Render
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def could_not_finish(check: str, error: Exception) -> Finding:
|
|
24
|
+
"""A check that hit a network error says so, instead of quietly disappearing."""
|
|
25
|
+
return Finding(
|
|
26
|
+
check=check,
|
|
27
|
+
level=Level.INFO,
|
|
28
|
+
title=f"Couldn't finish the {check} check ({type(error).__name__})",
|
|
29
|
+
why="The site stopped answering partway through, so this part wasn't checked.",
|
|
30
|
+
fix="Run deploy-checkup again. If it keeps happening, check your site's logs.",
|
|
31
|
+
)
|
|
@@ -77,4 +77,19 @@ def test_sends_frontend_origin_then_made_up_origin():
|
|
|
77
77
|
|
|
78
78
|
def test_origin_drops_path_and_keeps_unusual_port():
|
|
79
79
|
assert _origin("https://app.example.com/dashboard?x=1") == "https://app.example.com"
|
|
80
|
-
assert _origin("http://127.0.0.1:8000/") == "http://127.0.0.1:8000"
|
|
80
|
+
assert _origin("http://127.0.0.1:8000/") == "http://127.0.0.1:8000"
|
|
81
|
+
|
|
82
|
+
def test_network_error_partway_is_reported_not_hidden():
|
|
83
|
+
calls = []
|
|
84
|
+
|
|
85
|
+
def handler(request):
|
|
86
|
+
calls.append(request)
|
|
87
|
+
if len(calls) == 2:
|
|
88
|
+
raise httpx.ReadTimeout("timed out", request=request)
|
|
89
|
+
return httpx.Response(200)
|
|
90
|
+
|
|
91
|
+
findings = check(handler)
|
|
92
|
+
|
|
93
|
+
assert len(findings) == 1
|
|
94
|
+
assert findings[0].level == Level.INFO
|
|
95
|
+
assert "ReadTimeout" in findings[0].title
|
|
@@ -57,4 +57,16 @@ def test_secret_values_are_never_shown():
|
|
|
57
57
|
findings = check(site({"/.env": "SECRET_KEY=abc123\n"}))
|
|
58
58
|
|
|
59
59
|
for finding in findings:
|
|
60
|
-
assert "abc123" not in finding.title + finding.why + finding.fix
|
|
60
|
+
assert "abc123" not in finding.title + finding.why + finding.fix
|
|
61
|
+
|
|
62
|
+
def test_network_error_keeps_earlier_results_and_says_so():
|
|
63
|
+
def handler(request):
|
|
64
|
+
if request.url.path == "/.git/config":
|
|
65
|
+
raise httpx.ReadTimeout("timed out", request=request)
|
|
66
|
+
return httpx.Response(404)
|
|
67
|
+
|
|
68
|
+
findings = check(handler)
|
|
69
|
+
|
|
70
|
+
assert find(findings, "/.env").level == Level.PASS
|
|
71
|
+
assert findings[-1].level == Level.INFO
|
|
72
|
+
assert "Couldn't finish" in findings[-1].title
|
|
@@ -52,13 +52,15 @@ def test_nosniff_missing_warns():
|
|
|
52
52
|
assert find(findings, "X-Content-Type-Options").level == Level.WARNING
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
def
|
|
55
|
+
def test_connection_error_is_reported_not_hidden():
|
|
56
56
|
def handler(request):
|
|
57
57
|
raise httpx.ConnectError("connection refused", request=request)
|
|
58
58
|
|
|
59
59
|
findings = check_headers(httpx.Client(transport=httpx.MockTransport(handler)), URL)
|
|
60
60
|
|
|
61
|
-
assert findings ==
|
|
61
|
+
assert len(findings) == 1
|
|
62
|
+
assert findings[0].level == Level.INFO
|
|
63
|
+
assert "Couldn't finish the headers check" in findings[0].title
|
|
62
64
|
|
|
63
65
|
def test_csp_present_passes():
|
|
64
66
|
findings = run_with_headers({"Content-Security-Policy": "default-src 'self'"})
|
|
@@ -36,4 +36,11 @@ def test_level_from_string():
|
|
|
36
36
|
assert Level("warning") is Level.WARNING
|
|
37
37
|
|
|
38
38
|
with pytest.raises(ValueError):
|
|
39
|
-
Level("warn")
|
|
39
|
+
Level("warn")
|
|
40
|
+
def test_could_not_finish_names_the_check_and_error():
|
|
41
|
+
from deploy_checkup.models import could_not_finish
|
|
42
|
+
|
|
43
|
+
finding = could_not_finish("cors", TimeoutError("slow"))
|
|
44
|
+
|
|
45
|
+
assert finding.level == Level.INFO
|
|
46
|
+
assert finding.title == "Couldn't finish the cors check (TimeoutError)"
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from enum import Enum
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class Level(str, Enum):
|
|
6
|
-
PASS = "pass"
|
|
7
|
-
WARNING = "warning"
|
|
8
|
-
FAIL = "fail"
|
|
9
|
-
INFO = "info"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@dataclass(frozen=True)
|
|
13
|
-
class Finding:
|
|
14
|
-
check: str
|
|
15
|
-
level: Level
|
|
16
|
-
title: str
|
|
17
|
-
why: str = ""
|
|
18
|
-
fix: str = "" # works on any host
|
|
19
|
-
render_fix: str = "" # replaces fix when the site is on Render
|
|
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
|
{deploy_checkup-0.1.0 → deploy_checkup-0.1.1}/src/deploy_checkup.egg-info/dependency_links.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
|