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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: djcheckup
3
- Version: 0.2.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.13
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.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.13"
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 = ["pytest>=8.4.2"]
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=False,
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, Self
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 and context.response_url.scheme == self.end_scheme:
278
- return True
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) -> 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.RequestError:
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: list[_BaseCheck]) -> SiteCheckResult:
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