djcheckup 0.2.1__tar.gz → 0.2.3__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.
- {djcheckup-0.2.1 → djcheckup-0.2.3}/PKG-INFO +2 -2
- {djcheckup-0.2.1 → djcheckup-0.2.3}/pyproject.toml +22 -2
- {djcheckup-0.2.1 → djcheckup-0.2.3}/src/djcheckup/check_defs.py +2 -2
- {djcheckup-0.2.1 → djcheckup-0.2.3}/src/djcheckup/checks.py +46 -32
- {djcheckup-0.2.1 → djcheckup-0.2.3}/LICENSE +0 -0
- {djcheckup-0.2.1 → djcheckup-0.2.3}/README.md +0 -0
- {djcheckup-0.2.1 → djcheckup-0.2.3}/src/djcheckup/__init__.py +0 -0
- {djcheckup-0.2.1 → djcheckup-0.2.3}/src/djcheckup/cli.py +0 -0
- {djcheckup-0.2.1 → djcheckup-0.2.3}/src/djcheckup/outputs.py +0 -0
- {djcheckup-0.2.1 → djcheckup-0.2.3}/src/djcheckup/run.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: djcheckup
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: DJ Checkup is a security scanner for Django sites.
|
|
5
5
|
Author: Stuart Maxwell
|
|
6
6
|
Author-email: Stuart Maxwell <stuart@amanzi.nz>
|
|
@@ -35,7 +35,7 @@ License: BSD 3-Clause License
|
|
|
35
35
|
Requires-Dist: httpx>=0.28.1
|
|
36
36
|
Requires-Dist: rich>=14.1.0
|
|
37
37
|
Requires-Dist: typer>=0.17.4
|
|
38
|
-
Requires-Python: >=3.
|
|
38
|
+
Requires-Python: >=3.10
|
|
39
39
|
Project-URL: Homepage, https://github.com/stuartmaxwell/djcheckup
|
|
40
40
|
Project-URL: Repository, https://github.com/stuartmaxwell/djcheckup
|
|
41
41
|
Description-Content-Type: text/markdown
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "djcheckup"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3"
|
|
4
4
|
description = "DJ Checkup is a security scanner for Django sites."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Stuart Maxwell", email = "stuart@amanzi.nz" }]
|
|
7
|
-
requires-python = ">=3.
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
8
|
dependencies = ["httpx>=0.28.1", "rich>=14.1.0", "typer>=0.17.4"]
|
|
9
9
|
license = { file = "LICENSE" }
|
|
10
10
|
|
|
@@ -26,5 +26,25 @@ target-version = "py313"
|
|
|
26
26
|
[tool.ruff.lint]
|
|
27
27
|
select = ["ALL"]
|
|
28
28
|
|
|
29
|
+
[tool.ruff.lint.extend-per-file-ignores]
|
|
30
|
+
"tests/**" = ["S101", "ANN201", "ANN001", "ARG001"]
|
|
31
|
+
|
|
29
32
|
[tool.ruff.lint.pydocstyle]
|
|
30
33
|
convention = "google" # Accepts: "google", "numpy", or "pep257".
|
|
34
|
+
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
dev = ["nox>=2025.5.1", "pytest>=8.4.2", "pytest-cov>=7.0.0"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
pythonpath = "."
|
|
40
|
+
python_files = "test_*.py"
|
|
41
|
+
|
|
42
|
+
[tool.coverage.html]
|
|
43
|
+
show_contexts = true
|
|
44
|
+
|
|
45
|
+
[tool.coverage.run]
|
|
46
|
+
branch = true
|
|
47
|
+
include = ["src/djcheckup/*"]
|
|
48
|
+
|
|
49
|
+
[tool.coverage.report]
|
|
50
|
+
# exclude_lines = ["if TYPE_CHECKING:", "# pragma: no cover"]
|
|
@@ -56,7 +56,7 @@ csrf_httponly_check = CookieHttpOnlyCheck(
|
|
|
56
56
|
depends_on="csrf_check",
|
|
57
57
|
name="Is the CSRF cookie HttpOnly?",
|
|
58
58
|
cookie_name="csrftoken",
|
|
59
|
-
success=
|
|
59
|
+
success=True,
|
|
60
60
|
severity=SeverityWeight.MEDIUM,
|
|
61
61
|
success_message="Your CSRF cookie is marked as HttpOnly, which helps prevent some XSS attacks.",
|
|
62
62
|
failure_message="""
|
|
@@ -211,7 +211,7 @@ Reference:
|
|
|
211
211
|
login_check = PathCheck(
|
|
212
212
|
check_id="login_check",
|
|
213
213
|
name="Is your login page exposed at a default or guessable URL?",
|
|
214
|
-
path="/accounts/login
|
|
214
|
+
path="/accounts/login",
|
|
215
215
|
success=False,
|
|
216
216
|
severity=SeverityWeight.MEDIUM,
|
|
217
217
|
success_message="Login page is not exposed at the default URL. This reduces the risk of automated attacks.",
|
|
@@ -4,7 +4,7 @@ from abc import ABC, abstractmethod
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from enum import Enum
|
|
6
6
|
from http.cookiejar import CookieJar
|
|
7
|
-
from typing import Literal
|
|
7
|
+
from typing import Literal
|
|
8
8
|
|
|
9
9
|
import httpx
|
|
10
10
|
|
|
@@ -99,25 +99,6 @@ class _BaseCheck(ABC):
|
|
|
99
99
|
)
|
|
100
100
|
|
|
101
101
|
|
|
102
|
-
@dataclass
|
|
103
|
-
class HeaderCheck(_BaseCheck):
|
|
104
|
-
"""A header check."""
|
|
105
|
-
|
|
106
|
-
header_name: str
|
|
107
|
-
header_value: str = ""
|
|
108
|
-
|
|
109
|
-
def check(self, context: SiteCheckContext) -> bool:
|
|
110
|
-
"""Check if a specific header is present and optionally if it matches a given value."""
|
|
111
|
-
# Normalize header names to lowercase for case-insensitive comparison
|
|
112
|
-
headers = {k.lower(): v for k, v in context.headers.items()}
|
|
113
|
-
actual_value = headers.get(self.header_name.lower())
|
|
114
|
-
if actual_value is None:
|
|
115
|
-
return False
|
|
116
|
-
|
|
117
|
-
# Compare header values case-insensitively if header_value is provided
|
|
118
|
-
return not (self.header_value and actual_value.lower() != self.header_value.lower())
|
|
119
|
-
|
|
120
|
-
|
|
121
102
|
@dataclass
|
|
122
103
|
class ContentCheck(_BaseCheck):
|
|
123
104
|
"""A content check.
|
|
@@ -230,6 +211,25 @@ class CookieSecureCheck(_BaseCheck):
|
|
|
230
211
|
return False
|
|
231
212
|
|
|
232
213
|
|
|
214
|
+
@dataclass
|
|
215
|
+
class HeaderCheck(_BaseCheck):
|
|
216
|
+
"""A header check."""
|
|
217
|
+
|
|
218
|
+
header_name: str
|
|
219
|
+
header_value: str = ""
|
|
220
|
+
|
|
221
|
+
def check(self, context: SiteCheckContext) -> bool:
|
|
222
|
+
"""Check if a specific header is present and optionally if it matches a given value."""
|
|
223
|
+
# Normalize header names to lowercase for case-insensitive comparison
|
|
224
|
+
headers = {k.lower(): v for k, v in context.headers.items()}
|
|
225
|
+
actual_value = headers.get(self.header_name.lower())
|
|
226
|
+
if actual_value is None:
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
# Compare header values case-insensitively if header_value is provided
|
|
230
|
+
return not (self.header_value and actual_value.lower() != self.header_value.lower())
|
|
231
|
+
|
|
232
|
+
|
|
233
233
|
@dataclass
|
|
234
234
|
class PathCheck(_BaseCheck):
|
|
235
235
|
"""A path check.
|
|
@@ -274,8 +274,8 @@ class SchemeCheck(_BaseCheck):
|
|
|
274
274
|
def check(self, context: SiteCheckContext) -> bool:
|
|
275
275
|
"""Check if the scheme of the URL in the request matches the final scheme."""
|
|
276
276
|
# If the start scheme matches the original URL scheme, then we don't need a new request.
|
|
277
|
-
if context.url.scheme == self.start_scheme
|
|
278
|
-
return
|
|
277
|
+
if context.url.scheme == self.start_scheme:
|
|
278
|
+
return context.response_url.scheme == self.end_scheme
|
|
279
279
|
|
|
280
280
|
# Need to create a new URL with the specified start scheme
|
|
281
281
|
new_url = context.url.copy_with(scheme=self.start_scheme)
|
|
@@ -286,6 +286,27 @@ class SchemeCheck(_BaseCheck):
|
|
|
286
286
|
return response.url.scheme == self.end_scheme
|
|
287
287
|
|
|
288
288
|
|
|
289
|
+
def create_context(url: httpx.URL, client: httpx.Client, response: httpx.Response) -> SiteCheckContext:
|
|
290
|
+
"""Create a SiteCheckContext context object.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
url: The URL to check.
|
|
294
|
+
client: The HTTPX client to use for requests.
|
|
295
|
+
response: The HTTPX response object from the initial request.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
A SiteCheckContext object containing the provided parameters.
|
|
299
|
+
"""
|
|
300
|
+
return SiteCheckContext(
|
|
301
|
+
url=url,
|
|
302
|
+
client=client,
|
|
303
|
+
headers=response.headers,
|
|
304
|
+
cookies=response.cookies.jar,
|
|
305
|
+
content=response.text,
|
|
306
|
+
response_url=response.url,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
289
310
|
class SiteChecker:
|
|
290
311
|
"""Run all the checks for a given site."""
|
|
291
312
|
|
|
@@ -328,7 +349,7 @@ class SiteChecker:
|
|
|
328
349
|
self.client.close()
|
|
329
350
|
|
|
330
351
|
# Context manager support for automatic cleanup
|
|
331
|
-
def __enter__(self) ->
|
|
352
|
+
def __enter__(self) -> "SiteChecker":
|
|
332
353
|
"""Context manager entry."""
|
|
333
354
|
return self
|
|
334
355
|
|
|
@@ -351,7 +372,7 @@ class SiteChecker:
|
|
|
351
372
|
response = self.client.get(self.url)
|
|
352
373
|
response.raise_for_status()
|
|
353
374
|
|
|
354
|
-
except httpx.
|
|
375
|
+
except httpx.HTTPStatusError:
|
|
355
376
|
return CheckResponse(
|
|
356
377
|
name=check_name,
|
|
357
378
|
result=CheckResult.FAILURE,
|
|
@@ -359,14 +380,7 @@ class SiteChecker:
|
|
|
359
380
|
message=error_message,
|
|
360
381
|
)
|
|
361
382
|
|
|
362
|
-
self.context =
|
|
363
|
-
url=self.url,
|
|
364
|
-
client=self.client,
|
|
365
|
-
headers=response.headers,
|
|
366
|
-
cookies=response.cookies.jar,
|
|
367
|
-
content=response.text,
|
|
368
|
-
response_url=response.url,
|
|
369
|
-
)
|
|
383
|
+
self.context = create_context(self.url, self.client, response)
|
|
370
384
|
|
|
371
385
|
return CheckResponse(
|
|
372
386
|
name=check_name,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|