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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: djcheckup
3
- Version: 0.2.1
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.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.1"
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.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,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=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.",
@@ -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, Self
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 and context.response_url.scheme == self.end_scheme:
278
- return True
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) -> 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.RequestError:
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 = SiteCheckContext(
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