diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f6353ce..1ee7d30 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,12 +10,12 @@ jobs: build: strategy: matrix: - python_version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python_version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: "ubuntu-latest" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python_version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} - name: Install dependencies diff --git a/pyproject.toml b/pyproject.toml index 72e4553..0ad3588 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ authors = [ ] license = {file = "LICENSE"} urls = {repo = "https://github.com/mam-dev/security-constraints"} -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ "requests", "pyyaml", @@ -15,11 +15,11 @@ dependencies = [ dynamic = ["version"] classifiers = [ "Development Status :: 5 - Production/Stable", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Security", "License :: OSI Approved :: Apache Software License", ] @@ -118,6 +118,7 @@ ignore = [ "PTH123", "TRY003", "TRY301", "UP032", + "ISC001", ] [tool.ruff.lint.per-file-ignores] diff --git a/src/security_constraints/common.py b/src/security_constraints/common.py index d868f78..630af90 100644 --- a/src/security_constraints/common.py +++ b/src/security_constraints/common.py @@ -1,4 +1,5 @@ """This module contains common definitions for use in any other module.""" + from __future__ import annotations import abc diff --git a/src/security_constraints/github_security_advisory.py b/src/security_constraints/github_security_advisory.py index 9b905d6..f73bbc8 100644 --- a/src/security_constraints/github_security_advisory.py +++ b/src/security_constraints/github_security_advisory.py @@ -1,8 +1,9 @@ """Module for fetching vulnerabilities from the GitHub Security Advisory.""" + import logging import os import string -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set +from typing import TYPE_CHECKING, Any, Optional import requests @@ -19,7 +20,7 @@ from typing import TypedDict class _GraphQlResponseJson(TypedDict, total=False): - data: Dict[Any, Any] + data: dict[Any, Any] if sys.version_info >= (3, 10): from typing import TypeGuard @@ -87,22 +88,22 @@ def get_database_name(self) -> str: return "Github Security Advisory" def get_vulnerabilities( - self, severities: Set[SeverityLevel] - ) -> List[SecurityVulnerability]: + self, severities: set[SeverityLevel] + ) -> list[SecurityVulnerability]: """Fetch all CRITICAL vulnerabilities from GitHub Security Advisory. The SeverityLevels map trivially to GitHub's SecurityAdvisorySeverity. """ after: Optional[str] = None - vulnerabilities: List[SecurityVulnerability] = [] + vulnerabilities: list[SecurityVulnerability] = [] more_data_exists = True while more_data_exists: json_response: "_GraphQlResponseJson" = self._do_graphql_request( severities=severities, after=after ) try: - json_data: Dict[str, Any] = json_response["data"] + json_data: dict[str, Any] = json_response["data"] vulnerabilities.extend( [ SecurityVulnerability( @@ -130,7 +131,7 @@ def get_vulnerabilities( return vulnerabilities def _do_graphql_request( - self, severities: Set[SeverityLevel], after: Optional[str] = None + self, severities: set[SeverityLevel], after: Optional[str] = None ) -> "_GraphQlResponseJson": query = QUERY_TEMPLATE.substitute( first=100, diff --git a/src/security_constraints/main.py b/src/security_constraints/main.py index ec3f5d7..914ac73 100644 --- a/src/security_constraints/main.py +++ b/src/security_constraints/main.py @@ -1,10 +1,12 @@ """Main module.""" + import argparse import logging import sys +from collections.abc import Sequence from datetime import datetime, timezone from importlib.metadata import version -from typing import IO, List, Optional, Sequence, Set +from typing import IO, Optional import yaml @@ -23,17 +25,17 @@ def get_security_vulnerability_database_apis() -> ( - List[SecurityVulnerabilityDatabaseAPI] + list[SecurityVulnerabilityDatabaseAPI] ): """Return the APIs to use for fetching vulnerabilities.""" return [GithubSecurityAdvisoryAPI()] def fetch_vulnerabilities( - apis: Sequence[SecurityVulnerabilityDatabaseAPI], severities: Set[SeverityLevel] -) -> List[SecurityVulnerability]: + apis: Sequence[SecurityVulnerabilityDatabaseAPI], severities: set[SeverityLevel] +) -> list[SecurityVulnerability]: """Use apis to fetch and return vulnerabilities.""" - vulnerabilities: List[SecurityVulnerability] = [] + vulnerabilities: list[SecurityVulnerability] = [] for api in apis: LOGGER.debug("Fetching vulnerabilities from %s...", api.get_database_name()) vulnerabilities.extend(api.get_vulnerabilities(severities=severities)) @@ -41,8 +43,8 @@ def fetch_vulnerabilities( def filter_vulnerabilities( - config: Configuration, vulnerabilities: List[SecurityVulnerability] -) -> List[SecurityVulnerability]: + config: Configuration, vulnerabilities: list[SecurityVulnerability] +) -> list[SecurityVulnerability]: """Filter out vulnerabilities that should be ignored and return the rest.""" if config.ignore_ids: LOGGER.debug("Applying ignore-ids...") @@ -53,8 +55,8 @@ def filter_vulnerabilities( def sort_vulnerabilities( - vulnerabilities: List[SecurityVulnerability], -) -> List[SecurityVulnerability]: + vulnerabilities: list[SecurityVulnerability], +) -> list[SecurityVulnerability]: """Sort vulnerabilities into the order they should appear in the constraints.""" sorted_vulnerabilities = sorted(vulnerabilities, key=lambda v: v.identifier) sorted_vulnerabilities.sort(key=lambda v: v.package) @@ -69,7 +71,7 @@ def get_safe_version_constraints( See SecurityVulnerability documentation for more information. """ - safe_specs: List[str] = [] + safe_specs: list[str] = [] vulnerable_spec: str if "," in vulnerability.vulnerable_range: # If there is a known min and max affected version, make the constraints @@ -124,9 +126,9 @@ def create_header( """Create the comment header which goes at the top of the output.""" time_format: str = "%Y-%m-%dT%H:%M:%S.%fZ" # ISO with Z for UTC timestamp: str = datetime.now(tz=timezone.utc).strftime(time_format) - sources: List[str] = [api.get_database_name() for api in apis] + sources: list[str] = [api.get_database_name() for api in apis] app_name: str = "security-constraints" - lines: List[str] = [ + lines: list[str] = [ f"Generated by {app_name} {version(app_name)} on {timestamp}", f"Data sources: {', '.join(sources)}", f"Configuration: {config.to_dict()}", @@ -259,11 +261,11 @@ def main() -> int: yaml.safe_dump(config.to_dict(), stream=sys.stdout) return 0 - apis: List[ - SecurityVulnerabilityDatabaseAPI - ] = get_security_vulnerability_database_apis() + apis: list[SecurityVulnerabilityDatabaseAPI] = ( + get_security_vulnerability_database_apis() + ) - vulnerabilities: List[SecurityVulnerability] = fetch_vulnerabilities( + vulnerabilities: list[SecurityVulnerability] = fetch_vulnerabilities( apis, severities=config.min_severity.get_higher_or_equal_severities() ) vulnerabilities = filter_vulnerabilities(config, vulnerabilities) diff --git a/test/conftest.py b/test/conftest.py index 76b5327..1e2bdd0 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,5 +1,6 @@ import datetime -from typing import TYPE_CHECKING, Generator +from collections.abc import Generator +from typing import TYPE_CHECKING from unittest.mock import Mock import freezegun diff --git a/test/test_common.py b/test/test_common.py index dd1ea32..c129cf6 100644 --- a/test/test_common.py +++ b/test/test_common.py @@ -1,4 +1,4 @@ -from typing import Any, List, Set +from typing import Any from unittest.mock import Mock import pytest @@ -50,7 +50,7 @@ def test_severity_level_compare_with_other_type() -> None: ], ) def test_sort_severity_levels( - severities: List[SeverityLevel], expected: List[SeverityLevel] + severities: list[SeverityLevel], expected: list[SeverityLevel] ) -> None: assert sorted(severities) == expected @@ -76,7 +76,7 @@ def test_sort_severity_levels( ], ) def test_get_higher_or_equal_severities( - severity: SeverityLevel, expected: Set[SeverityLevel] + severity: SeverityLevel, expected: set[SeverityLevel] ) -> None: assert severity.get_higher_or_equal_severities() == expected @@ -225,7 +225,7 @@ def test_configuration_from_args() -> None: ], ) def test_configuration_merge( - configs: List[Configuration], expected: Configuration + configs: list[Configuration], expected: Configuration ) -> None: assert Configuration.merge(*configs) == expected @@ -244,7 +244,7 @@ def test_configuration_supported_keys() -> None: ], ) def test_package_constraints_str( - package: str, specifiers: List[str], expected: str + package: str, specifiers: list[str], expected: str ) -> None: assert str(PackageConstraints(package=package, specifiers=specifiers)) == expected diff --git a/test/test_github_security_advisory.py b/test/test_github_security_advisory.py index 0444151..bc15904 100644 --- a/test/test_github_security_advisory.py +++ b/test/test_github_security_advisory.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Any, Dict, List, Set +from typing import TYPE_CHECKING, Any import pytest @@ -37,7 +37,7 @@ def test_get_database_name(github_token: str) -> None: def test_get_vulnerabilities( github_token: str, requests_mock: "RequestsMock", - severities: Set[SeverityLevel], + severities: set[SeverityLevel], expected_graphql_severities: str, ) -> None: cursors = ( @@ -46,8 +46,8 @@ def test_get_vulnerabilities( "Y3Vyc29yOnYyOpK5MjAyMC0wOS0yNVQxOTo0MjowMCswMXowMM0DeQ==", "Y3Vyc29yOnYyOpK5MjAyMC0wOS0yNVQxOTo0MjowMCswMHowMM0LeQ==", ) - expected_vulnerabilities: List[SecurityVulnerability] = [] - vulnerability_nodes: List[Dict[str, Any]] = [] + expected_vulnerabilities: list[SecurityVulnerability] = [] + vulnerability_nodes: list[dict[str, Any]] = [] for request_index in range(3): for i in range(100 if request_index < 2 else 41): ghsa = f"GHSA-{request_index}-{i}" @@ -91,8 +91,9 @@ def test_get_vulnerabilities( "hasNextPage": request_index < 2, }, "nodes": vulnerability_nodes[ - request_index - * 100 : min(request_index * 100 + 100, 241) + request_index * 100 : min( + request_index * 100 + 100, 241 + ) ], } } diff --git a/test/test_main.py b/test/test_main.py index 03f9939..9c7e37a 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -1,7 +1,7 @@ import argparse import logging import sys -from typing import TYPE_CHECKING, List, Set, Type +from typing import TYPE_CHECKING from unittest.mock import Mock, call, create_autospec import pytest @@ -192,7 +192,7 @@ def test_setup_logging(monkeypatch: "MonkeyPatch", debug: bool) -> None: "severities", [{SeverityLevel.CRITICAL}, {SeverityLevel.CRITICAL, SeverityLevel.HIGH}], ) -def test_fetch_vulnerabilities(severities: Set[SeverityLevel]) -> None: +def test_fetch_vulnerabilities(severities: set[SeverityLevel]) -> None: mock_vulnerabilities = [create_autospec(SecurityVulnerability) for _ in range(3)] mock_apis = [ Mock( @@ -273,9 +273,9 @@ def test_fetch_vulnerabilities(severities: Set[SeverityLevel]) -> None: ], ) def test_filter_vulnerabilities( - vulnerabilities: List[SecurityVulnerability], + vulnerabilities: list[SecurityVulnerability], config: Configuration, - expected: List[SecurityVulnerability], + expected: list[SecurityVulnerability], ) -> None: assert ( filter_vulnerabilities(config=config, vulnerabilities=vulnerabilities) @@ -375,7 +375,7 @@ def test_filter_vulnerabilities( ids=["sort by package", "empty", "sub-sort by identifier"], ) def test_sort_vulnerabilities( - vulnerabilities: List[SecurityVulnerability], expected: List[SecurityVulnerability] + vulnerabilities: list[SecurityVulnerability], expected: list[SecurityVulnerability] ) -> None: original_vulnerabilities = vulnerabilities.copy() assert sort_vulnerabilities(vulnerabilities=vulnerabilities) == expected @@ -423,7 +423,7 @@ def test_sort_vulnerabilities( def test_create_header( monkeypatch: "MonkeyPatch", frozen_time: None, - db_names: List[str], + db_names: list[str], config: Configuration, expected: str, ) -> None: @@ -769,7 +769,7 @@ def test_main__exception( mock_get_apis: Mock, mock_filter_vulnerabilities: Mock, arg_namespace: ArgumentNamespace, - exception_type: Type[Exception], + exception_type: type[Exception], expected_exit_code: int, ) -> None: mock_stream = Mock(isatty=Mock(return_value=True)) diff --git a/tox.ini b/tox.ini index c0c2a14..e95f6bb 100644 --- a/tox.ini +++ b/tox.ini @@ -5,18 +5,18 @@ minversion = 4.0.0 [gh-actions] python = - 3.8: py38 3.9: py39 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313 [testenv] deps = -rrequirements-test.txt -rrequirements-lint.txt commands = - ruff check . - black --check src test + ruff check + ruff format pytest mypy