diff --git a/.dockerignore b/.dockerignore index a8ecf1f4b..ad8a40792 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,3 +2,8 @@ .github .venv .semgrep_rules +.vscode +htmlcov +logs +coverage.xml + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c0c25951..d79caedff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,11 +9,11 @@ on: jobs: build-pex: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: - python-version: ["3.6"] + python-version: ["3.9"] steps: - uses: actions/checkout@v3 @@ -50,14 +50,13 @@ jobs: with: name: Logprep path: logprep.pex - if: matrix.python-version == 3.6 test: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9"] + python-version: ["3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 @@ -69,7 +68,8 @@ jobs: - name: Install dependencies run: | - python -m pip install --upgrade pip + sudo apt-get update && sudo apt-get -y install libhyperscan-dev librdkafka-dev + pip install --upgrade pip wheel pip install -r requirements_dev.txt - name: Perform unit tests @@ -85,7 +85,7 @@ jobs: pytest -vv tests/acceptance build-docs: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: @@ -102,7 +102,8 @@ jobs: - name: Install dependencies run: | - python -m pip install --upgrade pip + sudo apt-get update && sudo apt-get -y install libhyperscan-dev librdkafka-dev + pip install --upgrade pip wheel pip install -r requirements_dev.txt pip install -r doc/requirements.txt @@ -113,7 +114,7 @@ jobs: make clean html code-quality: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: @@ -140,17 +141,12 @@ jobs: - name: Install dependencies run: | - python -m pip install --upgrade pip + sudo apt-get update && sudo apt-get -y install libhyperscan-dev librdkafka-dev + pip install --upgrade pip wheel pip install -r requirements_dev.txt - - name: Install semgrep if python version > 3.6 - run: | - pip install semgrep - if: matrix.python-version == 3.7 || matrix.python-version == 3.8 || matrix.python-version == 3.9 - - name: check black formating run: | - pip install black black --check --diff --config ./pyproject.toml . - name: lint changed and added files diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 78df4c2e8..b1bfc8bce 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: - python-version: [ '3.6', '3.7', '3.8', '3.9' ] + python-version: ["3.9"] steps: - uses: actions/checkout@v3 @@ -19,7 +19,7 @@ jobs: uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - cache: 'pip' + cache: "pip" - name: Install dependencies run: | @@ -48,14 +48,13 @@ jobs: with: name: Logprep path: logprep.pex - if: matrix.python-version == 3.6 test: runs-on: ubuntu-latest strategy: matrix: - python-version: [ '3.6', '3.7', '3.8', '3.9' ] + python-version: ["3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 @@ -63,11 +62,12 @@ jobs: uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - cache: 'pip' + cache: "pip" - name: Install dependencies run: | - python -m pip install --upgrade pip + sudo apt-get update && sudo apt-get -y install libhyperscan-dev librdkafka-dev + pip install --upgrade pip wheel pip install -r requirements_dev.txt - name: Perform unit tests @@ -87,7 +87,7 @@ jobs: strategy: matrix: - python-version: [ '3.6', '3.7', '3.8', '3.9' ] + python-version: ["3.9"] steps: - uses: actions/checkout@v3 @@ -99,7 +99,7 @@ jobs: uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - cache: 'pip' + cache: "pip" - name: Get changed python files id: changed-files @@ -110,17 +110,12 @@ jobs: - name: Install dependencies run: | - python -m pip install --upgrade pip + sudo apt-get update && sudo apt-get -y install libhyperscan-dev librdkafka-dev + pip install --upgrade pip wheel pip install -r requirements_dev.txt - - name: Install semgrep if python version > 3.6 - run: | - pip install semgrep - if: matrix.python-version == 3.7 || matrix.python-version == 3.8 || matrix.python-version == 3.9 - - name: check black formating run: | - pip install black black --check --diff --config ./pyproject.toml . - name: lint changed and added files @@ -135,5 +130,5 @@ jobs: uses: codecov/codecov-action@v2 - name: Check semgrep rules - if: steps.changed-files.outputs.all_changed_files && (matrix.python-version == 3.7 || matrix.python-version == 3.8 || matrix.python-version == 3.9) - run: semgrep -c .semgrep_rules -c r/python --error -l python --skip-unknown-extensions ${{ steps.changed-files.outputs.all_changed_files }} \ No newline at end of file + if: steps.changed-files.outputs.all_changed_files + run: semgrep -c .semgrep_rules -c r/python --error -l python --skip-unknown-extensions ${{ steps.changed-files.outputs.all_changed_files }} diff --git a/.github/workflows/publish-latest-dev-release-to-pypi.yml b/.github/workflows/publish-latest-dev-release-to-pypi.yml index 46b8d2877..398deb390 100644 --- a/.github/workflows/publish-latest-dev-release-to-pypi.yml +++ b/.github/workflows/publish-latest-dev-release-to-pypi.yml @@ -17,12 +17,11 @@ jobs: - name: Initialize Python uses: actions/setup-python@v1 with: - python-version: 3.6 + python-version: 3.9 - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install wheel + pip install --upgrade pip wheel - name: Build binary wheel and a source tarball run: python setup.py sdist bdist_wheel diff --git a/.github/workflows/publish-release-to-pypi.yml b/.github/workflows/publish-release-to-pypi.yml index a37bd2571..7afe482d0 100644 --- a/.github/workflows/publish-release-to-pypi.yml +++ b/.github/workflows/publish-release-to-pypi.yml @@ -16,13 +16,12 @@ jobs: - name: Initialize Python uses: actions/setup-python@v1 with: - python-version: 3.6 - cache: 'pip' + python-version: 3.9 + cache: "pip" - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install wheel + python -m pip install --upgrade pip wheel - name: Build binary wheel and a source tarball run: python setup.py sdist bdist_wheel @@ -32,4 +31,4 @@ jobs: uses: pypa/gh-action-pypi-publish@master with: user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index a3d36a8df..a94227a76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,19 @@ ## next release +### Breaking + +* drop support for python `3.6`, `3.7`, `3.8` + +### Features + * Add an `http input connector` that spawns a uvicorn server which parses requests content to events. * provide the possibility to consume lists, rules and configuration from files and http endpoints +### Improvements + +* add support for python `3.10` and `3.11` + ## v4.0.0 ### Breaking diff --git a/Dockerfile b/Dockerfile index cda888930..de3ef7391 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,18 @@ -FROM python:3.9-slim as build +FROM python:3.11-slim as build ADD . /logprep WORKDIR /logprep -RUN python -m pip install --upgrade pip && python -m pip install wheel +RUN apt-get update && apt-get -y install libhyperscan-dev librdkafka-dev +RUN python -m pip install --upgrade pip wheel RUN python setup.py sdist bdist_wheel -FROM python:3.9-slim as prod +FROM python:3.11-slim as prod RUN useradd -s /bin/sh -m -c "logprep user" logprep USER logprep ENV PATH=/home/logprep/.local/bin:$PATH COPY --from=build /logprep/dist/logprep-0+unknown-py3-none-any.whl / +RUN apt-get update && apt-get -y install libhyperscan-dev librdkafka-dev RUN pip install logprep-0+unknown-py3-none-any.whl ENV PROMETHEUS_MULTIPROC_DIR=/tmp/logprep/prometheus_multiproc/ ENV TLDEXTRACT_CACHE=/tmp/logprep/tld_extract_cache/ diff --git a/README.md b/README.md index 1bebaa268..a08dcd8d5 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,10 @@ Details about the rule language and how to write rules for the processors can be ### Installation -Python should be present on the system, currently supported are the versions 3.6 - 3.9. +Python should be present on the system, currently supported are the versions 3.9 - 3.11. +You have to install `hyperscan` and `rdkafka` before installing logprep. On debian based +systems, this is performed by `sudo apt-get update && sudo apt-get -y install libhyperscan-dev librdkafka-dev`. + To install Logprep you have following options: **1. Option:** Installation via PyPI: @@ -276,16 +279,16 @@ Those can be executed via: `tox -e [name of the test environment]`. For Example: ``` -tox -e py36-all +tox -e py39-all ``` This runs all tests, calculates the test coverage and evaluates the code quality for the python -3.6 version. +3.9 version. Multiple environments can be tested within one call: ``` -tox -e py36-all -e py37-all -e py38-all -e py39-all +tox -e py39-all -e py310-all -e py311-all ``` If you want to run them in parallel attach the option `-p`. @@ -301,15 +304,12 @@ tox -av In case the requirements change, the test environments must be rebuilt with the parameter `-r`: ``` -tox -e all -r +tox -e py39 -e py310 -e py311 -r ``` ### Semgrep -To run the semgrep rules against the semgrep python registry at least python 3.7 is required. -Because of that and the default logprep support for python 3.6 semgrep is not part of the -requirements_dev.txt. -If you want to run semgrep rules use a python environment with version higher than 3.6 and run +If you want to run semgrep rules run ``` pip install semgrep @@ -318,7 +318,7 @@ pip install semgrep Afterwards you can just call the tox environment with for example ``` -tox -e py37-semgrep +tox -e py39-semgrep ``` ### Running Logprep diff --git a/doc/source/getting_started.rst b/doc/source/getting_started.rst index fd0973e58..25c2eb810 100644 --- a/doc/source/getting_started.rst +++ b/doc/source/getting_started.rst @@ -5,7 +5,7 @@ Getting Started Installation ============ -Python should be present on the system, currently supported are the versions 3.6 - 3.9. +Python should be present on the system. Currently, Python 3.9 - 3.11 are supported. To install Logprep you have following options: **1. Option:** Installation via PyPI: diff --git a/logprep/abc/component.py b/logprep/abc/component.py index 28e8435eb..b0ea9d3ca 100644 --- a/logprep/abc/component.py +++ b/logprep/abc/component.py @@ -1,5 +1,4 @@ """ abstract module for connectors""" -import sys from abc import ABC from logging import Logger @@ -17,10 +16,8 @@ class Config: type: str = field(validator=validators.instance_of(str)) """Type of the component""" - __slots__ = ["name", "_logger", "_config"] - - if not sys.version_info.minor < 7: - __slots__.append("__dict__") + # __dict__ is added to support functools.cached_property + __slots__ = ["name", "_logger", "_config", "__dict__"] name: str _logger: Logger diff --git a/logprep/connector/confluent_kafka/input.py b/logprep/connector/confluent_kafka/input.py index 607765b6d..685e32150 100644 --- a/logprep/connector/confluent_kafka/input.py +++ b/logprep/connector/confluent_kafka/input.py @@ -27,8 +27,7 @@ offset_reset_policy: smallest """ import json -import sys -from functools import partial +from functools import cached_property, partial from logging import Logger from socket import getfqdn from typing import Any, List, Tuple, Union @@ -39,11 +38,6 @@ from logprep.abc.input import CriticalInputError, Input from logprep.util.validators import dict_with_keys_validator -if sys.version_info.minor < 8: # pragma: no cover - from backports.cached_property import cached_property # pylint: disable=import-error -else: - from functools import cached_property - class ConfluentKafkaInput(Input): """A kafka input connector.""" diff --git a/logprep/connector/confluent_kafka/output.py b/logprep/connector/confluent_kafka/output.py index f46c11539..fcfd3014e 100644 --- a/logprep/connector/confluent_kafka/output.py +++ b/logprep/connector/confluent_kafka/output.py @@ -27,23 +27,17 @@ """ import json -import sys from datetime import datetime -from functools import partial +from functools import cached_property, partial from socket import getfqdn from typing import List, Optional from attrs import define, field, validators from confluent_kafka import Producer -from logprep.abc.output import Output, CriticalOutputError +from logprep.abc.output import CriticalOutputError, Output from logprep.util.validators import dict_with_keys_validator -if sys.version_info.minor < 8: # pragma: no cover - from backports.cached_property import cached_property # pylint: disable=import-error -else: - from functools import cached_property - class ConfluentKafkaOutput(Output): """A kafka connector that serves as output connector.""" diff --git a/logprep/connector/elasticsearch/output.py b/logprep/connector/elasticsearch/output.py index 79793df9f..164c44fa0 100644 --- a/logprep/connector/elasticsearch/output.py +++ b/logprep/connector/elasticsearch/output.py @@ -33,7 +33,7 @@ import json import re import ssl -import sys +from functools import cached_property from logging import Logger from typing import List, Optional @@ -45,11 +45,6 @@ from logprep.abc.output import FatalOutputError, Output -if sys.version_info.minor < 8: # pragma: no cover - from backports.cached_property import cached_property # pylint: disable=import-error -else: - from functools import cached_property - class ElasticsearchOutput(Output): """An Elasticsearch output connector.""" diff --git a/logprep/connector/http/input.py b/logprep/connector/http/input.py index ee1312ee0..609af7cb0 100644 --- a/logprep/connector/http/input.py +++ b/logprep/connector/http/input.py @@ -5,8 +5,6 @@ A http input connector that spawns an uvicorn server and accepts http requests, parses them, puts them to an internal queue and pops them via :code:`get_next` method. -This Processor is not supported for python 3.6 and lower versions. - Example ^^^^^^^ .. code-block:: yaml diff --git a/logprep/connector/json/input.py b/logprep/connector/json/input.py index 0f9ce625d..3e606c14a 100644 --- a/logprep/connector/json/input.py +++ b/logprep/connector/json/input.py @@ -19,18 +19,14 @@ documents_path: path/to/a/document.json """ -import sys +from functools import cached_property from attrs import define + from logprep.abc.input import Input from logprep.connector.dummy.input import DummyInput from logprep.util.json_handling import parse_json -if sys.version_info.minor < 8: # pragma: no cover - from backports.cached_property import cached_property # pylint: disable=import-error -else: - from functools import cached_property - class JsonInput(DummyInput): """JsonInput Connector""" diff --git a/logprep/connector/jsonl/input.py b/logprep/connector/jsonl/input.py index 9ccae08c3..32ad459c0 100644 --- a/logprep/connector/jsonl/input.py +++ b/logprep/connector/jsonl/input.py @@ -19,15 +19,11 @@ documents_path: path/to/a/document.jsonl """ -import sys +from functools import cached_property + from logprep.connector.json.input import JsonInput from logprep.util.json_handling import parse_jsonl -if sys.version_info.minor < 8: # pragma: no cover - from backports.cached_property import cached_property # pylint: disable=import-error -else: - from functools import cached_property - class JsonlInput(JsonInput): """JsonlInput Connector""" diff --git a/logprep/connector/opensearch/output.py b/logprep/connector/opensearch/output.py index 445caac27..52dd3db43 100644 --- a/logprep/connector/opensearch/output.py +++ b/logprep/connector/opensearch/output.py @@ -31,18 +31,13 @@ """ import logging -import sys +from functools import cached_property import opensearchpy as opensearch from logprep.abc.output import FatalOutputError, Output from logprep.connector.elasticsearch.output import ElasticsearchOutput -if sys.version_info.minor < 8: # pragma: no cover - from backports.cached_property import cached_property # pylint: disable=import-error -else: - from functools import cached_property - logging.getLogger("opensearch").setLevel(logging.WARNING) diff --git a/logprep/processor/domain_label_extractor/processor.py b/logprep/processor/domain_label_extractor/processor.py index b57b211d1..952bfb518 100644 --- a/logprep/processor/domain_label_extractor/processor.py +++ b/logprep/processor/domain_label_extractor/processor.py @@ -26,25 +26,18 @@ tld_lists: /path/to/list/file tagging_field_name: resolved """ -import sys import ipaddress - +from functools import cached_property from typing import Optional -from attr import define, field, validators +from attr import define, field, validators from tldextract import TLDExtract - from logprep.abc import Processor from logprep.processor.base.exceptions import DuplicationError -from logprep.util.validators import list_of_urls_validator from logprep.processor.domain_label_extractor.rule import DomainLabelExtractorRule from logprep.util.helper import add_field_to, get_dotted_field_value - -if sys.version_info.minor < 8: # pragma: no cover - from backports.cached_property import cached_property # pylint: disable=import-error -else: - from functools import cached_property +from logprep.util.validators import list_of_urls_validator class DomainLabelExtractor(Processor): diff --git a/logprep/processor/domain_resolver/processor.py b/logprep/processor/domain_resolver/processor.py index 3453c7e22..7988fcac4 100644 --- a/logprep/processor/domain_resolver/processor.py +++ b/logprep/processor/domain_resolver/processor.py @@ -26,14 +26,14 @@ debug_cache: false """ import datetime -import sys import socket +from functools import cached_property from logging import Logger from multiprocessing import context from multiprocessing.pool import ThreadPool from typing import Optional -from attr import define, field, validators +from attr import define, field, validators from tldextract import TLDExtract from logprep.abc import Processor @@ -44,11 +44,6 @@ from logprep.util.helper import add_field_to, get_dotted_field_value from logprep.util.validators import list_of_urls_validator -if sys.version_info.minor < 8: # pragma: no cover - from backports.cached_property import cached_property # pylint: disable=import-error -else: - from functools import cached_property - class DomainResolver(Processor): """Resolve domains.""" diff --git a/logprep/processor/geoip_enricher/processor.py b/logprep/processor/geoip_enricher/processor.py index 8dcbb665e..73fd50e51 100644 --- a/logprep/processor/geoip_enricher/processor.py +++ b/logprep/processor/geoip_enricher/processor.py @@ -16,11 +16,11 @@ - tests/testdata/geoip_enricher/rules/ db_path: /path/to/GeoLite2-City.mmdb """ -import sys +from functools import cached_property from ipaddress import ip_address from typing import List -from attr import define, field +from attr import define, field from geoip2 import database from geoip2.errors import AddressNotFoundError @@ -29,11 +29,6 @@ from logprep.util.helper import add_field_to, get_dotted_field_value from logprep.util.validators import url_validator -if sys.version_info.minor < 8: # pragma: no cover - from backports.cached_property import cached_property # pylint: disable=import-error -else: - from functools import cached_property - class GeoipEnricherError(BaseException): """Base class for GeoipEnricher related exceptions.""" diff --git a/logprep/processor/pre_detector/processor.py b/logprep/processor/pre_detector/processor.py index d228359ff..eadf0bec5 100644 --- a/logprep/processor/pre_detector/processor.py +++ b/logprep/processor/pre_detector/processor.py @@ -20,21 +20,17 @@ pre_detector_topic: sre_topic alert_ip_list_path: /tmp/ip_list.yml """ -import sys +from functools import cached_property from logging import DEBUG, Logger from uuid import uuid4 from attr import define, field, validators + from logprep.abc import Processor from logprep.processor.pre_detector.ip_alerter import IPAlerter from logprep.processor.pre_detector.rule import PreDetectorRule from logprep.util.validators import file_validator -if sys.version_info.minor < 8: # pragma: no cover - from backports.cached_property import cached_property # pylint: disable=import-error -else: - from functools import cached_property - class PreDetectorError(BaseException): """Base class for PreDetector related exceptions.""" diff --git a/logprep/processor/pseudonymizer/processor.py b/logprep/processor/pseudonymizer/processor.py index a83ff6b52..121ef6281 100644 --- a/logprep/processor/pseudonymizer/processor.py +++ b/logprep/processor/pseudonymizer/processor.py @@ -29,7 +29,7 @@ """ import datetime import re -import sys +from functools import cached_property from logging import Logger from typing import Any, List, Optional, Tuple, Union from urllib.parse import parse_qs @@ -46,11 +46,6 @@ from logprep.util.hasher import SHA256Hasher from logprep.util.validators import file_validator, list_of_urls_validator -if sys.version_info.minor < 8: # pragma: no cover - from backports.cached_property import cached_property # pylint: disable=import-error -else: - from functools import cached_property - class Pseudonymizer(Processor): """Pseudonymize log events to conform to EU privacy laws.""" diff --git a/requirements.in b/requirements.in index 9b3a939c2..ddcef2e89 100644 --- a/requirements.in +++ b/requirements.in @@ -1,8 +1,10 @@ +wheel +attrs~=21.3 # because of semgrep in development luqum jsonref pyyaml ruamel.yaml -confluent-kafka==1.8.2 +confluent-kafka<1.9.0 pycryptodome urlextract tldextract @@ -11,12 +13,10 @@ pygrok colorama python-dateutil pytz -numpy +numpy==1.22.1 arrow prometheus_client -hyperscan==0.1.5; python_version >= '3.6' and python_version <= '3.8' and sys_platform == 'linux' and platform_machine == 'x86_64' -hyperscan>=0.2.0; python_version >= '3.9' and sys_platform == 'linux' and platform_machine == 'x86_64' -backports.cached_property ; python_version < "3.8" +hyperscan==0.2.0; sys_platform == 'linux' and platform_machine == 'x86_64' elasticsearch>=7,<8 opensearch-py mysql-connector-python diff --git a/requirements.txt b/requirements.txt index b3ed67f85..a2c1ece8f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,99 +1,104 @@ # -# This file is autogenerated by pip-compile with python 3.6 +# This file is autogenerated by pip-compile with python 3.9 # To update, run: # -# pip-compile requirements.in +# pip-compile ./requirements.in # -aiohttp==3.8.1 +aiohttp==3.8.3 # via geoip2 -aiosignal==1.2.0 +aiosignal==1.3.1 # via aiohttp -arrow==1.2.2 - # via -r requirements.in +anyio==3.6.2 + # via starlette +arrow==1.2.3 + # via -r ./requirements.in async-timeout==4.0.2 # via aiohttp -asynctest==0.13.0 - # via aiohttp attrs==21.4.0 - # via aiohttp -backports.cached-property==1.0.1 ; python_version < "3.8" - # via -r requirements.in -certifi==2021.10.8 + # via + # -r ./requirements.in + # aiohttp +certifi==2022.9.24 # via # elasticsearch # opensearch-py # requests -charset-normalizer==2.0.11 +charset-normalizer==2.1.1 # via # aiohttp # requests -colorama==0.4.4 - # via -r requirements.in +click==8.1.3 + # via uvicorn +colorama==0.4.6 + # via -r ./requirements.in confluent-kafka==1.8.2 - # via -r requirements.in -elasticsearch==7.17.0 - # via -r requirements.in -filelock==3.4.1 + # via -r ./requirements.in +elasticsearch==7.17.7 + # via -r ./requirements.in +fastapi==0.88.0 + # via -r ./requirements.in +filelock==3.8.0 # via # tldextract # urlextract -frozenlist==1.2.0 +frozenlist==1.3.3 # via # aiohttp # aiosignal -geoip2==4.5.0 - # via -r requirements.in -hyperscan==0.1.5 ; python_version >= "3.6" and python_version <= "3.8" and sys_platform == "linux" and platform_machine == "x86_64" -hyperscan>=0.2.0; python_version >= '3.9' and sys_platform == 'linux' and platform_machine == 'x86_64' - # via -r requirements.in -idna==3.3 +geoip2==4.6.0 + # via -r ./requirements.in +h11==0.14.0 + # via uvicorn +hyperscan==0.2.0 ; sys_platform == "linux" and platform_machine == "x86_64" + # via -r ./requirements.in +idna==3.4 # via - # idna-ssl + # anyio # requests # tldextract # urlextract # yarl -idna-ssl==1.1.0 - # via aiohttp -jsonref==0.2 - # via -r requirements.in -luqum==0.11.0 - # via -r requirements.in +jsonref==1.0.1 + # via -r ./requirements.in +luqum==0.12.0 + # via -r ./requirements.in maxminddb==2.2.0 # via geoip2 -multidict==5.2.0 +multidict==6.0.2 # via # aiohttp # yarl -mysql-connector-python==8.0.28 - # via -r requirements.in -numpy==1.19.5 - # via -r requirements.in +mysql-connector-python==8.0.31 + # via -r ./requirements.in +numpy==1.22.1 + # via -r ./requirements.in opensearch-py==2.0.0 - # via -r requirements.in -platformdirs==2.4.0 + # via -r ./requirements.in +platformdirs==2.5.4 # via urlextract ply==3.11 # via luqum -prometheus-client==0.13.1 - # via -r requirements.in -protobuf==3.19.4 +prometheus-client==0.15.0 + # via -r ./requirements.in +protobuf==3.20.1 # via mysql-connector-python -pycryptodome==3.14.0 - # via -r requirements.in +pycryptodome==3.16.0 + # via -r ./requirements.in +pydantic==1.10.2 + # via fastapi pygrok==1.0.0 - # via -r requirements.in + # via -r ./requirements.in python-dateutil==2.8.2 # via - # -r requirements.in + # -r ./requirements.in # arrow -pytz==2021.3 - # via -r requirements.in +pytz==2022.6 + # via -r ./requirements.in pyyaml==6.0 - # via -r requirements.in -regex==2022.1.18 + # via -r ./requirements.in +regex==2022.10.31 # via pygrok -requests==2.27.1 +requests==2.28.1 # via # geoip2 # opensearch-py @@ -101,35 +106,37 @@ requests==2.27.1 # tldextract requests-file==1.5.1 # via tldextract -ruamel.yaml==0.17.20 - # via -r requirements.in -ruamel.yaml.clib==0.2.6 - # via ruamel.yaml +ruamel-yaml==0.17.21 + # via -r ./requirements.in +ruamel-yaml-clib==0.2.7 + # via ruamel-yaml six==1.16.0 # via # python-dateutil # requests-file -tldextract==3.1.2 - # via -r requirements.in -typing==3.7.4.3 - # via backports.cached-property -typing-extensions==4.0.1 +sniffio==1.3.0 + # via anyio +starlette==0.22.0 + # via fastapi +tldextract==3.4.0 + # via -r ./requirements.in +typing-extensions==4.4.0 # via - # aiohttp - # arrow - # async-timeout - # yarl -uritools==3.0.2 + # pydantic + # starlette +uritools==4.0.0 # via urlextract -urlextract==1.5.0 - # via -r requirements.in -urllib3==1.26.8 +urlextract==1.7.1 + # via -r ./requirements.in +urllib3==1.26.13 # via # elasticsearch # geoip2 # opensearch-py # requests -yarl==1.7.2 +uvicorn==0.20.0 + # via -r ./requirements.in +wheel==0.38.4 + # via -r ./requirements.in +yarl==1.8.1 # via aiohttp -fastapi==0.83.0 -uvicorn==0.17.0 diff --git a/requirements_dev.in b/requirements_dev.in index 6eb788f17..4878474cd 100644 --- a/requirements_dev.in +++ b/requirements_dev.in @@ -2,3 +2,7 @@ pytest pytest-cov pylint +httpx +black +semgrep +isort diff --git a/requirements_dev.txt b/requirements_dev.txt index b7d85af6b..1f362b8f0 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,228 +1,302 @@ # -# This file is autogenerated by pip-compile with python 3.6 +# This file is autogenerated by pip-compile with python 3.9 # To update, run: # -# pip-compile requirements_dev.in +# pip-compile ./requirements_dev.in # -aiohttp==3.8.1 +aiohttp==3.8.3 # via - # -r requirements.txt + # -r ./requirements.txt # geoip2 -aiosignal==1.2.0 +aiosignal==1.3.1 # via - # -r requirements.txt + # -r ./requirements.txt # aiohttp -arrow==1.2.2 - # via -r requirements.txt -astroid==2.9.3 +anyio==3.6.2 + # via + # -r ./requirements.txt + # httpcore + # starlette +arrow==1.2.3 + # via -r ./requirements.txt +astroid==2.12.13 # via pylint async-timeout==4.0.2 # via - # -r requirements.txt - # aiohttp -asynctest==0.13.0 - # via - # -r requirements.txt + # -r ./requirements.txt # aiohttp attrs==21.4.0 # via - # -r requirements.txt + # -r ./requirements.txt # aiohttp + # glom + # jsonschema # pytest -backports.cached-property==1.0.1 ; python_version < "3.8" - # via -r requirements.txt -certifi==2021.10.8 + # semgrep +black==22.10.0 + # via -r ./requirements_dev.in +boltons==21.0.0 + # via + # face + # glom + # semgrep +bracex==2.3.post1 + # via wcmatch +certifi==2022.9.24 # via - # -r requirements.txt + # -r ./requirements.txt # elasticsearch + # httpcore + # httpx # opensearch-py # requests -charset-normalizer==2.0.11 +charset-normalizer==2.1.1 # via - # -r requirements.txt + # -r ./requirements.txt # aiohttp # requests -colorama==0.4.4 - # via -r requirements.txt +click==8.1.3 + # via + # -r ./requirements.txt + # black + # click-option-group + # semgrep + # uvicorn +click-option-group==0.5.5 + # via semgrep +colorama==0.4.6 + # via + # -r ./requirements.txt + # semgrep confluent-kafka==1.8.2 - # via -r requirements.txt -coverage[toml]==6.2 + # via -r ./requirements.txt +coverage[toml]==6.5.0 # via pytest-cov -elasticsearch==7.17.0 - # via -r requirements.txt -filelock==3.4.1 +defusedxml==0.7.1 + # via semgrep +dill==0.3.6 + # via pylint +elasticsearch==7.17.7 + # via -r ./requirements.txt +exceptiongroup==1.0.4 + # via pytest +face==22.0.0 + # via glom +fastapi==0.88.0 + # via -r ./requirements.txt +filelock==3.8.0 # via - # -r requirements.txt + # -r ./requirements.txt # tldextract # urlextract -frozenlist==1.2.0 +frozenlist==1.3.3 # via - # -r requirements.txt + # -r ./requirements.txt # aiohttp # aiosignal -geoip2==4.5.0 - # via -r requirements.txt -hyperscan==0.1.5 ; python_version >= "3.6" and python_version <= "3.8" and sys_platform == "linux" and platform_machine == "x86_64" -hyperscan>=0.2.0; python_version >= '3.9' and sys_platform == 'linux' and platform_machine == 'x86_64' - # via -r requirements.txt -idna==3.3 - # via - # -r requirements.txt - # idna-ssl +geoip2==4.6.0 + # via -r ./requirements.txt +glom==22.1.0 + # via semgrep +h11==0.14.0 + # via + # -r ./requirements.txt + # httpcore + # uvicorn +httpcore==0.16.2 + # via httpx +httpx==0.23.1 + # via -r ./requirements_dev.in +hyperscan==0.2.0 ; sys_platform == "linux" and platform_machine == "x86_64" + # via -r ./requirements.txt +idna==3.4 + # via + # -r ./requirements.txt + # anyio # requests + # rfc3986 # tldextract # urlextract # yarl -idna-ssl==1.1.0 - # via - # -r requirements.txt - # aiohttp -importlib-metadata==4.8.3 - # via - # pluggy - # pytest iniconfig==1.1.1 # via pytest isort==5.10.1 - # via pylint -jsonref==0.2 - # via -r requirements.txt -lazy-object-proxy==1.7.1 + # via + # -r ./requirements_dev.in + # pylint +jsonref==1.0.1 + # via -r ./requirements.txt +jsonschema==4.17.1 + # via semgrep +lazy-object-proxy==1.8.0 # via astroid -luqum==0.11.0 - # via -r requirements.txt +luqum==0.12.0 + # via -r ./requirements.txt maxminddb==2.2.0 # via - # -r requirements.txt + # -r ./requirements.txt # geoip2 -mccabe==0.6.1 +mccabe==0.7.0 # via pylint -multidict==5.2.0 +multidict==6.0.2 # via - # -r requirements.txt + # -r ./requirements.txt # aiohttp # yarl -mysql-connector-python==8.0.28 - # via -r requirements.txt -numpy==1.19.5 - # via -r requirements.txt +mypy-extensions==0.4.3 + # via black +mysql-connector-python==8.0.31 + # via -r ./requirements.txt +numpy==1.22.1 + # via -r ./requirements.txt opensearch-py==2.0.0 - # via -r requirements.txt + # via -r ./requirements.txt packaging==21.3 - # via pytest -platformdirs==2.4.0 # via - # -r requirements.txt + # pytest + # semgrep +pathspec==0.10.2 + # via black +peewee==3.15.4 + # via semgrep +platformdirs==2.5.4 + # via + # -r ./requirements.txt + # black # pylint # urlextract pluggy==1.0.0 # via pytest ply==3.11 # via - # -r requirements.txt + # -r ./requirements.txt # luqum -prometheus-client==0.13.1 - # via -r requirements.txt -protobuf==3.19.4 +prometheus-client==0.15.0 + # via -r ./requirements.txt +protobuf==3.20.1 # via - # -r requirements.txt + # -r ./requirements.txt # mysql-connector-python -py==1.11.0 - # via pytest -pycryptodome==3.14.0 - # via -r requirements.txt +pycryptodome==3.16.0 + # via -r ./requirements.txt +pydantic==1.10.2 + # via + # -r ./requirements.txt + # fastapi pygrok==1.0.0 - # via -r requirements.txt -pylint==2.12.2 - # via -r requirements_dev.in -pyparsing==3.0.7 + # via -r ./requirements.txt +pylint==2.15.6 + # via -r ./requirements_dev.in +pyparsing==3.0.9 # via packaging -pytest==6.2.5 +pyrsistent==0.19.2 + # via jsonschema +pytest==7.2.0 # via - # -r requirements_dev.in + # -r ./requirements_dev.in # pytest-cov -pytest-cov==3.0.0 - # via -r requirements_dev.in +pytest-cov==4.0.0 + # via -r ./requirements_dev.in python-dateutil==2.8.2 # via - # -r requirements.txt + # -r ./requirements.txt # arrow -pytz==2021.3 - # via -r requirements.txt +python-lsp-jsonrpc==1.0.0 + # via semgrep +pytz==2022.6 + # via -r ./requirements.txt pyyaml==6.0 - # via -r requirements.txt -regex==2022.1.18 + # via -r ./requirements.txt +regex==2022.10.31 # via - # -r requirements.txt + # -r ./requirements.txt # pygrok -requests==2.27.1 +requests==2.28.1 # via - # -r requirements.txt + # -r ./requirements.txt # geoip2 # opensearch-py # requests-file + # semgrep # tldextract requests-file==1.5.1 # via - # -r requirements.txt + # -r ./requirements.txt # tldextract -ruamel.yaml==0.17.20 - # via -r requirements.txt -ruamel.yaml.clib==0.2.6 +rfc3986[idna2008]==1.5.0 + # via httpx +ruamel-yaml==0.17.21 # via - # -r requirements.txt - # ruamel.yaml + # -r ./requirements.txt + # semgrep +ruamel-yaml-clib==0.2.7 + # via + # -r ./requirements.txt + # ruamel-yaml +semgrep==0.122.0 + # via -r ./requirements_dev.in six==1.16.0 # via - # -r requirements.txt + # -r ./requirements.txt # python-dateutil # requests-file -tldextract==3.1.2 - # via -r requirements.txt -toml==0.10.2 +sniffio==1.3.0 + # via + # -r ./requirements.txt + # anyio + # httpcore + # httpx +starlette==0.22.0 # via + # -r ./requirements.txt + # fastapi +tldextract==3.4.0 + # via -r ./requirements.txt +tomli==2.0.1 + # via + # black + # coverage # pylint # pytest -tomli==1.2.3 - # via coverage -typed-ast==1.5.4 - # via astroid -typing==3.7.4.3 - # via - # -r requirements.txt - # backports.cached-property -typing-extensions==4.0.1 + # semgrep +tomlkit==0.11.6 + # via pylint +tqdm==4.64.1 + # via semgrep +typing-extensions==4.4.0 # via - # -r requirements.txt - # aiohttp - # arrow + # -r ./requirements.txt # astroid - # async-timeout - # importlib-metadata + # black + # pydantic # pylint - # yarl -uritools==3.0.2 + # semgrep + # starlette +ujson==5.5.0 + # via python-lsp-jsonrpc +uritools==4.0.0 # via - # -r requirements.txt + # -r ./requirements.txt # urlextract -urlextract==1.5.0 - # via -r requirements.txt -urllib3==1.26.8 +urlextract==1.7.1 + # via -r ./requirements.txt +urllib3==1.26.13 # via - # -r requirements.txt + # -r ./requirements.txt # elasticsearch # geoip2 # opensearch-py # requests -wrapt==1.13.3 + # semgrep +uvicorn==0.20.0 + # via -r ./requirements.txt +wcmatch==8.4.1 + # via semgrep +wheel==0.38.4 + # via -r ./requirements.txt +wrapt==1.14.1 # via astroid -yarl==1.7.2 +yarl==1.8.1 # via - # -r requirements.txt + # -r ./requirements.txt # aiohttp -zipp==3.6.0 - # via importlib-metadata -fastapi==0.83.0 -uvicorn==0.17.0 -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/setup.py b/setup.py index 4975b37a3..7f544bccf 100644 --- a/setup.py +++ b/setup.py @@ -25,10 +25,9 @@ classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ], project_urls={ "Homepage": "https://github.com/fkie-cad/Logprep", @@ -36,7 +35,7 @@ }, packages=find_packages(), install_requires=["setuptools"] + requirements, - python_requires=">=3.6", + python_requires=">=3.9", entry_points={ "console_scripts": [ "logprep = logprep.run_logprep:main", diff --git a/tests/acceptance/test_http_input.py b/tests/acceptance/test_http_input.py index ff03bdfef..6319e13c2 100644 --- a/tests/acceptance/test_http_input.py +++ b/tests/acceptance/test_http_input.py @@ -84,7 +84,6 @@ def teardown_function(): pass -@pytest.mark.skipif(sys.version_info.minor < 7, reason="not supported for python 3.6") def test_http_input_accepts_message_for_single_pipeline(tmp_path, config): output_path = tmp_path / "output.jsonl" config["output"] = {"testoutput": {"type": "jsonl_output", "output_file": str(output_path)}} @@ -97,7 +96,6 @@ def test_http_input_accepts_message_for_single_pipeline(tmp_path, config): assert "my message" in output_path.read_text() -@pytest.mark.skipif(sys.version_info.minor < 7, reason="not supported for python 3.6") def test_http_input_accepts_message_for_two_pipelines(tmp_path, config): config["process_count"] = 2 output_path = tmp_path / "output.jsonl" @@ -118,7 +116,6 @@ def test_http_input_accepts_message_for_two_pipelines(tmp_path, config): assert "my second message" in output_content -@pytest.mark.skipif(sys.version_info.minor < 7, reason="not supported for python 3.6") def test_http_input_accepts_message_for_three_pipelines(tmp_path, config): config["process_count"] = 3 output_path = tmp_path / "output.jsonl" diff --git a/tests/unit/connector/test_http_input.py b/tests/unit/connector/test_http_input.py index dc2bcb3b8..fb4a38b76 100644 --- a/tests/unit/connector/test_http_input.py +++ b/tests/unit/connector/test_http_input.py @@ -1,11 +1,9 @@ # pylint: disable=missing-docstring # pylint: disable=protected-access # pylint: disable=attribute-defined-outside-init -import base64 from copy import deepcopy import json import sys -import zlib import pytest import requests @@ -17,7 +15,6 @@ from tests.unit.connector.base import BaseInputTestCase -@pytest.mark.skipif(sys.version_info.minor < 7, reason="not supported for python 3.6") class TestHttpConnector(BaseInputTestCase): def setup_method(self): super().setup_method() @@ -41,19 +38,19 @@ def test_has_fastapi_app(self): def test_json_endpoint_accepts_post_request(self): data = {"message": "my log message"} - resp = self.client.post("/json", json.dumps(data)) + resp = self.client.post(url="/json", data=json.dumps(data)) assert resp.status_code == 200 def test_json_message_is_put_in_queue(self): data = {"message": "my log message"} - resp = self.client.post("/json", json.dumps(data)) + resp = self.client.post(url="/json", data=json.dumps(data)) assert resp.status_code == 200 event_from_queue = self.object._messages.get(timeout=0.001) assert event_from_queue == data def test_plaintext_endpoint_accepts_post_request(self): data = "my log message" - resp = self.client.post("/plaintext", data) + resp = self.client.post(url="/plaintext", data=data) assert resp.status_code == 200 def test_plaintext_message_is_put_in_queue(self): @@ -81,7 +78,7 @@ def test_jsonl_messages_are_put_in_queue(self): def test_get_next_returns_message_from_queue(self): data = {"message": "my log message"} - self.client.post("/json", json.dumps(data)) + self.client.post(url="/json", data=json.dumps(data)) assert self.object.get_next(0.001) == (data, None) def test_get_next_returns_first_in_first_out(self): @@ -91,7 +88,7 @@ def test_get_next_returns_first_in_first_out(self): {"message": "third message"}, ] for message in data: - self.client.post("/json", json.dumps(message)) + self.client.post(url="/json", data=json.dumps(message)) assert self.object.get_next(0.001) == (data[0], None) assert self.object.get_next(0.001) == (data[1], None) assert self.object.get_next(0.001) == (data[2], None) @@ -105,7 +102,7 @@ def test_get_next_returns_first_in_first_out_for_mixed_endpoints(self): for message in data: endpoint, post_data = message.values() if endpoint == "json": - self.client.post("/json", json.dumps(post_data)) + self.client.post(url="/json", data=json.dumps(post_data)) if endpoint == "plaintext": self.client.post("/plaintext", data=post_data) assert self.object.get_next(0.001)[0] == data[0].get("data") diff --git a/tests/unit/processor/pseudonymizer/test_encrypter.py b/tests/unit/processor/pseudonymizer/test_encrypter.py index ce0f4a71a..2ad4e12f2 100644 --- a/tests/unit/processor/pseudonymizer/test_encrypter.py +++ b/tests/unit/processor/pseudonymizer/test_encrypter.py @@ -38,7 +38,7 @@ def test_encrypt_without_loaded_keys(self): with pytest.raises(ValueError): encrypter.encrypt("foo") - @mock.patch("io.open") + @mock.patch("pathlib.Path.open") def test_load_public_keys_successfully(self, mock_open): mock_open.side_effect = [ mock.mock_open(read_data=MOCK_PUBKEY_1024).return_value, @@ -49,7 +49,7 @@ def test_load_public_keys_successfully(self, mock_open): assert len(str(encrypter._pubkey_analyst.n)) == 309 assert len(str(encrypter._pubkey_depseudo.n)) == 617 - @mock.patch("io.open") + @mock.patch("pathlib.Path.open") def test_load_public_keys_invalid_key(self, mock_open): mock_open.side_effect = [ mock.mock_open(read_data="foo").return_value, @@ -64,7 +64,7 @@ def test_load_public_keys_invalid_path(self): with pytest.raises(FileNotFoundError): encrypter.load_public_keys("non_existent_file_1", "non_existent_file_1") - @mock.patch("io.open") + @mock.patch("pathlib.Path.open") def test_encrypt(self, mock_open): mock_open.side_effect = [ mock.mock_open(read_data=MOCK_PUBKEY_1024).return_value, diff --git a/tests/unit/processor/test_processor_rule.py b/tests/unit/processor/test_processor_rule.py index 081ce4732..ebbb4389e 100644 --- a/tests/unit/processor/test_processor_rule.py +++ b/tests/unit/processor/test_processor_rule.py @@ -41,7 +41,7 @@ class TestRule: ) def test_create_rules_from_file(self, file_data, raises): mock_open = mock.mock_open(read_data=file_data) - with mock.patch("io.open", mock_open): + with mock.patch("pathlib.Path.open", mock_open): if raises: error, message = raises with pytest.raises(error, match=message): diff --git a/tests/unit/util/test_configuration.py b/tests/unit/util/test_configuration.py index 22ebde588..18655e16f 100644 --- a/tests/unit/util/test_configuration.py +++ b/tests/unit/util/test_configuration.py @@ -3,6 +3,7 @@ # pylint: disable=line-too-long from copy import deepcopy from logging import getLogger +import re import pytest @@ -307,7 +308,7 @@ def test_verify_metrics_config( [ ( InvalidProcessorConfigurationError, - "Invalid processor config: labelername - __init__() missing 1 required keyword-only argument: 'generic_rules'", + "missing 1 required keyword-only argument: 'generic_rules'", ) ], ), @@ -353,7 +354,11 @@ def test_verify_error(self, config_dict, raised_errors, test_case): with pytest.raises(InvalidConfigurationErrors) as e_info: config.verify(logger) errors_set = [(type(err), str(err)) for err in e_info.value.errors] - assert errors_set == raised_errors, f"For test case '{test_case}'!" + assert len(raised_errors) == len(errors_set) + zipped_errors = zip(raised_errors, errors_set) + for expected_error, raised_error in zipped_errors: + assert expected_error[0] == raised_error[0], "error class differ" + assert re.search(expected_error[1], raised_error[1]), "error message differ" @pytest.mark.parametrize( "test_case, config_dict, raised_errors", @@ -398,7 +403,7 @@ def test_verify_error(self, config_dict, raised_errors, test_case): [ ( InvalidProcessorConfigurationError, - "Invalid processor config: labelername - __init__() missing 1 required keyword-only argument: 'generic_rules'", + "missing 1 required keyword-only argument: 'generic_rules'", ) ], ), @@ -501,7 +506,11 @@ def test_verify_errors_get_collected(self, config_dict, raised_errors, test_case for error in errors: collected_errors += error.errors errors_set = [(type(error), str(error)) for error in collected_errors] - assert errors_set == raised_errors, f"For test case '{test_case}'!" + assert len(raised_errors) == len(errors_set) + zipped_errors = zip(raised_errors, errors_set) + for expected_error, raised_error in zipped_errors: + assert expected_error[0] == raised_error[0], "error class differ" + assert re.search(expected_error[1], raised_error[1]), "error message differ" else: config._verify_metrics_config() diff --git a/tests/unit/util/test_getter.py b/tests/unit/util/test_getter.py index aee77ae01..97abd8924 100644 --- a/tests/unit/util/test_getter.py +++ b/tests/unit/util/test_getter.py @@ -61,7 +61,7 @@ def test_factory_returns_file_getter_with_protocol(self): def test_get_returns_content(self): file_getter = GetterFactory.from_string("/my/file") - with mock.patch("io.open", mock.mock_open(read_data="my content")): + with mock.patch("pathlib.Path.open", mock.mock_open(read_data="my content")): content = file_getter.get() assert content == "my content" @@ -151,7 +151,7 @@ def test_get_returns_content(self): def test_parses_content(self, method_name, input_content, expected_output): file_getter = GetterFactory.from_string("/my/file") method = getattr(file_getter, method_name) - with mock.patch("io.open", mock.mock_open(read_data=input_content)): + with mock.patch("pathlib.Path.open", mock.mock_open(read_data=input_content)): output = method() assert output == expected_output diff --git a/tox.ini b/tox.ini index 00291485e..75e3b5ae1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,12 @@ [tox] -envlist = py{36,37,38,39} -minversion = 3.6 +envlist = py{39,310,311} +minversion = 3.9 [testenv] deps = -rrequirements_dev.txt -[testenv:py{36,37,38,39}-unit] +[testenv:py{39,310,311}-unit] description = Run unit-tests usedevelop = True deps = {[testenv]deps} @@ -14,7 +14,7 @@ commands = pytest -vv tests/unit {posargs} -[testenv:py{36,37,38,39}-acceptance] +[testenv:py{39,310,311}-acceptance] description = Run acceptance-tests usedevelop = True deps = @@ -24,7 +24,7 @@ commands = pytest -vv -s tests/acceptance {posargs} -[testenv:py{36,37,38,39}-lint] +[testenv:py{39,310,311}-lint] description = Run pylint to determine code-quality usedevelop = True deps = pylint @@ -42,7 +42,7 @@ commands = semgrep -c .semgrep_rules -c r/python --error --emacs -[testenv:py{36,37,38,39}-docs] +[testenv:py{39,310,311}-docs] description = Build sphinx HTML documentation changedir = doc usedevelop = True @@ -58,7 +58,7 @@ commands = make clean html -[testenv:py{36,37,38,39}-all] +[testenv:py{39,310,311}-all] description = Run all tests with coverage and lint usedevelop = True deps =