djcheckup 0.2.2__tar.gz → 0.2.4__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.4}/PKG-INFO +2 -2
- {djcheckup-0.2.2 → djcheckup-0.2.4}/pyproject.toml +21 -3
- {djcheckup-0.2.2 → djcheckup-0.2.4}/src/djcheckup/check_defs.py +2 -2
- {djcheckup-0.2.2 → djcheckup-0.2.4}/src/djcheckup/checks.py +34 -26
- {djcheckup-0.2.2 → djcheckup-0.2.4}/LICENSE +0 -0
- {djcheckup-0.2.2 → djcheckup-0.2.4}/README.md +0 -0
- {djcheckup-0.2.2 → djcheckup-0.2.4}/src/djcheckup/__init__.py +0 -0
- {djcheckup-0.2.2 → djcheckup-0.2.4}/src/djcheckup/cli.py +0 -0
- {djcheckup-0.2.2 → djcheckup-0.2.4}/src/djcheckup/outputs.py +0 -0
- {djcheckup-0.2.2 → djcheckup-0.2.4}/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.4
|
|
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.4"
|
|
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,6 +26,9 @@ target-version = "py313"
|
|
|
26
26
|
[tool.ruff.lint]
|
|
27
27
|
select = ["ALL"]
|
|
28
28
|
|
|
29
|
+
[tool.pyrefly]
|
|
30
|
+
project-includes = ["src/djcheckup/**"]
|
|
31
|
+
|
|
29
32
|
[tool.ruff.lint.extend-per-file-ignores]
|
|
30
33
|
"tests/**" = ["S101", "ANN201", "ANN001", "ARG001"]
|
|
31
34
|
|
|
@@ -33,8 +36,23 @@ select = ["ALL"]
|
|
|
33
36
|
convention = "google" # Accepts: "google", "numpy", or "pep257".
|
|
34
37
|
|
|
35
38
|
[dependency-groups]
|
|
36
|
-
dev = [
|
|
39
|
+
dev = [
|
|
40
|
+
"nox>=2025.5.1",
|
|
41
|
+
"pyrefly>=0.34.0",
|
|
42
|
+
"pytest>=8.4.2",
|
|
43
|
+
"pytest-cov>=7.0.0",
|
|
44
|
+
]
|
|
37
45
|
|
|
38
46
|
[tool.pytest.ini_options]
|
|
39
47
|
pythonpath = "."
|
|
40
48
|
python_files = "test_*.py"
|
|
49
|
+
|
|
50
|
+
[tool.coverage.html]
|
|
51
|
+
show_contexts = true
|
|
52
|
+
|
|
53
|
+
[tool.coverage.run]
|
|
54
|
+
branch = true
|
|
55
|
+
include = ["src/djcheckup/*"]
|
|
56
|
+
|
|
57
|
+
[tool.coverage.report]
|
|
58
|
+
# 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.",
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""Check types."""
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import Sequence
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from enum import Enum
|
|
6
7
|
from http.cookiejar import CookieJar
|
|
7
|
-
from typing import Literal
|
|
8
|
+
from typing import Literal
|
|
8
9
|
|
|
9
10
|
import httpx
|
|
10
11
|
|
|
@@ -99,25 +100,6 @@ class _BaseCheck(ABC):
|
|
|
99
100
|
)
|
|
100
101
|
|
|
101
102
|
|
|
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
103
|
@dataclass
|
|
122
104
|
class ContentCheck(_BaseCheck):
|
|
123
105
|
"""A content check.
|
|
@@ -230,6 +212,25 @@ class CookieSecureCheck(_BaseCheck):
|
|
|
230
212
|
return False
|
|
231
213
|
|
|
232
214
|
|
|
215
|
+
@dataclass
|
|
216
|
+
class HeaderCheck(_BaseCheck):
|
|
217
|
+
"""A header check."""
|
|
218
|
+
|
|
219
|
+
header_name: str
|
|
220
|
+
header_value: str = ""
|
|
221
|
+
|
|
222
|
+
def check(self, context: SiteCheckContext) -> bool:
|
|
223
|
+
"""Check if a specific header is present and optionally if it matches a given value."""
|
|
224
|
+
# Normalize header names to lowercase for case-insensitive comparison
|
|
225
|
+
headers = {k.lower(): v for k, v in context.headers.items()}
|
|
226
|
+
actual_value = headers.get(self.header_name.lower())
|
|
227
|
+
if actual_value is None:
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
# Compare header values case-insensitively if header_value is provided
|
|
231
|
+
return not (self.header_value and actual_value.lower() != self.header_value.lower())
|
|
232
|
+
|
|
233
|
+
|
|
233
234
|
@dataclass
|
|
234
235
|
class PathCheck(_BaseCheck):
|
|
235
236
|
"""A path check.
|
|
@@ -274,8 +275,8 @@ class SchemeCheck(_BaseCheck):
|
|
|
274
275
|
def check(self, context: SiteCheckContext) -> bool:
|
|
275
276
|
"""Check if the scheme of the URL in the request matches the final scheme."""
|
|
276
277
|
# 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
|
|
278
|
+
if context.url.scheme == self.start_scheme:
|
|
279
|
+
return context.response_url.scheme == self.end_scheme
|
|
279
280
|
|
|
280
281
|
# Need to create a new URL with the specified start scheme
|
|
281
282
|
new_url = context.url.copy_with(scheme=self.start_scheme)
|
|
@@ -349,7 +350,7 @@ class SiteChecker:
|
|
|
349
350
|
self.client.close()
|
|
350
351
|
|
|
351
352
|
# Context manager support for automatic cleanup
|
|
352
|
-
def __enter__(self) ->
|
|
353
|
+
def __enter__(self) -> "SiteChecker":
|
|
353
354
|
"""Context manager entry."""
|
|
354
355
|
return self
|
|
355
356
|
|
|
@@ -372,7 +373,7 @@ class SiteChecker:
|
|
|
372
373
|
response = self.client.get(self.url)
|
|
373
374
|
response.raise_for_status()
|
|
374
375
|
|
|
375
|
-
except httpx.
|
|
376
|
+
except httpx.HTTPStatusError:
|
|
376
377
|
return CheckResponse(
|
|
377
378
|
name=check_name,
|
|
378
379
|
result=CheckResult.FAILURE,
|
|
@@ -389,8 +390,15 @@ class SiteChecker:
|
|
|
389
390
|
message=success_message,
|
|
390
391
|
)
|
|
391
392
|
|
|
392
|
-
def run_checks(self, checks:
|
|
393
|
-
"""Run all checks.
|
|
393
|
+
def run_checks(self, checks: Sequence[_BaseCheck]) -> SiteCheckResult:
|
|
394
|
+
"""Run all checks.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
checks: Accepts any sequence (list, tuple, etc.) of check instances inheriting from _BaseCheck.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
A SiteCheckResult object containing the results of all checks.
|
|
401
|
+
"""
|
|
394
402
|
# First, run the first check
|
|
395
403
|
first_check = self.run_first_check()
|
|
396
404
|
site_check_results = SiteCheckResult(url=str(self.url), check_results=[first_check])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|