deploy-checkup 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.
Files changed (38) hide show
  1. deploy_checkup-0.1.0/LICENSE +21 -0
  2. deploy_checkup-0.1.0/PKG-INFO +178 -0
  3. deploy_checkup-0.1.0/README.md +148 -0
  4. deploy_checkup-0.1.0/pyproject.toml +44 -0
  5. deploy_checkup-0.1.0/setup.cfg +4 -0
  6. deploy_checkup-0.1.0/src/deploy_checkup/__init__.py +0 -0
  7. deploy_checkup-0.1.0/src/deploy_checkup/__main__.py +3 -0
  8. deploy_checkup-0.1.0/src/deploy_checkup/checks/__init__.py +0 -0
  9. deploy_checkup-0.1.0/src/deploy_checkup/checks/cold_start.py +37 -0
  10. deploy_checkup-0.1.0/src/deploy_checkup/checks/cors.py +85 -0
  11. deploy_checkup-0.1.0/src/deploy_checkup/checks/exposed_files.py +65 -0
  12. deploy_checkup-0.1.0/src/deploy_checkup/checks/headers.py +140 -0
  13. deploy_checkup-0.1.0/src/deploy_checkup/checks/health.py +106 -0
  14. deploy_checkup-0.1.0/src/deploy_checkup/checks/https.py +79 -0
  15. deploy_checkup-0.1.0/src/deploy_checkup/checks/reachable.py +59 -0
  16. deploy_checkup-0.1.0/src/deploy_checkup/cli.py +83 -0
  17. deploy_checkup-0.1.0/src/deploy_checkup/hosts.py +27 -0
  18. deploy_checkup-0.1.0/src/deploy_checkup/models.py +19 -0
  19. deploy_checkup-0.1.0/src/deploy_checkup/report.py +72 -0
  20. deploy_checkup-0.1.0/src/deploy_checkup/runner.py +60 -0
  21. deploy_checkup-0.1.0/src/deploy_checkup.egg-info/PKG-INFO +178 -0
  22. deploy_checkup-0.1.0/src/deploy_checkup.egg-info/SOURCES.txt +36 -0
  23. deploy_checkup-0.1.0/src/deploy_checkup.egg-info/dependency_links.txt +1 -0
  24. deploy_checkup-0.1.0/src/deploy_checkup.egg-info/entry_points.txt +2 -0
  25. deploy_checkup-0.1.0/src/deploy_checkup.egg-info/requires.txt +7 -0
  26. deploy_checkup-0.1.0/src/deploy_checkup.egg-info/top_level.txt +1 -0
  27. deploy_checkup-0.1.0/tests/test_cli.py +108 -0
  28. deploy_checkup-0.1.0/tests/test_cold_start.py +28 -0
  29. deploy_checkup-0.1.0/tests/test_cors.py +80 -0
  30. deploy_checkup-0.1.0/tests/test_exposed_files.py +60 -0
  31. deploy_checkup-0.1.0/tests/test_headers.py +102 -0
  32. deploy_checkup-0.1.0/tests/test_health.py +93 -0
  33. deploy_checkup-0.1.0/tests/test_hosts.py +61 -0
  34. deploy_checkup-0.1.0/tests/test_https.py +80 -0
  35. deploy_checkup-0.1.0/tests/test_models.py +39 -0
  36. deploy_checkup-0.1.0/tests/test_reachable.py +49 -0
  37. deploy_checkup-0.1.0/tests/test_report.py +46 -0
  38. deploy_checkup-0.1.0/tests/test_runner.py +50 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nandha Gopal
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,178 @@
1
+ Metadata-Version: 2.4
2
+ Name: deploy-checkup
3
+ Version: 0.1.0
4
+ Summary: Check whether your deployed app is set up safely, in plain language, with fixes for your host.
5
+ Author: Nandhagopal
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Nandhagopal2912/deploy-checkup
8
+ Project-URL: Issues, https://github.com/Nandhagopal2912/deploy-checkup/issues
9
+ Keywords: deploy,security,cors,headers,https,render,cli
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Security
19
+ Classifier: Topic :: Internet :: WWW/HTTP
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: httpx<1,>=0.28
24
+ Requires-Dist: rich<16,>=14
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8; extra == "dev"
27
+ Requires-Dist: build; extra == "dev"
28
+ Requires-Dist: twine; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # deploy-checkup
32
+
33
+ [![CI](https://github.com/Nandhagopal2912/deploy-checkup/actions/workflows/ci.yml/badge.svg)](https://github.com/Nandhagopal2912/deploy-checkup/actions/workflows/ci.yml)
34
+
35
+ Tells you, in plain language, whether your deployed app is set up safely, and exactly how to fix what isn't on your host.
36
+
37
+ > **Only test apps you own or have permission to test.** Against someone else's site, unusual requests can look like an attack and may be illegal.
38
+
39
+ ## Install
40
+
41
+ Requires Python 3.10 or newer.
42
+
43
+ ```bash
44
+ pipx install deploy-checkup
45
+ ```
46
+
47
+ `pipx` installs command-line tools in their own environment ([how to install pipx](https://pipx.pypa.io/stable/installation/)). `pip install deploy-checkup` works too.
48
+
49
+ ## Usage
50
+
51
+ ```bash
52
+ deploy-checkup https://your-frontend.onrender.com --api https://your-api.onrender.com
53
+ ```
54
+
55
+ | Option | Meaning |
56
+ |---|---|
57
+ | `url` | Your frontend's address (required) |
58
+ | `--api` | Your API's address, if it is separate |
59
+ | `--health-path` | The API's health check path (default `/health`) |
60
+ | `--yes-i-own-it` | Run the probing checks without asking (for CI) |
61
+ | `--json` | Print the findings as JSON instead of a coloured report |
62
+ | `-V`, `--version` | Show the version |
63
+ | `-h`, `--help` | Show help |
64
+
65
+ Example output:
66
+
67
+ ```
68
+ ──────────── https://your-frontend.onrender.com ────────────
69
+ PASS Site answered with status 200
70
+ PASS Plain HTTP redirects to HTTPS (301)
71
+ WARNING Content-Security-Policy is missing
72
+ Why: If an attacker manages to inject a script into your page, the browser will run it. ...
73
+ Fix: Start with Content-Security-Policy: default-src 'self' and loosen it ...
74
+
75
+ Summary: 6 pass, 1 warning, 0 fail, 0 info
76
+ ```
77
+
78
+ Each finding says what was found, why it matters, and how to fix it.
79
+
80
+ | Level | Meaning |
81
+ |---|---|
82
+ | **PASS** | Checked, and it's set up well. |
83
+ | **WARNING** | Probably worth fixing, but your app may be fine without it. Used whenever deploy-checkup can't be sure. |
84
+ | **FAIL** | Something is clearly broken or unsafe. Fix this first. |
85
+ | **INFO** | Good to know, such as a check that was skipped or a hint that may not apply. |
86
+
87
+ The command exits with code `1` if anything fails and `0` otherwise.
88
+
89
+ ### Use it in CI
90
+
91
+ Run it after each deploy, so a broken setup fails the pipeline. For example, in GitHub Actions:
92
+
93
+ ```yaml
94
+ - run: pip install deploy-checkup
95
+ - run: deploy-checkup https://your-frontend.onrender.com --api https://your-api.onrender.com --yes-i-own-it
96
+ ```
97
+
98
+ `--yes-i-own-it` runs the probing checks without the question nobody can answer in CI. Leave it out to run only the gentle checks.
99
+
100
+ ## What it checks
101
+
102
+ | Check | Runs on | What it asks |
103
+ |---|---|---|
104
+ | **reachable** | frontend | Does the address answer, and with what status? |
105
+ | **https** | frontend and API | Does plain `http://` redirect to `https://`, and permanently (301/308)? |
106
+ | **headers** | frontend | Are the browser security headers set? Strict-Transport-Security (at least 6 months), X-Content-Type-Options (`nosniff`), Content-Security-Policy, X-Frame-Options (or CSP `frame-ancestors`), Referrer-Policy |
107
+ | **cors** | API | Asks the API as your frontend, then as a made-up site (`deploy-checkup-test.invalid`). Is your frontend allowed and everyone else blocked? |
108
+ | **health** | API | Calls the health path 3 times. Does it exist, and what's the median response time? |
109
+ | **cold_start** | API | Was the first of those requests much slower than the rest (a free tier waking up)? Reported as a hint, not a verdict. |
110
+ | **exposed_files** | frontend and API | Can anyone download `/.env` or `/.git/config`? **Probing:** runs only after you confirm you own the site. |
111
+
112
+ The headers check skips the API on purpose: a JSON API doesn't need CSP or X-Frame-Options, and flagging them would be a false alarm.
113
+
114
+ **Fix hints match your host.** When a response carries Render's `x-render-origin-server` header, fixes point to the exact place in the Render dashboard. On any other host you get general advice.
115
+
116
+ ### Probing checks ask first
117
+
118
+ Most checks send ordinary requests any browser could send. The exposed files check asks for paths like `/.env`, which on someone else's site can look like an attack. So deploy-checkup asks *"Do you own this site?"* before running it. If nobody can answer (in CI, for example), it is skipped and the report says so, unless you pass `--yes-i-own-it`. A leaked file's contents are never shown in the report.
119
+
120
+ ### Polite by design
121
+
122
+ Every request uses a 60-second timeout (Render's free tier can take about 50 seconds to wake up) and identifies itself as `User-Agent: DeployCheckup/<version>`, so site owners can see what called them. A full run sends a few dozen ordinary requests.
123
+
124
+ ### What it doesn't do
125
+
126
+ deploy-checkup is a quick first look, not a full security audit:
127
+
128
+ - It only sends ordinary requests. It doesn't try to break in, and it isn't a replacement for tools like OWASP ZAP.
129
+ - CORS is tested on the one API address you give it, and health on one path.
130
+ - Cold start detection is a guess from a few timings.
131
+ - Fix hints are written for Render first; other hosts get general advice.
132
+
133
+ ## JSON output
134
+
135
+ `--json` prints the same findings for scripts and CI. Only the JSON goes to standard output; the permission question goes to standard error, so you can save the report safely:
136
+
137
+ ```bash
138
+ deploy-checkup https://your-frontend.onrender.com --api https://your-api.onrender.com --json > report.json
139
+ ```
140
+
141
+ ```json
142
+ {
143
+ "tool": "deploy-checkup",
144
+ "version": "0.1.0",
145
+ "targets": [
146
+ {
147
+ "url": "https://your-api.onrender.com",
148
+ "findings": [
149
+ {"check": "health", "level": "pass", "title": "API answered in 108 ms (median of 2)", "why": "", "fix": ""}
150
+ ]
151
+ }
152
+ ],
153
+ "summary": {"pass": 16, "warning": 0, "fail": 0, "info": 0}
154
+ }
155
+ ```
156
+
157
+ The exit code is the same as for the coloured report.
158
+
159
+ ## Development
160
+
161
+ ```bash
162
+ git clone https://github.com/Nandhagopal2912/deploy-checkup.git
163
+ cd deploy-checkup
164
+ python -m venv .venv
165
+ .venv\Scripts\activate # Windows; on macOS/Linux: source .venv/bin/activate
166
+ pip install -e ".[dev]"
167
+ pytest
168
+ ```
169
+
170
+ `-e` installs in editable mode, so code changes take effect without reinstalling. Re-run the install after changing `dependencies` or `[project.scripts]` in `pyproject.toml`.
171
+
172
+ Tests never touch the network. Each check receives an `httpx.Client`, and tests pass one built on `httpx.MockTransport`, which answers requests with fake responses. CI runs them on Python 3.10 and 3.13.
173
+
174
+ [`demo/`](https://github.com/Nandhagopal2912/deploy-checkup/tree/main/demo) holds a deliberately broken app with known flaws, used to prove each check catches what it should. The project's plan is in the [roadmap](https://github.com/Nandhagopal2912/deploy-checkup/blob/main/docs/Deploy%20Doctor%20Roadmap.pdf).
175
+
176
+ ## License
177
+
178
+ [MIT](https://github.com/Nandhagopal2912/deploy-checkup/blob/main/LICENSE)
@@ -0,0 +1,148 @@
1
+ # deploy-checkup
2
+
3
+ [![CI](https://github.com/Nandhagopal2912/deploy-checkup/actions/workflows/ci.yml/badge.svg)](https://github.com/Nandhagopal2912/deploy-checkup/actions/workflows/ci.yml)
4
+
5
+ Tells you, in plain language, whether your deployed app is set up safely, and exactly how to fix what isn't on your host.
6
+
7
+ > **Only test apps you own or have permission to test.** Against someone else's site, unusual requests can look like an attack and may be illegal.
8
+
9
+ ## Install
10
+
11
+ Requires Python 3.10 or newer.
12
+
13
+ ```bash
14
+ pipx install deploy-checkup
15
+ ```
16
+
17
+ `pipx` installs command-line tools in their own environment ([how to install pipx](https://pipx.pypa.io/stable/installation/)). `pip install deploy-checkup` works too.
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ deploy-checkup https://your-frontend.onrender.com --api https://your-api.onrender.com
23
+ ```
24
+
25
+ | Option | Meaning |
26
+ |---|---|
27
+ | `url` | Your frontend's address (required) |
28
+ | `--api` | Your API's address, if it is separate |
29
+ | `--health-path` | The API's health check path (default `/health`) |
30
+ | `--yes-i-own-it` | Run the probing checks without asking (for CI) |
31
+ | `--json` | Print the findings as JSON instead of a coloured report |
32
+ | `-V`, `--version` | Show the version |
33
+ | `-h`, `--help` | Show help |
34
+
35
+ Example output:
36
+
37
+ ```
38
+ ──────────── https://your-frontend.onrender.com ────────────
39
+ PASS Site answered with status 200
40
+ PASS Plain HTTP redirects to HTTPS (301)
41
+ WARNING Content-Security-Policy is missing
42
+ Why: If an attacker manages to inject a script into your page, the browser will run it. ...
43
+ Fix: Start with Content-Security-Policy: default-src 'self' and loosen it ...
44
+
45
+ Summary: 6 pass, 1 warning, 0 fail, 0 info
46
+ ```
47
+
48
+ Each finding says what was found, why it matters, and how to fix it.
49
+
50
+ | Level | Meaning |
51
+ |---|---|
52
+ | **PASS** | Checked, and it's set up well. |
53
+ | **WARNING** | Probably worth fixing, but your app may be fine without it. Used whenever deploy-checkup can't be sure. |
54
+ | **FAIL** | Something is clearly broken or unsafe. Fix this first. |
55
+ | **INFO** | Good to know, such as a check that was skipped or a hint that may not apply. |
56
+
57
+ The command exits with code `1` if anything fails and `0` otherwise.
58
+
59
+ ### Use it in CI
60
+
61
+ Run it after each deploy, so a broken setup fails the pipeline. For example, in GitHub Actions:
62
+
63
+ ```yaml
64
+ - run: pip install deploy-checkup
65
+ - run: deploy-checkup https://your-frontend.onrender.com --api https://your-api.onrender.com --yes-i-own-it
66
+ ```
67
+
68
+ `--yes-i-own-it` runs the probing checks without the question nobody can answer in CI. Leave it out to run only the gentle checks.
69
+
70
+ ## What it checks
71
+
72
+ | Check | Runs on | What it asks |
73
+ |---|---|---|
74
+ | **reachable** | frontend | Does the address answer, and with what status? |
75
+ | **https** | frontend and API | Does plain `http://` redirect to `https://`, and permanently (301/308)? |
76
+ | **headers** | frontend | Are the browser security headers set? Strict-Transport-Security (at least 6 months), X-Content-Type-Options (`nosniff`), Content-Security-Policy, X-Frame-Options (or CSP `frame-ancestors`), Referrer-Policy |
77
+ | **cors** | API | Asks the API as your frontend, then as a made-up site (`deploy-checkup-test.invalid`). Is your frontend allowed and everyone else blocked? |
78
+ | **health** | API | Calls the health path 3 times. Does it exist, and what's the median response time? |
79
+ | **cold_start** | API | Was the first of those requests much slower than the rest (a free tier waking up)? Reported as a hint, not a verdict. |
80
+ | **exposed_files** | frontend and API | Can anyone download `/.env` or `/.git/config`? **Probing:** runs only after you confirm you own the site. |
81
+
82
+ The headers check skips the API on purpose: a JSON API doesn't need CSP or X-Frame-Options, and flagging them would be a false alarm.
83
+
84
+ **Fix hints match your host.** When a response carries Render's `x-render-origin-server` header, fixes point to the exact place in the Render dashboard. On any other host you get general advice.
85
+
86
+ ### Probing checks ask first
87
+
88
+ Most checks send ordinary requests any browser could send. The exposed files check asks for paths like `/.env`, which on someone else's site can look like an attack. So deploy-checkup asks *"Do you own this site?"* before running it. If nobody can answer (in CI, for example), it is skipped and the report says so, unless you pass `--yes-i-own-it`. A leaked file's contents are never shown in the report.
89
+
90
+ ### Polite by design
91
+
92
+ Every request uses a 60-second timeout (Render's free tier can take about 50 seconds to wake up) and identifies itself as `User-Agent: DeployCheckup/<version>`, so site owners can see what called them. A full run sends a few dozen ordinary requests.
93
+
94
+ ### What it doesn't do
95
+
96
+ deploy-checkup is a quick first look, not a full security audit:
97
+
98
+ - It only sends ordinary requests. It doesn't try to break in, and it isn't a replacement for tools like OWASP ZAP.
99
+ - CORS is tested on the one API address you give it, and health on one path.
100
+ - Cold start detection is a guess from a few timings.
101
+ - Fix hints are written for Render first; other hosts get general advice.
102
+
103
+ ## JSON output
104
+
105
+ `--json` prints the same findings for scripts and CI. Only the JSON goes to standard output; the permission question goes to standard error, so you can save the report safely:
106
+
107
+ ```bash
108
+ deploy-checkup https://your-frontend.onrender.com --api https://your-api.onrender.com --json > report.json
109
+ ```
110
+
111
+ ```json
112
+ {
113
+ "tool": "deploy-checkup",
114
+ "version": "0.1.0",
115
+ "targets": [
116
+ {
117
+ "url": "https://your-api.onrender.com",
118
+ "findings": [
119
+ {"check": "health", "level": "pass", "title": "API answered in 108 ms (median of 2)", "why": "", "fix": ""}
120
+ ]
121
+ }
122
+ ],
123
+ "summary": {"pass": 16, "warning": 0, "fail": 0, "info": 0}
124
+ }
125
+ ```
126
+
127
+ The exit code is the same as for the coloured report.
128
+
129
+ ## Development
130
+
131
+ ```bash
132
+ git clone https://github.com/Nandhagopal2912/deploy-checkup.git
133
+ cd deploy-checkup
134
+ python -m venv .venv
135
+ .venv\Scripts\activate # Windows; on macOS/Linux: source .venv/bin/activate
136
+ pip install -e ".[dev]"
137
+ pytest
138
+ ```
139
+
140
+ `-e` installs in editable mode, so code changes take effect without reinstalling. Re-run the install after changing `dependencies` or `[project.scripts]` in `pyproject.toml`.
141
+
142
+ Tests never touch the network. Each check receives an `httpx.Client`, and tests pass one built on `httpx.MockTransport`, which answers requests with fake responses. CI runs them on Python 3.10 and 3.13.
143
+
144
+ [`demo/`](https://github.com/Nandhagopal2912/deploy-checkup/tree/main/demo) holds a deliberately broken app with known flaws, used to prove each check catches what it should. The project's plan is in the [roadmap](https://github.com/Nandhagopal2912/deploy-checkup/blob/main/docs/Deploy%20Doctor%20Roadmap.pdf).
145
+
146
+ ## License
147
+
148
+ [MIT](https://github.com/Nandhagopal2912/deploy-checkup/blob/main/LICENSE)
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "deploy-checkup"
7
+ version = "0.1.0"
8
+ description = "Check whether your deployed app is set up safely, in plain language, with fixes for your host."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Nandhagopal" }]
13
+ requires-python = ">=3.10"
14
+ keywords = ["deploy", "security", "cors", "headers", "https", "render", "cli"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Security",
25
+ "Topic :: Internet :: WWW/HTTP",
26
+ ]
27
+ dependencies = [
28
+ "httpx >= 0.28,<1",
29
+ "rich >= 14,<16",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "pytest >=8",
35
+ "build",
36
+ "twine",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/Nandhagopal2912/deploy-checkup"
41
+ Issues = "https://github.com/Nandhagopal2912/deploy-checkup/issues"
42
+
43
+ [project.scripts]
44
+ deploy-checkup = "deploy_checkup.cli:main"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,37 @@
1
+ from statistics import median
2
+
3
+ from deploy_checkup.models import Finding, Level
4
+
5
+ CHECK = "cold_start"
6
+ COLD_MS = 5000 # a first request this slow usually means the server was asleep
7
+
8
+
9
+ def check_cold_start(times_ms: list[float]) -> Finding:
10
+ """Compare the first response time with the ones after it."""
11
+ first = round(times_ms[0])
12
+ warm = round(median(times_ms[1:]))
13
+
14
+ if first >= COLD_MS and first > 3 * warm:
15
+ return Finding(
16
+ check=CHECK,
17
+ level=Level.INFO,
18
+ title=f"First request took {first / 1000:.1f} s, later ones {warm} ms: free tier waking up?",
19
+ why=(
20
+ "Free hosting plans often put an app to sleep when nobody visits, "
21
+ "and the next visitor waits while it starts again."
22
+ ),
23
+ fix=(
24
+ "Say so in your README so visitors know to wait, or use a plan that "
25
+ "keeps the server running."
26
+ ),
27
+ render_fix=(
28
+ "Render's free tier sleeps after 15 minutes without visitors. Say so in "
29
+ "your README, or move the service to a paid instance type to keep it awake."
30
+ ),
31
+ )
32
+
33
+ return Finding(
34
+ check=CHECK,
35
+ level=Level.PASS,
36
+ title=f"No cold start this time (first request {first} ms)",
37
+ )
@@ -0,0 +1,85 @@
1
+ import httpx
2
+
3
+ from deploy_checkup.models import Finding, Level
4
+
5
+ CHECK = "cors"
6
+ MADE_UP_SITE = "https://deploy-checkup-test.invalid" # .invalid can never be a real website
7
+
8
+
9
+ def check_cors(client: httpx.Client, api_url: str, frontend_url: str) -> list[Finding]:
10
+ """Ask the API as your frontend, then as a made-up site, and compare the answers."""
11
+ frontend = _origin(frontend_url)
12
+ try:
13
+ as_frontend = client.get(api_url, headers={"Origin": frontend})
14
+ as_stranger = client.get(api_url, headers={"Origin": MADE_UP_SITE})
15
+ except httpx.RequestError:
16
+ return [] # the other checks already report connection problems
17
+
18
+ return [
19
+ _check_own_frontend(as_frontend, frontend),
20
+ _check_other_sites(as_stranger),
21
+ ]
22
+
23
+
24
+ def _origin(url: str) -> str:
25
+ """The part of a URL a browser sends as Origin: scheme, host and (if unusual) port."""
26
+ parsed = httpx.URL(url)
27
+ origin = f"{parsed.scheme}://{parsed.host}"
28
+ if parsed.port is not None:
29
+ origin += f":{parsed.port}"
30
+ return origin
31
+
32
+
33
+ def _check_own_frontend(response: httpx.Response, frontend: str) -> Finding:
34
+ allowed = response.headers.get("access-control-allow-origin")
35
+ if allowed in (frontend, "*"):
36
+ return Finding(check=CHECK, level=Level.PASS, title=f"API allows your frontend ({frontend})")
37
+
38
+ return Finding(
39
+ check=CHECK,
40
+ level=Level.WARNING,
41
+ title=f"API doesn't allow your frontend ({frontend})",
42
+ why=(
43
+ "Browsers will block your frontend from reading the API's answers, "
44
+ "so pages that load data will break."
45
+ ),
46
+ fix=(
47
+ f"Add {frontend} to your API's allowed origins (often set through an "
48
+ "environment variable), then redeploy."
49
+ ),
50
+ render_fix=(
51
+ f"Add {frontend} to your API's allowed origins, for example through an "
52
+ "environment variable in the Render dashboard, then redeploy."
53
+ ),
54
+ )
55
+
56
+
57
+ def _check_other_sites(response: httpx.Response) -> Finding:
58
+ allowed = response.headers.get("access-control-allow-origin")
59
+ with_cookies = response.headers.get("access-control-allow-credentials", "").lower() == "true"
60
+
61
+ if allowed is None:
62
+ return Finding(check=CHECK, level=Level.PASS, title="API blocks other websites")
63
+
64
+ if allowed == MADE_UP_SITE and with_cookies:
65
+ return Finding(
66
+ check=CHECK,
67
+ level=Level.FAIL,
68
+ title="API lets any website read it with your visitors' cookies",
69
+ why=(
70
+ "A stranger's site can call your API from a logged-in visitor's browser "
71
+ "and read their private data."
72
+ ),
73
+ fix=(
74
+ "List your real frontend address as the only allowed origin. "
75
+ "Never combine 'allow every origin' with allow_credentials."
76
+ ),
77
+ )
78
+
79
+ return Finding(
80
+ check=CHECK,
81
+ level=Level.WARNING,
82
+ title=f"API accepts requests from any website (Access-Control-Allow-Origin: {allowed})",
83
+ why="Any site can read your API's answers. That's fine for public data, risky for anything else.",
84
+ fix="If the API isn't meant to be public, allow only your frontend's address.",
85
+ )
@@ -0,0 +1,65 @@
1
+ import re
2
+
3
+ import httpx
4
+
5
+ from deploy_checkup.models import Finding, Level
6
+
7
+ CHECK = "exposed_files"
8
+
9
+
10
+ def _looks_like_env(body: str) -> bool:
11
+ return re.search(r"^[A-Z][A-Z0-9_]*=", body, re.MULTILINE) is not None
12
+
13
+
14
+ def _looks_like_git_config(body: str) -> bool:
15
+ return "[core]" in body
16
+
17
+
18
+ # Each path, and how to recognise the real file (so a friendly 200 page isn't a false alarm).
19
+ PATHS = {
20
+ "/.env": _looks_like_env,
21
+ "/.git/config": _looks_like_git_config,
22
+ }
23
+
24
+
25
+ def check_exposed_files(client: httpx.Client, url: str) -> list[Finding]:
26
+ """Ask for a few files that should never be public, and check what comes back."""
27
+ findings = []
28
+ for path, looks_real in PATHS.items():
29
+ try:
30
+ response = client.get(httpx.URL(url).join(path))
31
+ except httpx.RequestError:
32
+ return [] # the other checks already report connection problems
33
+
34
+ is_html = "text/html" in response.headers.get("content-type", "")
35
+ if response.status_code == 200 and not is_html and looks_real(response.text):
36
+ findings.append(_exposed(path))
37
+ else:
38
+ findings.append(Finding(check=CHECK, level=Level.PASS, title=f"{path} is not exposed"))
39
+ return findings
40
+
41
+
42
+ def _exposed(path: str) -> Finding:
43
+ return Finding(
44
+ check=CHECK,
45
+ level=Level.FAIL,
46
+ title=f"{path} is publicly downloadable",
47
+ why=(
48
+ "Anyone can download this file. It usually holds passwords, API keys or "
49
+ "your code's history, which attackers collect automatically."
50
+ ),
51
+ fix=(
52
+ f"Stop serving {path} (don't copy it into your build or static folder), "
53
+ "then change every secret it contained: assume they're already leaked."
54
+ ),
55
+ )
56
+
57
+
58
+ def skipped() -> Finding:
59
+ return Finding(
60
+ check=CHECK,
61
+ level=Level.INFO,
62
+ title="Exposed files check skipped (needs your permission)",
63
+ why="It asks for files like /.env, which only the site's owner should do.",
64
+ fix="Run again and answer yes, or pass --yes-i-own-it in CI.",
65
+ )