djcheckup 0.2.2__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.2 → djcheckup-0.2.3}/PKG-INFO +2 -2
- {djcheckup-0.2.2 → djcheckup-0.2.3}/pyproject.toml +13 -3
- {djcheckup-0.2.2 → djcheckup-0.2.3}/src/djcheckup/check_defs.py +2 -2
- {djcheckup-0.2.2 → djcheckup-0.2.3}/src/djcheckup/checks.py +24 -24
- {djcheckup-0.2.2 → djcheckup-0.2.3}/LICENSE +0 -0
- {djcheckup-0.2.2 → djcheckup-0.2.3}/README.md +0 -0
- {djcheckup-0.2.2 → djcheckup-0.2.3}/src/djcheckup/__init__.py +0 -0
- {djcheckup-0.2.2 → djcheckup-0.2.3}/src/djcheckup/cli.py +0 -0
- {djcheckup-0.2.2 → djcheckup-0.2.3}/src/djcheckup/outputs.py +0 -0
- {djcheckup-0.2.2 → 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
|
|
|
@@ -33,8 +33,18 @@ select = ["ALL"]
|
|
|
33
33
|
convention = "google" # Accepts: "google", "numpy", or "pep257".
|
|
34
34
|
|
|
35
35
|
[dependency-groups]
|
|
36
|
-
dev = ["pytest>=8.4.2"]
|
|
36
|
+
dev = ["nox>=2025.5.1", "pytest>=8.4.2", "pytest-cov>=7.0.0"]
|
|
37
37
|
|
|
38
38
|
[tool.pytest.ini_options]
|
|
39
39
|
pythonpath = "."
|
|
40
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)
|
|
@@ -349,7 +349,7 @@ class SiteChecker:
|
|
|
349
349
|
self.client.close()
|
|
350
350
|
|
|
351
351
|
# Context manager support for automatic cleanup
|
|
352
|
-
def __enter__(self) ->
|
|
352
|
+
def __enter__(self) -> "SiteChecker":
|
|
353
353
|
"""Context manager entry."""
|
|
354
354
|
return self
|
|
355
355
|
|
|
@@ -372,7 +372,7 @@ class SiteChecker:
|
|
|
372
372
|
response = self.client.get(self.url)
|
|
373
373
|
response.raise_for_status()
|
|
374
374
|
|
|
375
|
-
except httpx.
|
|
375
|
+
except httpx.HTTPStatusError:
|
|
376
376
|
return CheckResponse(
|
|
377
377
|
name=check_name,
|
|
378
378
|
result=CheckResult.FAILURE,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|