djcheckup 0.2.5__tar.gz → 0.3.0__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.5
3
+ Version: 0.3.0
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>
@@ -32,6 +32,12 @@ License: BSD 3-Clause License
32
32
  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
33
33
  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34
34
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35
+ Classifier: Programming Language :: Python
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Programming Language :: Python :: 3.14
35
41
  Requires-Dist: httpx>=0.28.1
36
42
  Requires-Dist: rich>=14.1.0
37
43
  Requires-Dist: typer>=0.17.4
@@ -1,12 +1,20 @@
1
1
  [project]
2
2
  name = "djcheckup"
3
- version = "0.2.5"
3
+ version = "0.3.0"
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
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
+ classifiers = [
11
+ "Programming Language :: Python",
12
+ "Programming Language :: Python :: 3.10",
13
+ "Programming Language :: Python :: 3.11",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
16
+ "Programming Language :: Python :: 3.14",
17
+ ]
10
18
 
11
19
  [project.scripts]
12
20
  djcheckup = "djcheckup.cli:app"
@@ -0,0 +1,6 @@
1
+ """DJ Checkup CLI package."""
2
+
3
+ from djcheckup.outputs import SiteCheckResultDict
4
+ from djcheckup.run import run_checks
5
+
6
+ __all__ = ["SiteCheckResultDict", "run_checks"]
@@ -7,7 +7,7 @@ import typer
7
7
 
8
8
  from djcheckup.check_defs import all_checks
9
9
  from djcheckup.checks import SiteChecker
10
- from djcheckup.outputs import output_results_as_json, rich_output
10
+ from djcheckup.outputs import rich_output, sitecheck_as_json
11
11
 
12
12
  app = typer.Typer()
13
13
 
@@ -26,6 +26,6 @@ def run_checks(
26
26
  results = checker.run_checks(all_checks)
27
27
 
28
28
  if output_json:
29
- rich.print_json(output_results_as_json(results))
29
+ rich.print_json(sitecheck_as_json(results))
30
30
  else:
31
31
  rich_output(results)
@@ -0,0 +1,96 @@
1
+ """Output formats for the site check results."""
2
+
3
+ import json
4
+ from collections.abc import Mapping, Sequence
5
+ from dataclasses import asdict
6
+ from enum import Enum
7
+ from typing import TypedDict
8
+
9
+ from rich.console import Console
10
+ from rich.markdown import Markdown
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+
14
+ from djcheckup.checks import CheckResult, SiteCheckResult
15
+
16
+
17
+ class CheckResponseDict(TypedDict):
18
+ """Represents the response for a single check."""
19
+
20
+ name: str
21
+ result: str # from CheckResult
22
+ severity_score: int # from SeverityWeight
23
+ message: str
24
+
25
+
26
+ class SiteCheckResultDict(TypedDict):
27
+ """Represents the response for a site check."""
28
+
29
+ url: str
30
+ check_results: list[CheckResponseDict]
31
+
32
+
33
+ def rich_output(check_results: SiteCheckResult) -> None:
34
+ """Display results using Rich."""
35
+ console = Console()
36
+
37
+ table = Table(title=f"DJ Checkup Results for {check_results.url}", show_lines=True)
38
+
39
+ table.add_column("Check", justify="left")
40
+ table.add_column("Result", justify="left")
41
+ table.add_column("Message", justify="left")
42
+
43
+ for result in check_results.check_results:
44
+ emoji = ""
45
+ if result.result == CheckResult.SUCCESS:
46
+ emoji = "🟢 "
47
+
48
+ if result.result == CheckResult.FAILURE:
49
+ emoji = "🔴 "
50
+
51
+ if result.result == CheckResult.SKIPPED:
52
+ emoji = "🟡 "
53
+
54
+ display_result = emoji + result.result.value.capitalize()
55
+ table.add_row(result.name, display_result, Markdown(result.message))
56
+
57
+ console.print(Panel(table))
58
+
59
+
60
+ type JSONValue = str | int | Mapping[str, "JSONValue"] | Sequence["JSONValue"]
61
+
62
+
63
+ def normalize_for_json(obj: JSONValue) -> JSONValue:
64
+ """Recursively convert a dict with Enums to a json-serialisable dict.
65
+
66
+ This is used to convert the SiteCheckResult object to one that can be converted directly to JSON.
67
+ """
68
+ # dict-like objects
69
+ if isinstance(obj, Mapping):
70
+ return {k: normalize_for_json(v) for k, v in obj.items()}
71
+
72
+ # list-like objects
73
+ if isinstance(obj, Sequence) and not isinstance(obj, (str, bytes, bytearray)):
74
+ return [normalize_for_json(v) for v in obj]
75
+
76
+ # Enums, e.g. SeverityWeight and CheckResult
77
+ if isinstance(obj, Enum):
78
+ return obj.value
79
+
80
+ if isinstance(obj, (str, int)):
81
+ return obj
82
+
83
+ # The following code should never be reached
84
+ msg = f"Unexpected type: {type(obj)}" # pragma: no cover
85
+ raise TypeError(msg) # pragma: no cover
86
+
87
+
88
+ def sitecheck_as_dict(site_result: SiteCheckResult) -> SiteCheckResultDict:
89
+ """Convert SiteCheckResult into JSON-serialisable dict."""
90
+ return normalize_for_json(asdict(site_result)) # type: ignore # noqa: PGH003
91
+
92
+
93
+ def sitecheck_as_json(site_result: SiteCheckResult) -> str:
94
+ """Convert SiteCheckResult into JSON string."""
95
+ data = sitecheck_as_dict(site_result)
96
+ return json.dumps(data)
@@ -5,8 +5,8 @@ from typing import Literal, overload
5
5
  import httpx
6
6
 
7
7
  from djcheckup.check_defs import all_checks
8
- from djcheckup.checks import SiteChecker, SiteCheckResult
9
- from djcheckup.outputs import output_results_as_json
8
+ from djcheckup.checks import SiteChecker
9
+ from djcheckup.outputs import SiteCheckResultDict, sitecheck_as_dict, sitecheck_as_json
10
10
 
11
11
 
12
12
  @overload
@@ -14,7 +14,7 @@ def run_checks(
14
14
  url: str,
15
15
  output_format: Literal["object"] = "object",
16
16
  client: httpx.Client | None = None,
17
- ) -> SiteCheckResult: ...
17
+ ) -> SiteCheckResultDict: ...
18
18
  @overload
19
19
  def run_checks(url: str, output_format: Literal["json"], client: httpx.Client | None = None) -> str: ...
20
20
 
@@ -23,7 +23,7 @@ def run_checks(
23
23
  url: str,
24
24
  output_format: Literal["object", "json"] = "object",
25
25
  client: httpx.Client | None = None,
26
- ) -> str | SiteCheckResult:
26
+ ) -> str | SiteCheckResultDict:
27
27
  """Run the DJ Checkup tool.
28
28
 
29
29
  Args:
@@ -42,7 +42,7 @@ def run_checks(
42
42
  raise ValueError(msg)
43
43
 
44
44
  if output_format == "json":
45
- return output_results_as_json(results)
45
+ return sitecheck_as_json(results)
46
46
 
47
47
  # Default to object output
48
- return results
48
+ return sitecheck_as_dict(results)
@@ -1,5 +0,0 @@
1
- """DJ Checkup CLI package."""
2
-
3
- from djcheckup.run import run_checks
4
-
5
- __all__ = ["run_checks"]
@@ -1,56 +0,0 @@
1
- """Output formats for the site check results."""
2
-
3
- import json
4
- from dataclasses import asdict
5
-
6
- from rich.console import Console
7
- from rich.markdown import Markdown
8
- from rich.panel import Panel
9
- from rich.table import Table
10
-
11
- from djcheckup.checks import CheckResult, SeverityWeight, SiteCheckResult
12
-
13
-
14
- def rich_output(check_results: SiteCheckResult) -> None:
15
- """Display results using Rich."""
16
- console = Console()
17
-
18
- table = Table(title=f"DJ Checkup Results for {check_results.url}", show_lines=True)
19
-
20
- table.add_column("Check", justify="left")
21
- table.add_column("Result", justify="left")
22
- table.add_column("Message", justify="left")
23
-
24
- for result in check_results.check_results:
25
- emoji = ""
26
- if result.result == CheckResult.SUCCESS:
27
- emoji = "🟢 "
28
-
29
- if result.result == CheckResult.FAILURE:
30
- emoji = "🔴 "
31
-
32
- if result.result == CheckResult.SKIPPED:
33
- emoji = "🟡 "
34
-
35
- display_result = emoji + result.result.value.capitalize()
36
- table.add_row(result.name, display_result, Markdown(result.message))
37
-
38
- console.print(Panel(table))
39
-
40
-
41
- def json_encoder(obj: object) -> str | int:
42
- """Custom JSON encoder for non-serialisable types."""
43
- if isinstance(obj, SeverityWeight):
44
- return obj.value
45
-
46
- if isinstance(obj, CheckResult):
47
- return obj.value
48
-
49
- # The following should never happen since this encoder is only used for SeverityWeight and CheckResult
50
- msg = f"Object of type {type(obj).__name__} is not JSON serialisable" # pragma: no cover
51
- raise TypeError(msg) # pragma: no cover
52
-
53
-
54
- def output_results_as_json(results: SiteCheckResult) -> str:
55
- """Convert the results to JSON format."""
56
- return json.dumps(asdict(results), default=json_encoder)
File without changes
File without changes