├── debian ├── compat ├── changelog ├── prerm ├── control ├── postrm ├── postinst └── rules ├── test ├── tests │ ├── __init__.py │ ├── pkg │ │ ├── __init__.py │ │ ├── test_build_rpm.py │ │ ├── test_build_deb.py │ │ └── test_scripts_nonroot.py │ ├── bouncer │ │ ├── __init__.py │ │ ├── test_yaml_local.py │ │ ├── test_blocklist_mirror.py │ │ └── test_tls.py │ ├── install │ │ ├── __init__.py │ │ ├── no_crowdsec │ │ │ ├── __init__.py │ │ │ ├── test_no_crowdsec_deb.py │ │ │ └── test_no_crowdsec_scripts.py │ │ └── with_crowdsec │ │ │ ├── __init__.py │ │ │ ├── test_crowdsec_scripts.py │ │ │ └── test_crowdsec_deb.py │ └── conftest.py ├── .python-version ├── default.env ├── pytest.ini ├── pyproject.toml └── uv.lock ├── .github ├── release-drafter.yml ├── workflows │ ├── publish-docker-doc.yaml │ ├── release-drafter.yml │ ├── build-binary-package.yml │ ├── lint.yml │ ├── tests_deb.yml │ ├── tests.yml │ └── release_publish_docker-image.yml └── release.py ├── main.go ├── .dockerignore ├── config ├── crowdsec-blocklist-mirror.service └── crowdsec-blocklist-mirror.yaml ├── scripts ├── upgrade.sh ├── uninstall.sh ├── install.sh └── _bouncer.sh ├── Dockerfile ├── .gitignore ├── README.md ├── LICENSE ├── pkg ├── formatters │ ├── mikrotik │ │ ├── mikrotik.tmpl │ │ └── mikrotik.go │ └── formatters.go ├── cfg │ ├── logging.go │ └── config.go ├── registry │ └── registry.go └── server │ ├── server.go │ └── logging.go ├── rpm └── SPECS │ └── crowdsec-blocklist-mirror.spec ├── go.mod ├── Makefile ├── cmd └── root.go ├── .golangci.yml ├── docker └── README.md └── go.sum /debian/compat: -------------------------------------------------------------------------------- 1 | 11 2 | -------------------------------------------------------------------------------- /test/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/tests/pkg/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/.python-version: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /test/tests/bouncer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/tests/install/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/tests/install/no_crowdsec/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/tests/install/with_crowdsec/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## What’s Changed 3 | 4 | $CHANGES 5 | -------------------------------------------------------------------------------- /test/default.env: -------------------------------------------------------------------------------- 1 | CROWDSEC_TEST_VERSION="dev" 2 | CROWDSEC_TEST_FLAVORS="full" 3 | CROWDSEC_TEST_NETWORK="net-test" 4 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | crowdsec-blocklist-mirror (1.0.0) UNRELEASED; urgency=medium 2 | 3 | * Initial debian packaging 4 | 5 | -- Shivam Sandbhor Fri, 29 Apr 2022 10:50:14 +0530 6 | -------------------------------------------------------------------------------- /debian/prerm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | BOUNCER="crowdsec-blocklist-mirror" 6 | 7 | systemctl stop "$BOUNCER" || echo "cannot stop service" 8 | systemctl disable "$BOUNCER" || echo "cannot disable service" 9 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | 6 | "github.com/crowdsecurity/cs-blocklist-mirror/cmd" 7 | ) 8 | 9 | func main() { 10 | err := cmd.Execute() 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/tests/pkg/test_build_rpm.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytestmark = pytest.mark.rpm 4 | 5 | 6 | def test_rpm_build(rpm_package, skip_unless_rpm): 7 | """Test that the package can be built.""" 8 | assert rpm_package.exists(), f'Package {rpm_package} not found' 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # We include .git in the build context because excluding it would break the 2 | # "make release" target, which uses git to retrieve the build version and tag. 3 | #.git 4 | 5 | crowdsec-blocklist-mirror 6 | crowdsec-blocklist-mirror-* 7 | crowdsec-blocklist-mirror.tgz 8 | docs/ 9 | debian/ 10 | rpm/ 11 | test/ 12 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: crowdsec-blocklist-mirror 2 | Maintainer: Crowdsec Team 3 | Build-Depends: debhelper 4 | Section: admin 5 | Priority: optional 6 | 7 | Package: crowdsec-blocklist-mirror 8 | Provides: crowdsec-blocklist-mirror 9 | Depends: gettext-base 10 | Description: Blocklist mirror of Crowdsec 11 | Architecture: any 12 | -------------------------------------------------------------------------------- /test/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = 3 | --pdbcls=IPython.terminal.debugger:Pdb 4 | --ignore=tests/install 5 | --strict-markers 6 | markers: 7 | deb: mark tests related to deb packaging 8 | rpm: mark tests related to rpm packaging 9 | systemd_debug: dump systemd status and journal on test failure 10 | env_files = 11 | .env 12 | default.env 13 | -------------------------------------------------------------------------------- /debian/postrm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | BOUNCER="crowdsec-blocklist-mirror" 6 | CONFIG="/etc/crowdsec/bouncers/$BOUNCER.yaml" 7 | 8 | if [ "$1" = "purge" ]; then 9 | if [ -f "$CONFIG.id" ]; then 10 | bouncer_id=$(cat "$CONFIG.id") 11 | cscli -oraw bouncers delete "$bouncer_id" 2>/dev/null || true 12 | rm -f "$CONFIG.id" 13 | fi 14 | fi 15 | -------------------------------------------------------------------------------- /config/crowdsec-blocklist-mirror.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=CrowdSec Blocklist Mirror 3 | After=syslog.target crowdsec.service 4 | 5 | [Service] 6 | Type=simple 7 | ExecStart=${BIN} -c ${CFG}/crowdsec-blocklist-mirror.yaml 8 | ExecStartPre=${BIN} -c ${CFG}/crowdsec-blocklist-mirror.yaml -t 9 | Restart=always 10 | RestartSec=10 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /test/tests/pkg/test_build_deb.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytestmark = pytest.mark.deb 4 | 5 | 6 | # This test has the side effect of building the package and leaving it in the 7 | # project's parent directory. 8 | def test_deb_build(deb_package, skip_unless_deb): 9 | """Test that the package can be built.""" 10 | assert deb_package.exists(), f'Package {deb_package} not found' 11 | -------------------------------------------------------------------------------- /scripts/upgrade.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | . ./scripts/_bouncer.sh 6 | 7 | assert_root 8 | 9 | # --------------------------------- # 10 | 11 | systemctl stop "$SERVICE" 12 | 13 | if ! upgrade_bin; then 14 | msg err "failed to upgrade $BOUNCER" 15 | exit 1 16 | fi 17 | 18 | systemctl start "$SERVICE" || msg warn "$SERVICE failed to start, please check the systemd logs" 19 | 20 | msg succ "$BOUNCER upgraded successfully." 21 | exit 0 22 | -------------------------------------------------------------------------------- /scripts/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | . ./scripts/_bouncer.sh 6 | 7 | assert_root 8 | 9 | # --------------------------------- # 10 | 11 | uninstall() { 12 | systemctl stop "$SERVICE" || true 13 | delete_bouncer 14 | rm -f "$CONFIG" 15 | rm -f "$SYSTEMD_PATH_FILE" 16 | rm -f "$BIN_PATH_INSTALLED" 17 | rm -f "/var/log/$BOUNCER.log" 18 | } 19 | 20 | uninstall 21 | msg succ "$BOUNCER has been successfully uninstalled" 22 | exit 0 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GOVERSION=1.25 2 | 3 | FROM golang:${GOVERSION}-alpine AS build 4 | 5 | WORKDIR /go/src/cs-blocklist-mirror 6 | 7 | RUN apk add --update --no-cache make git 8 | COPY . . 9 | 10 | RUN make build DOCKER_BUILD=1 11 | 12 | FROM alpine:latest 13 | COPY --from=build /go/src/cs-blocklist-mirror/crowdsec-blocklist-mirror /usr/local/bin/crowdsec-blocklist-mirror 14 | COPY --from=build /go/src/cs-blocklist-mirror/config/crowdsec-blocklist-mirror.yaml /etc/crowdsec/bouncers/crowdsec-blocklist-mirror.yaml 15 | 16 | ENTRYPOINT ["/usr/local/bin/crowdsec-blocklist-mirror", "-c", "/etc/crowdsec/bouncers/crowdsec-blocklist-mirror.yaml"] 17 | -------------------------------------------------------------------------------- /test/tests/pkg/test_scripts_nonroot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | 5 | def test_scripts_nonroot(project_repo, bouncer_binary, must_be_nonroot): 6 | assert os.geteuid() != 0, "This test must be run as non-root" 7 | 8 | for script in ['install.sh', 'upgrade.sh', 'uninstall.sh']: 9 | c = subprocess.run( 10 | ['/usr/bin/sh', f'scripts/{script}'], 11 | stdout=subprocess.PIPE, 12 | stderr=subprocess.PIPE, 13 | cwd=project_repo, 14 | encoding='utf-8', 15 | ) 16 | 17 | assert c.returncode == 1 18 | assert c.stdout == '' 19 | assert 'This script must be run as root' in c.stderr 20 | -------------------------------------------------------------------------------- /test/tests/bouncer/test_yaml_local.py: -------------------------------------------------------------------------------- 1 | 2 | def test_yaml_local(bouncer, bm_cfg_factory): 3 | cfg = bm_cfg_factory() 4 | 5 | with bouncer(cfg) as bm: 6 | bm.wait_for_lines_fnmatch([ 7 | "*one of lapi_key or cert_path is required*", 8 | ]) 9 | bm.proc.wait(timeout=0.2) 10 | assert not bm.proc.is_running() 11 | 12 | config_local = { 13 | 'crowdsec_config': { 14 | 'lapi_key': 'not-used', 15 | } 16 | } 17 | 18 | with bouncer(cfg, config_local=config_local) as bm: 19 | bm.wait_for_lines_fnmatch([ 20 | "*lapi_url is required*", 21 | ]) 22 | bm.proc.wait(timeout=0.2) 23 | assert not bm.proc.is_running() 24 | 25 | -------------------------------------------------------------------------------- /debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | systemctl daemon-reload 4 | 5 | #shellcheck source=./scripts/_bouncer.sh 6 | . "/usr/lib/$DPKG_MAINTSCRIPT_PACKAGE/_bouncer.sh" 7 | START=1 8 | 9 | if [ "$1" = "configure" ]; then 10 | if need_api_key; then 11 | if ! set_api_key; then 12 | START=0 13 | fi 14 | fi 15 | fi 16 | 17 | systemctl --quiet is-enabled "$SERVICE" || systemctl unmask "$SERVICE" && systemctl enable "$SERVICE" 18 | 19 | set_local_lapi_url 'CROWDSEC_LAPI_URL' 20 | 21 | if [ "$START" -eq 0 ]; then 22 | echo "no api key was generated, you can generate one on your LAPI server by running 'cscli bouncers add ' and add it to '$CONFIG'" >&2 23 | else 24 | systemctl start "$SERVICE" 25 | fi 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Test binary, built with `go test -c` 2 | *.test 3 | 4 | # Output of the go coverage tool, specifically when used with LiteIDE 5 | *.out 6 | 7 | # Dependencies are not vendored by default, but a tarball is created by "make vendor" 8 | # and provided in the release. Used by freebsd, gentoo, etc. 9 | vendor/ 10 | vendor.tgz 11 | 12 | # Python 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | venv/ 17 | 18 | # built by make 19 | /crowdsec-blocklist-mirror 20 | /crowdsec-blocklist-mirror-* 21 | /crowdsec-blocklist-mirror.tgz 22 | 23 | # built by dpkg-buildpackage 24 | /debian/crowdsec-blocklist-mirror 25 | /debian/files 26 | /debian/*.substvars 27 | /debian/*.debhelper 28 | /debian/*-stamp 29 | 30 | # built by rpmbuild 31 | /rpm/BUILD 32 | /rpm/BUILDROOT 33 | /rpm/RPMS 34 | /rpm/SOURCES/*.tar.gz 35 | /rpm/SRPMS 36 | -------------------------------------------------------------------------------- /.github/workflows/publish-docker-doc.yaml: -------------------------------------------------------------------------------- 1 | name: Update Docker Hub README 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'docker/README.md' 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | update-docker-hub-readme: 15 | name: Update the README on Docker Hub 16 | runs-on: ubuntu-latest 17 | steps: 18 | - 19 | name: Check out the repo 20 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 21 | - 22 | name: Update docker hub README 23 | uses: ms-jpq/sync-dockerhub-readme@e2991ea1ba48832e73555cdbd5b82f5a2e91ee9b # v1 24 | with: 25 | username: ${{ secrets.DOCKER_USERNAME }} 26 | password: ${{ secrets.DOCKER_PASSWORD }} 27 | repository: crowdsecurity/blocklist-mirror 28 | readme: "./docker/README.md" 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | CrowdSec-Http-blocklist-mirror-logo 3 |

4 |

5 | 6 | 7 |

8 |

9 | 💠 Hub 10 | 💬 Discourse 11 |

12 | 13 | # CrowdSec Blocklist Mirror 14 | 15 | This bouncer exposes CrowdSec's active decisions via provided HTTP endpoints in pre-defined formats. It can be used by network appliances which support consumption of blocklists via HTTP. 16 | 17 | # Documentation 18 | 19 | Please follow the [official documentation](https://docs.crowdsec.net/docs/bouncers/blocklist-mirror). 20 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - main 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | update_release_draft: 14 | permissions: 15 | # write permission is required to create a github release 16 | contents: write 17 | # write permission is required for autolabeler 18 | # otherwise, read permission is required at least 19 | pull-requests: read 20 | runs-on: ubuntu-latest 21 | name: Update the release draft 22 | steps: 23 | # Drafts your next Release notes as Pull Requests are merged into "main" 24 | - uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0 25 | with: 26 | config-name: release-drafter.yml 27 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 28 | # config-name: my-config.yml 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Crowdsec 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/formatters/mikrotik/mikrotik.tmpl: -------------------------------------------------------------------------------- 1 | {{ if not $.IPv6Only }} 2 | /ip/firewall/address-list/remove [ find where list="{{$.ListName}}" ]; 3 | :global {{$.NameOfMikrotikFunction}}; 4 | :set {{$.NameOfMikrotikFunction}} do={ 5 | :do { /ip/firewall/address-list/add list={{$.ListName}} address=$1 comment="$2" timeout=$3; } on-error={ } 6 | } 7 | {{ end -}} 8 | {{ if not $.IPv4Only }} 9 | /ipv6/firewall/address-list/remove [ find where list="{{$.ListName}}" ]; 10 | :global {{$.NameOfMikrotikFunction}}v6; 11 | :set {{$.NameOfMikrotikFunction}}v6 do={ 12 | :do { /ipv6/firewall/address-list/add list={{$.ListName}} address=$1 comment="$2" timeout=$3; } on-error={ } 13 | } 14 | {{ end -}} 15 | 16 | {{- range .Decisions -}} 17 | {{ $ipv6Check := contains .Value ":" }} 18 | {{- if not $ipv6Check -}} 19 | ${{$.NameOfMikrotikFunction}} "{{.Value}}" "{{.Scenario}}" "{{.Duration}}" 20 | {{- else -}} 21 | ${{$.NameOfMikrotikFunction}}v6 "{{.Value}}" "{{.Scenario}}" "{{.Duration}}" 22 | {{- end }} 23 | {{ end -}} 24 | 25 | {{ if not $.IPv6Only }} 26 | :set {{$.NameOfMikrotikFunction}}; 27 | {{- end -}} 28 | {{ if not $.IPv4Only }} 29 | :set {{$.NameOfMikrotikFunction}}v6; 30 | {{- end -}} 31 | 32 | -------------------------------------------------------------------------------- /config/crowdsec-blocklist-mirror.yaml: -------------------------------------------------------------------------------- 1 | config_version: v1.0 2 | crowdsec_config: 3 | lapi_key: ${API_KEY} 4 | lapi_url: ${CROWDSEC_LAPI_URL} 5 | update_frequency: 10s 6 | include_scenarios_containing: [] 7 | exclude_scenarios_containing: [] 8 | only_include_decisions_from: [] 9 | insecure_skip_verify: false 10 | supported_decisions_types: 11 | - ban 12 | 13 | blocklists: 14 | - format: plain_text # Supported formats are either "plain_text" or "mikrotik" 15 | endpoint: /security/blocklist 16 | authentication: 17 | type: none # Supported types are either "none", "ip_based" or "basic" 18 | user: 19 | password: 20 | trusted_ips: # IP ranges, or IPs that don't require auth to access this blocklist 21 | - 127.0.0.1 22 | - ::1 23 | 24 | listen_uri: 127.0.0.1:41412 25 | tls: 26 | cert_file: 27 | key_file: 28 | 29 | metrics: 30 | enabled: true 31 | endpoint: /metrics 32 | 33 | # logging configuration 34 | log_media: file 35 | log_dir: /var/log/ 36 | log_level: info 37 | log_max_size: 40 38 | log_max_age: 30 39 | log_max_backups: 3 40 | compress_logs: true 41 | # enable access log of the HTTP server 42 | enable_access_logs: true 43 | -------------------------------------------------------------------------------- /.github/workflows/build-binary-package.yml: -------------------------------------------------------------------------------- 1 | name: build-binary-package 2 | 3 | on: 4 | release: 5 | types: 6 | - prereleased 7 | 8 | permissions: 9 | # Use write for: hub release edit 10 | contents: write 11 | 12 | env: 13 | PROGRAM_NAME: crowdsec-blocklist-mirror 14 | 15 | jobs: 16 | build: 17 | name: Build and upload all platforms 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | 22 | - name: Check out repository 23 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: Set up Go 28 | uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 29 | with: 30 | go-version-file: go.mod 31 | 32 | - name: Build all platforms 33 | run: | 34 | # build platform-all first so the .xz vendor file is not removed 35 | make platform-all vendor 36 | 37 | - name: Upload to release 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | run: | 41 | tag_name="${GITHUB_REF##*/}" 42 | # this will upload the $PROGRAM_NAME-vendor.tar.xz file as well 43 | gh release upload "$tag_name" $PROGRAM_NAME* vendor.tgz 44 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | export DEB_VERSION=$(shell dpkg-parsechangelog | grep -E '^Version:' | cut -f 2 -d ' ') 4 | export BUILD_VERSION=v${DEB_VERSION}-debian-pragmatic 5 | 6 | %: 7 | dh $@ 8 | 9 | override_dh_systemd_start: 10 | echo "Not running dh_systemd_start" 11 | override_dh_auto_clean: 12 | override_dh_auto_test: 13 | override_dh_auto_build: 14 | override_dh_auto_install: 15 | @make build 16 | 17 | @BOUNCER=crowdsec-blocklist-mirror; \ 18 | PKG="$$BOUNCER"; \ 19 | mkdir -p "debian/$$PKG/var/lib/crowdsec/$$BOUNCER/cache/"; \ 20 | install -D "$$BOUNCER" -t "debian/$$PKG/usr/bin/"; \ 21 | install -D "scripts/_bouncer.sh" -t "debian/$$PKG/usr/lib/$$PKG/"; \ 22 | install -D "config/$$BOUNCER.yaml" "debian/$$PKG/etc/crowdsec/bouncers/$$BOUNCER.yaml"; \ 23 | mkdir -p "debian/$$PKG/etc/systemd/system"; \ 24 | BIN="/usr/bin/$$BOUNCER" CFG="/etc/crowdsec/bouncers" envsubst '$$BIN $$CFG' < "config/$$BOUNCER.service" > "debian/$$PKG/etc/systemd/system/$$BOUNCER.service" 25 | 26 | execute_after_dh_fixperms: 27 | @BOUNCER=crowdsec-blocklist-mirror; \ 28 | PKG="$$BOUNCER"; \ 29 | chmod 0755 "debian/$$PKG/usr/bin/$$BOUNCER"; \ 30 | chmod 0600 "debian/$$PKG/usr/lib/$$PKG/_bouncer.sh"; \ 31 | chmod 0600 "debian/$$PKG/etc/crowdsec/bouncers/$$BOUNCER.yaml"; \ 32 | chmod 0644 "debian/$$PKG/etc/systemd/system/$$BOUNCER.service" 33 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Static Analysis 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | 10 | jobs: 11 | build: 12 | name: "golangci-lint + codeql" 13 | runs-on: ubuntu-latest 14 | 15 | permissions: 16 | security-events: write 17 | 18 | steps: 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 26 | with: 27 | go-version-file: go.mod 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8 31 | with: 32 | languages: go, python 33 | 34 | - name: Build 35 | run: | 36 | make build 37 | 38 | - name: golangci-lint 39 | uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 40 | with: 41 | version: v2.5 42 | args: --issues-exit-code=1 --timeout 10m 43 | only-new-issues: false 44 | 45 | - name: Perform CodeQL Analysis 46 | uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8 47 | -------------------------------------------------------------------------------- /test/tests/bouncer/test_blocklist_mirror.py: -------------------------------------------------------------------------------- 1 | 2 | def test_no_api_key(crowdsec, bouncer, bm_cfg_factory): 3 | cfg = bm_cfg_factory() 4 | with bouncer(cfg) as bm: 5 | bm.wait_for_lines_fnmatch([ 6 | "*one of lapi_key or cert_path is required*", 7 | ]) 8 | bm.proc.wait(timeout=0.2) 9 | assert not bm.proc.is_running() 10 | 11 | cfg['crowdsec_config']['lapi_key'] = '' 12 | 13 | with bouncer(cfg) as bm: 14 | bm.wait_for_lines_fnmatch([ 15 | "*one of lapi_key or cert_path is required*", 16 | ]) 17 | bm.proc.wait(timeout=0.2) 18 | assert not bm.proc.is_running() 19 | 20 | 21 | def test_no_lapi_url(bouncer, bm_cfg_factory): 22 | cfg = bm_cfg_factory() 23 | 24 | cfg['crowdsec_config']['lapi_key'] = 'not-used' 25 | 26 | with bouncer(cfg) as bm: 27 | bm.wait_for_lines_fnmatch([ 28 | "*lapi_url is required*", 29 | ]) 30 | bm.proc.wait(timeout=0.2) 31 | assert not bm.proc.is_running() 32 | 33 | cfg['crowdsec_config']['lapi_url'] = '' 34 | 35 | with bouncer(cfg) as bm: 36 | bm.wait_for_lines_fnmatch([ 37 | "*lapi_url is required*", 38 | ]) 39 | bm.proc.wait(timeout=0.2) 40 | assert not bm.proc.is_running() 41 | 42 | 43 | def test_no_lapi(bouncer, bm_cfg_factory): 44 | cfg = bm_cfg_factory() 45 | cfg['crowdsec_config']['lapi_key'] = 'not-used' 46 | cfg['crowdsec_config']['lapi_url'] = 'http://localhost:8237' 47 | 48 | with bouncer(cfg) as bm: 49 | bm.wait_for_lines_fnmatch([ 50 | "*connection refused*", 51 | "*terminating bouncer process*", 52 | "*process terminated with error*", 53 | ]) 54 | bm.proc.wait(timeout=0.2) 55 | assert not bm.proc.is_running() 56 | -------------------------------------------------------------------------------- /test/tests/install/no_crowdsec/test_no_crowdsec_deb.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | import pytest 5 | 6 | pytestmark = pytest.mark.deb 7 | 8 | 9 | def test_deb_install_purge(deb_package_path, bouncer_under_test, must_be_root): 10 | # test the full install-purge cycle, doing that in separate tests would 11 | # be a bit too much 12 | 13 | # TODO: remove and reinstall 14 | 15 | # use the package built as non-root by test_deb_build() 16 | assert deb_package_path.exists(), f'This test requires {deb_package_path}' 17 | 18 | bouncer_exe = f"/usr/bin/{bouncer_under_test}" 19 | assert not os.path.exists(bouncer_exe) 20 | 21 | config = f"/etc/crowdsec/bouncers/{bouncer_under_test}.yaml" 22 | assert not os.path.exists(config) 23 | 24 | # install the package 25 | p = subprocess.run( 26 | ['dpkg', '--install', deb_package_path.as_posix()], 27 | stdout=subprocess.PIPE, 28 | stderr=subprocess.PIPE, 29 | encoding='utf-8' 30 | ) 31 | assert p.returncode == 0, f'Failed to install {deb_package_path}' 32 | 33 | assert os.path.exists(bouncer_exe) 34 | assert os.stat(bouncer_exe).st_mode & 0o777 == 0o755 35 | 36 | assert os.path.exists(config) 37 | assert os.stat(config).st_mode & 0o777 == 0o600 38 | 39 | p = subprocess.check_output( 40 | ['dpkg-deb', '-f', deb_package_path.as_posix(), 'Package'], 41 | encoding='utf-8' 42 | ) 43 | package_name = p.strip() 44 | 45 | p = subprocess.run( 46 | ['dpkg', '--purge', package_name], 47 | stdout=subprocess.PIPE, 48 | stderr=subprocess.PIPE, 49 | encoding='utf-8' 50 | ) 51 | assert p.returncode == 0, f'Failed to purge {package_name}' 52 | 53 | assert not os.path.exists(bouncer_exe) 54 | assert not os.path.exists(config) 55 | -------------------------------------------------------------------------------- /.github/workflows/tests_deb.yml: -------------------------------------------------------------------------------- 1 | name: Test .deb packaging 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | name: "Test .deb packages" 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 26 | with: 27 | go-version-file: go.mod 28 | 29 | - name: Install uv 30 | uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 31 | with: 32 | version: 0.5.24 33 | enable-cache: true 34 | cache-dependency-glob: "test/uv.lock" 35 | 36 | - name: "Set up Python" 37 | uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 38 | with: 39 | python-version-file: "test/.python-version" 40 | 41 | - name: Install the project 42 | run: uv sync --all-extras --dev 43 | working-directory: ./test 44 | 45 | - name: Install functional test dependencies 46 | run: | 47 | sudo apt update 48 | sudo apt install -y build-essential debhelper devscripts fakeroot lintian 49 | docker network create net-test 50 | 51 | - name: Run functional tests 52 | env: 53 | CROWDSEC_TEST_VERSION: dev 54 | CROWDSEC_TEST_FLAVORS: full 55 | CROWDSEC_TEST_NETWORK: net-test 56 | CROWDSEC_TEST_TIMEOUT: 60 57 | PYTEST_ADDOPTS: --durations=0 -vv --color=yes 58 | working-directory: ./test 59 | run: | 60 | uv run pytest ./tests/pkg/test_build_deb.py 61 | sudo -E $(which uv) run pytest -m deb ./tests/install/no_crowdsec 62 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | . ./scripts/_bouncer.sh 6 | 7 | assert_root 8 | 9 | # --------------------------------- # 10 | 11 | API_KEY="" 12 | 13 | gen_apikey() { 14 | if command -v cscli >/dev/null; then 15 | msg succ "cscli found, generating bouncer api key." 16 | bouncer_id="$BOUNCER_PREFIX-$(date +%s)" 17 | API_KEY=$(cscli -oraw bouncers add "$bouncer_id") 18 | echo "$bouncer_id" > "$CONFIG.id" 19 | msg info "API Key: $API_KEY" 20 | READY="yes" 21 | else 22 | msg warn "cscli not found, you will need to generate an api key." 23 | READY="no" 24 | fi 25 | } 26 | 27 | gen_config_file() { 28 | # shellcheck disable=SC2016 29 | (umask 177 && API_KEY="$API_KEY" envsubst '$API_KEY' <"./config/$CONFIG_FILE" > "$CONFIG") 30 | } 31 | 32 | install_bouncer() { 33 | if [ ! -f "$BIN_PATH" ]; then 34 | msg err "$BIN_PATH not found, exiting." 35 | exit 1 36 | fi 37 | if [ -e "$BIN_PATH_INSTALLED" ]; then 38 | msg err "$BIN_PATH_INSTALLED is already installed. Exiting" 39 | exit 1 40 | fi 41 | msg info "Installing $BOUNCER" 42 | install -v -m 0755 -D "$BIN_PATH" "$BIN_PATH_INSTALLED" 43 | mkdir -p "$(dirname "$CONFIG")" 44 | # shellcheck disable=SC2016 45 | CFG=${CONFIG_DIR} BIN=${BIN_PATH_INSTALLED} envsubst '$CFG $BIN' <"./config/$SERVICE" >"$SYSTEMD_PATH_FILE" 46 | systemctl daemon-reload 47 | gen_apikey 48 | gen_config_file 49 | set_local_lapi_url 'CROWDSEC_LAPI_URL' 50 | } 51 | 52 | # --------------------------------- # 53 | 54 | install_bouncer 55 | 56 | systemctl enable "$SERVICE" 57 | if [ "$READY" = "yes" ]; then 58 | systemctl start "$SERVICE" 59 | else 60 | msg warn "service not started. You need to get an API key and configure it in $CONFIG" 61 | fi 62 | 63 | msg succ "The $BOUNCER service has been installed." 64 | exit 0 65 | -------------------------------------------------------------------------------- /pkg/formatters/mikrotik/mikrotik.go: -------------------------------------------------------------------------------- 1 | package mikrotik 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "net/http" 7 | "strings" 8 | "text/template" 9 | 10 | "github.com/crowdsecurity/crowdsec/pkg/models" 11 | "github.com/crowdsecurity/cs-blocklist-mirror/pkg/registry" 12 | ) 13 | 14 | type CustomMikrotikData struct { 15 | ListName string 16 | Decisions []*models.Decision 17 | NameOfMikrotikFunction string 18 | IPv6Only bool 19 | IPv4Only bool 20 | } 21 | 22 | //go:embed mikrotik.tmpl 23 | var MikrotikScriptTemplate string 24 | 25 | func Format(w http.ResponseWriter, r *http.Request) { 26 | 27 | // Extract decisions from the context 28 | decisions := r.Context().Value(registry.GlobalDecisionRegistry.Key).([]*models.Decision) 29 | 30 | // Get query parameters 31 | query := r.URL.Query() 32 | 33 | // check if ipv6only or ipv4only is set 34 | ipv6only := query.Has("ipv6only") 35 | ipv4only := query.Has("ipv4only") 36 | 37 | listName := query.Get("listname") 38 | if listName == "" { 39 | listName = "CrowdSec" 40 | } 41 | 42 | data := CustomMikrotikData{ 43 | ListName: listName, 44 | Decisions: decisions, 45 | NameOfMikrotikFunction: "CrowdSecAddIP", 46 | IPv6Only: ipv6only, 47 | IPv4Only: ipv4only, 48 | } 49 | 50 | // Parse the template 51 | parsedTemplate, err := template.New("script").Funcs(template.FuncMap{ 52 | "contains": strings.Contains, 53 | }).Parse(MikrotikScriptTemplate) 54 | if err != nil { 55 | http.Error(w, "Error parsing template: "+err.Error(), http.StatusInternalServerError) 56 | return 57 | } 58 | 59 | var buf = new(bytes.Buffer) 60 | // Execute the template 61 | err = parsedTemplate.Execute(buf, data) 62 | if err != nil { 63 | http.Error(w, "Error executing template "+err.Error(), http.StatusInternalServerError) 64 | return 65 | } 66 | w.Write(buf.Bytes()) 67 | } 68 | -------------------------------------------------------------------------------- /pkg/formatters/formatters.go: -------------------------------------------------------------------------------- 1 | package formatters 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/crowdsecurity/crowdsec/pkg/models" 9 | 10 | "github.com/crowdsecurity/cs-blocklist-mirror/pkg/formatters/mikrotik" 11 | "github.com/crowdsecurity/cs-blocklist-mirror/pkg/registry" 12 | ) 13 | 14 | var ByName = map[string]func(w http.ResponseWriter, r *http.Request){ 15 | "plain_text": PlainText, 16 | "mikrotik": mikrotik.Format, 17 | "f5": F5, 18 | "juniper": Juniper, 19 | } 20 | 21 | func PlainText(w http.ResponseWriter, r *http.Request) { 22 | decisions := r.Context().Value(registry.GlobalDecisionRegistry.Key).([]*models.Decision) 23 | for _, decision := range decisions { 24 | fmt.Fprintf(w, "%s\n", *decision.Value) 25 | } 26 | } 27 | 28 | func F5(w http.ResponseWriter, r *http.Request) { 29 | decisions := r.Context().Value(registry.GlobalDecisionRegistry.Key).([]*models.Decision) 30 | for _, decision := range decisions { 31 | category := *decision.Scenario 32 | if strings.Contains(*decision.Scenario, "/") { 33 | category = strings.Split(*decision.Scenario, "/")[1] 34 | } 35 | 36 | switch strings.ToLower(*decision.Scope) { 37 | case "ip": 38 | mask := 32 39 | if strings.Contains(*decision.Value, ":") { 40 | mask = 64 41 | } 42 | 43 | fmt.Fprintf(w, 44 | "%s,%d,bl,%s\n", 45 | *decision.Value, 46 | mask, 47 | category, 48 | ) 49 | case "range": 50 | sep := strings.Split(*decision.Value, "/") 51 | fmt.Fprintf(w, 52 | "%s,%s,bl,%s\n", 53 | sep[0], 54 | sep[1], 55 | category, 56 | ) 57 | default: 58 | } 59 | } 60 | } 61 | 62 | func Juniper(w http.ResponseWriter, r *http.Request) { 63 | decisions := r.Context().Value(registry.GlobalDecisionRegistry.Key).([]*models.Decision) 64 | for _, decision := range decisions { 65 | switch strings.ToLower(*decision.Scope) { 66 | case "ip": 67 | mask := "/32" 68 | if strings.Contains(*decision.Value, ":") { 69 | mask = "/128" 70 | } 71 | fmt.Fprintf(w, "%s%s\n", *decision.Value, mask) 72 | case "range": 73 | fmt.Fprintf(w, "%s\n", *decision.Value) 74 | default: 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | 3 | import pytest 4 | 5 | # from pytest_cs import plugin 6 | 7 | # pytest_exception_interact = plugin.pytest_exception_interact 8 | 9 | 10 | # provide the name of the bouncer binary to test 11 | @pytest.fixture(scope='session') 12 | def bouncer_under_test(): 13 | return 'crowdsec-blocklist-mirror' 14 | 15 | 16 | # Create a lapi container, register a bouncer and run it with the updated config. 17 | # - Return context manager that yields a tuple of (bouncer, lapi) 18 | @pytest.fixture(scope='session') 19 | def bouncer_with_lapi(bouncer, crowdsec, bm_cfg_factory, api_key_factory, tmp_path_factory, bouncer_binary): 20 | @contextlib.contextmanager 21 | def closure(config_lapi=None, config_bouncer=None, api_key=None): 22 | if config_bouncer is None: 23 | config_bouncer = {} 24 | if config_lapi is None: 25 | config_lapi = {} 26 | # can be overridden by config_lapi + config_bouncer 27 | api_key = api_key_factory() 28 | env = { 29 | 'BOUNCER_KEY_custom': api_key, 30 | } 31 | try: 32 | env.update(config_lapi) 33 | with crowdsec(environment=env) as lapi: 34 | lapi.wait_for_http(8080, '/health') 35 | port = lapi.probe.get_bound_port('8080') 36 | cfg = bm_cfg_factory() 37 | cfg.setdefault('crowdsec_config', {}) 38 | cfg['crowdsec_config']['lapi_url'] = f'http://localhost:{port}/' 39 | cfg['crowdsec_config']['lapi_key'] = api_key 40 | cfg.update(config_bouncer) 41 | with bouncer(cfg) as cb: 42 | yield cb, lapi 43 | finally: 44 | pass 45 | 46 | yield closure 47 | 48 | 49 | _default_config = { 50 | 'update_frequency': '0.1s', 51 | 'log_mode': 'stdout', 52 | 'log_level': 'info', 53 | 'prometheus': { 54 | 'enabled': False, 55 | } 56 | } 57 | 58 | 59 | @pytest.fixture(scope='session') 60 | def bm_cfg_factory(): 61 | def closure(**kw): 62 | cfg = _default_config.copy() 63 | cfg.setdefault('crowdsec_config', {}) 64 | cfg |= kw 65 | return cfg | kw 66 | yield closure 67 | -------------------------------------------------------------------------------- /test/tests/install/no_crowdsec/test_no_crowdsec_scripts.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pexpect 3 | import yaml 4 | 5 | import pytest 6 | 7 | BOUNCER = "crowdsec-blocklist-mirror" 8 | CONFIG = f"/etc/crowdsec/bouncers/{BOUNCER}.yaml" 9 | 10 | 11 | @pytest.mark.dependency 12 | def test_install_no_crowdsec(project_repo, bouncer_binary, must_be_root): 13 | c = pexpect.spawn( 14 | '/usr/bin/sh', ['scripts/install.sh'], 15 | cwd=project_repo 16 | ) 17 | 18 | c.expect(f"Installing {BOUNCER}") 19 | c.expect("WARN.* cscli not found, you will need to generate an api key.") 20 | c.expect(f"WARN.* service not started. You need to get an API key and configure it in {CONFIG}") 21 | c.expect(f"The {BOUNCER} service has been installed.") 22 | c.wait() 23 | assert c.terminated 24 | assert c.exitstatus == 0 25 | 26 | with open(CONFIG) as f: 27 | y = yaml.safe_load(f) 28 | assert y['crowdsec_config']['lapi_key'] == '' 29 | assert y['crowdsec_config']['lapi_url'] == '${CROWDSEC_LAPI_URL}' # XXX: we may want to change this 30 | 31 | assert os.path.exists(CONFIG) 32 | assert os.stat(CONFIG).st_mode & 0o777 == 0o600 33 | assert os.path.exists(f'/usr/local/bin/{BOUNCER}') 34 | assert os.stat(f'/usr/local/bin/{BOUNCER}').st_mode & 0o777 == 0o755 35 | 36 | c = pexpect.spawn( 37 | '/usr/bin/sh', ['scripts/install.sh'], 38 | cwd=project_repo 39 | ) 40 | 41 | c.expect(f"ERR.* /usr/local/bin/{BOUNCER} is already installed. Exiting") 42 | 43 | 44 | @pytest.mark.dependency(depends=['test_install_no_crowdsec']) 45 | def test_upgrade_no_crowdsec(project_repo, must_be_root): 46 | os.remove(f'/usr/local/bin/{BOUNCER}') 47 | 48 | c = pexpect.spawn( 49 | '/usr/bin/sh', ['scripts/upgrade.sh'], 50 | cwd=project_repo 51 | ) 52 | 53 | c.expect(f"{BOUNCER} upgraded successfully") 54 | c.wait() 55 | assert c.terminated 56 | assert c.exitstatus == 0 57 | 58 | assert os.path.exists(f'/usr/local/bin/{BOUNCER}') 59 | assert os.stat(f'/usr/local/bin/{BOUNCER}').st_mode & 0o777 == 0o755 60 | 61 | 62 | @pytest.mark.dependency(depends=['test_upgrade_no_crowdsec']) 63 | def test_uninstall_no_crowdsec(project_repo, must_be_root): 64 | c = pexpect.spawn( 65 | '/usr/bin/sh', ['scripts/uninstall.sh'], 66 | cwd=project_repo 67 | ) 68 | 69 | c.expect(f"{BOUNCER} has been successfully uninstalled") 70 | c.wait() 71 | assert c.terminated 72 | assert c.exitstatus == 0 73 | 74 | assert not os.path.exists(CONFIG) 75 | assert not os.path.exists(f'/usr/local/bin/{BOUNCER}') 76 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Build + tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | name: "Build + tests" 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 26 | with: 27 | go-version-file: go.mod 28 | 29 | - name: Build 30 | run: | 31 | make build 32 | 33 | - name: Run unit tests 34 | run: | 35 | go install github.com/kyoh86/richgo@v0.3.12 36 | set -o pipefail 37 | make test | richgo testfilter 38 | env: 39 | RICHGO_FORCE_COLOR: 1 40 | 41 | - name: Install uv 42 | uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7.1.0 43 | with: 44 | version: 0.5.24 45 | enable-cache: true 46 | cache-dependency-glob: "test/uv.lock" 47 | 48 | - name: "Set up Python" 49 | uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 50 | with: 51 | python-version-file: "test/.python-version" 52 | 53 | - name: Install the project 54 | working-directory: ./test 55 | run: uv sync --all-extras --dev 56 | 57 | - name: Install functional test dependencies 58 | run: | 59 | docker network create net-test 60 | 61 | - name: Run functional tests 62 | env: 63 | CROWDSEC_TEST_VERSION: dev 64 | CROWDSEC_TEST_FLAVORS: full 65 | CROWDSEC_TEST_NETWORK: net-test 66 | CROWDSEC_TEST_TIMEOUT: 60 67 | PYTEST_ADDOPTS: --durations=0 -vv --color=yes -m "not (deb or rpm)" 68 | working-directory: ./test 69 | run: | 70 | # everything except for 71 | # - install (requires root, ignored by default) 72 | # - deb/rpm (on their own workflows) 73 | uv run pytest 74 | # these need root 75 | sudo -E $(which uv) run pytest ./tests/install/no_crowdsec 76 | # these need a running crowdsec 77 | docker run -d --name crowdsec -e CI_TESTING=true -e DISABLE_ONLINE_API=true -e CROWDSEC_BYPASS_DB_VOLUME_CHECK=true -p 8080:8080 -ti crowdsecurity/crowdsec 78 | cat >/usr/local/bin/cscli <<'EOT' 79 | #!/bin/sh 80 | docker exec crowdsec cscli "$@" 81 | EOT 82 | chmod u+x /usr/local/bin/cscli 83 | sleep 5 84 | sudo -E $(which uv) run pytest ./tests/install/with_crowdsec 85 | 86 | - name: Lint 87 | working-directory: ./test 88 | run: | 89 | uv run ruff check 90 | uv run basedpyright 91 | 92 | -------------------------------------------------------------------------------- /rpm/SPECS/crowdsec-blocklist-mirror.spec: -------------------------------------------------------------------------------- 1 | Name: crowdsec-blocklist-mirror 2 | Version: %(echo $VERSION) 3 | Release: %(echo $PACKAGE_NUMBER)%{?dist} 4 | Summary: CrowdSec blocklist mirror 5 | 6 | License: MIT 7 | URL: https://crowdsec.net 8 | Source0: https://github.com/crowdsecurity/%{name}/archive/v%(echo $VERSION).tar.gz 9 | BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) 10 | 11 | BuildRequires: make 12 | %{?fc33:BuildRequires: systemd-rpm-macros} 13 | 14 | Requires: gettext 15 | 16 | %define debug_package %{nil} 17 | 18 | %description 19 | 20 | %define version_number %(echo $VERSION) 21 | %define releasever %(echo $RELEASEVER) 22 | %global local_version v%{version_number}-%{releasever}-rpm 23 | %global name crowdsec-blocklist-mirror 24 | %global __mangle_shebangs_exclude_from /usr/bin/env 25 | 26 | %prep 27 | %setup -n %{name}-%{version} 28 | 29 | %build 30 | BUILD_VERSION=%{local_version} make 31 | 32 | %install 33 | rm -rf %{buildroot} 34 | mkdir -p %{buildroot}%{_bindir} 35 | install -m 755 -D %{name} %{buildroot}%{_bindir}/%{name} 36 | install -m 600 -D config/%{name}.yaml %{buildroot}/etc/crowdsec/bouncers/%{name}.yaml 37 | install -m 600 -D scripts/_bouncer.sh %{buildroot}/usr/lib/%{name}/_bouncer.sh 38 | 39 | mkdir -p %{buildroot}%{_unitdir} 40 | BIN=%{_bindir}/%{name} CFG=/etc/crowdsec/bouncers envsubst '$BIN $CFG' < config/%{name}.service > %{buildroot}%{_unitdir}/%{name}.service 41 | 42 | %clean 43 | rm -rf %{buildroot} 44 | 45 | %files 46 | %defattr(-,root,root,-) 47 | %{_bindir}/%{name} 48 | /usr/lib/%{name}/_bouncer.sh 49 | %{_unitdir}/%{name}.service 50 | %config(noreplace) /etc/crowdsec/bouncers/%{name}.yaml 51 | 52 | %post 53 | systemctl daemon-reload 54 | 55 | . /usr/lib/%{name}/_bouncer.sh 56 | START=1 57 | 58 | if [ "$1" = "1" ]; then 59 | if need_api_key; then 60 | if ! set_api_key; then 61 | START=0 62 | fi 63 | fi 64 | fi 65 | 66 | set_local_lapi_url 'CROWDSEC_LAPI_URL' 67 | 68 | %systemd_post %{name}.service 69 | 70 | if [ "$START" -eq 0 ]; then 71 | echo "no api key was generated, you can generate one on your LAPI Server by running 'cscli bouncers add ' and add it to '$CONFIG'" >&2 72 | else 73 | %if 0%{?fc35} 74 | systemctl enable "$SERVICE" 75 | %endif 76 | systemctl start "$SERVICE" 77 | fi 78 | 79 | echo "$BOUNCER has been successfully installed" 80 | 81 | %changelog 82 | * Fri Apr 29 2022 Shivam Sandbhor 83 | - First initial packaging 84 | 85 | %preun 86 | . /usr/lib/%{name}/_bouncer.sh 87 | 88 | if [ "$1" = "0" ]; then 89 | systemctl stop "$SERVICE" || echo "cannot stop service" 90 | systemctl disable "$SERVICE" || echo "cannot disable service" 91 | delete_bouncer 92 | fi 93 | 94 | %postun 95 | 96 | if [ "$1" == "1" ] ; then 97 | systemctl restart %{name} || echo "cannot restart service" 98 | fi 99 | -------------------------------------------------------------------------------- /pkg/cfg/logging.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | 10 | log "github.com/sirupsen/logrus" 11 | "github.com/sirupsen/logrus/hooks/writer" 12 | "gopkg.in/natefinch/lumberjack.v2" 13 | 14 | "github.com/crowdsecurity/go-cs-lib/ptr" 15 | ) 16 | 17 | type LoggingConfig struct { 18 | LogLevel *log.Level `yaml:"log_level"` 19 | LogMedia string `yaml:"log_media"` 20 | LogDir string `yaml:"log_dir"` 21 | LogMaxSize int `yaml:"log_max_size,omitempty"` 22 | LogMaxFiles int `yaml:"log_max_backups,omitempty"` 23 | LogMaxAge int `yaml:"log_max_age,omitempty"` 24 | CompressLogs *bool `yaml:"compress_logs,omitempty"` 25 | } 26 | 27 | func (c *LoggingConfig) LoggerForFile(fileName string) (io.Writer, error) { 28 | if c.LogMedia == "stdout" { 29 | return os.Stderr, nil 30 | } 31 | 32 | // default permissions will be 0600 from lumberjack 33 | // and are preserved if the file already exists 34 | 35 | l := &lumberjack.Logger{ 36 | Filename: filepath.Join(c.LogDir, fileName), 37 | MaxSize: c.LogMaxSize, 38 | MaxBackups: c.LogMaxFiles, 39 | MaxAge: c.LogMaxAge, 40 | Compress: *c.CompressLogs, 41 | } 42 | 43 | return l, nil 44 | } 45 | 46 | func (c *LoggingConfig) setDefaults() { 47 | if c.LogMedia == "" { 48 | c.LogMedia = "stdout" 49 | } 50 | 51 | if c.LogDir == "" { 52 | c.LogDir = "/var/log/" 53 | } 54 | 55 | if c.LogLevel == nil { 56 | c.LogLevel = ptr.Of(log.InfoLevel) 57 | } 58 | 59 | if c.LogMaxSize == 0 { 60 | c.LogMaxSize = 40 61 | } 62 | 63 | if c.LogMaxFiles == 0 { 64 | c.LogMaxFiles = 3 65 | } 66 | 67 | if c.LogMaxAge == 0 { 68 | c.LogMaxAge = 30 69 | } 70 | 71 | if c.CompressLogs == nil { 72 | c.CompressLogs = ptr.Of(true) 73 | } 74 | } 75 | 76 | func (c *LoggingConfig) validate() error { 77 | if c.LogMedia != "stdout" && c.LogMedia != "file" { 78 | return errors.New("log_media should be either 'stdout' or 'file'") 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func (c *LoggingConfig) setup(fileName string) error { 85 | c.setDefaults() 86 | 87 | if err := c.validate(); err != nil { 88 | return err 89 | } 90 | 91 | log.SetLevel(*c.LogLevel) 92 | 93 | if c.LogMedia == "stdout" { 94 | return nil 95 | } 96 | 97 | log.SetFormatter(&log.TextFormatter{TimestampFormat: time.RFC3339, FullTimestamp: true}) 98 | 99 | logger, err := c.LoggerForFile(fileName) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | log.SetOutput(logger) 105 | 106 | // keep stderr for panic/fatal, otherwise process failures 107 | // won't be visible enough 108 | log.AddHook(&writer.Hook{ 109 | Writer: os.Stderr, 110 | LogLevels: []log.Level{ 111 | log.PanicLevel, 112 | log.FatalLevel, 113 | }, 114 | }) 115 | 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/crowdsecurity/cs-blocklist-mirror 2 | 3 | go 1.25.2 4 | 5 | require ( 6 | github.com/crowdsecurity/crowdsec v1.7.3 7 | github.com/crowdsecurity/go-cs-bouncer v0.0.19 8 | github.com/crowdsecurity/go-cs-lib v0.0.23 9 | github.com/felixge/httpsnoop v1.0.4 10 | github.com/prometheus/client_golang v1.23.2 11 | github.com/sirupsen/logrus v1.9.3 12 | golang.org/x/sync v0.17.0 13 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 14 | gopkg.in/yaml.v3 v3.0.1 15 | ) 16 | 17 | require ( 18 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 19 | github.com/beorn7/perks v1.0.1 // indirect 20 | github.com/cenkalti/backoff/v5 v5.0.3 // indirect 21 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 22 | github.com/corazawaf/coraza/v3 v3.3.3 // indirect 23 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 24 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 25 | github.com/ebitengine/purego v0.8.4 // indirect 26 | github.com/expr-lang/expr v1.17.5 // indirect 27 | github.com/go-ole/go-ole v1.2.6 // indirect 28 | github.com/go-openapi/analysis v0.23.0 // indirect 29 | github.com/go-openapi/errors v0.22.2 // indirect 30 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 31 | github.com/go-openapi/jsonreference v0.21.0 // indirect 32 | github.com/go-openapi/loads v0.22.0 // indirect 33 | github.com/go-openapi/spec v0.21.0 // indirect 34 | github.com/go-openapi/strfmt v0.23.0 // indirect 35 | github.com/go-openapi/swag v0.23.1 // indirect 36 | github.com/go-openapi/validate v0.24.0 // indirect 37 | github.com/goccy/go-yaml v1.18.0 // indirect 38 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 39 | github.com/google/go-querystring v1.1.0 // indirect 40 | github.com/google/uuid v1.6.0 // indirect 41 | github.com/josharian/intern v1.0.0 // indirect 42 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 43 | github.com/mailru/easyjson v0.9.0 // indirect 44 | github.com/mitchellh/mapstructure v1.5.0 // indirect 45 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 46 | github.com/oklog/ulid v1.3.1 // indirect 47 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 48 | github.com/prometheus/client_model v0.6.2 // indirect 49 | github.com/prometheus/common v0.66.1 // indirect 50 | github.com/prometheus/procfs v0.17.0 // indirect 51 | github.com/shirou/gopsutil/v4 v4.25.8 // indirect 52 | github.com/tklauser/go-sysconf v0.3.15 // indirect 53 | github.com/tklauser/numcpus v0.10.0 // indirect 54 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 55 | go.mongodb.org/mongo-driver v1.17.4 // indirect 56 | go.yaml.in/yaml/v2 v2.4.2 // indirect 57 | golang.org/x/net v0.44.0 // indirect 58 | golang.org/x/sys v0.36.0 // indirect 59 | google.golang.org/protobuf v1.36.8 // indirect 60 | gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect 61 | gopkg.in/yaml.v2 v2.4.0 // indirect 62 | ) 63 | -------------------------------------------------------------------------------- /test/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "cs-blocklist-mirror-tests" 3 | version = "0.1.0" 4 | description = "Tests for cs-blocklist-mirror" 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "pexpect>=4.9.0", 9 | "pytest>=8.3.5", 10 | "pytest-cs>=0.7.24", 11 | "pytest-dependency>=0.6.0", 12 | "pytest-dotenv>=0.5.2", 13 | "zxcvbn>=4.5.0", 14 | ] 15 | 16 | [tool.uv.sources] 17 | pytest-cs = { git = "https://github.com/crowdsecurity/pytest-cs" } 18 | 19 | [dependency-groups] 20 | dev = [ 21 | "basedpyright>=1.28.4", 22 | "ipdb>=0.13.13", 23 | "ruff>=0.11.2", 24 | ] 25 | 26 | [tool.ruff] 27 | 28 | line-length = 120 29 | 30 | [tool.ruff.lint] 31 | select = [ 32 | "ALL" 33 | ] 34 | 35 | ignore = [ 36 | "ANN", # Missing type annotations 37 | "ARG001", # Unused function argument: `...` 38 | "COM812", # Trailing comma missing 39 | "D100", # Missing docstring in public module 40 | "D103", # Missing docstring in public function 41 | "D104", # Missing docstring in public package 42 | "D202", 43 | "D203", # incorrect-blank-line-before-class 44 | "D212", # Multi-line docstring summary should start at the first line 45 | "D400", # First line should end with a period 46 | "D415", # First line should end with a period, question mark, or exclamation point 47 | "ERA", 48 | "FIX002", # Line contains TODO, consider resolving the issue 49 | "FIX003", # Line contains XXX, consider resolving the issue 50 | "I001", 51 | "PLW1510", # `subprocess.run` without explicit `check` argument 52 | "S101", # Use of 'assert' detected 53 | "S603", # `subprocess` call: check for execution of untrusted input 54 | "S607", # Starting a process with a partial executable path 55 | "TD", 56 | "PLR2004", # Magic value used in comparison, consider replacing `...` with a constant variable 57 | "PLR0913", # Too many arguments in function definition (6 > 5) 58 | "PTH107", # `os.remove()` should be replaced by `Path.unlink()` 59 | "PTH108", # `os.unlink()` should be replaced by `Path.unlink()` 60 | "PTH110", # `os.path.exists()` should be replaced by `Path.exists()` 61 | "PTH116", # `os.stat()` should be replaced by `Path.stat()`, `Path.owner()`, or `Path.group()` 62 | "PTH123", # `open()` should be replaced by `Path.open()` 63 | "PT022", # No teardown in fixture `fw_cfg_factory`, use `return` instead of `yield` 64 | "Q000", # Single quotes found but double quotes preferred 65 | "UP022", # Prefer `capture_output` over sending `stdout` and `stderr` to `PIPE` 66 | ] 67 | 68 | [tool.basedpyright] 69 | reportAny = "none" 70 | reportArgumentType = "none" 71 | reportAttributeAccessIssue = "none" 72 | reportMissingParameterType = "none" 73 | reportMissingTypeStubs = "none" 74 | reportOptionalMemberAccess = "none" 75 | reportUnknownArgumentType = "none" 76 | reportUnknownMemberType = "none" 77 | reportUnknownParameterType = "none" 78 | reportUnknownVariableType = "none" 79 | reportUnusedCallResult = "none" 80 | reportUnusedParameter = "none" 81 | -------------------------------------------------------------------------------- /.github/workflows/release_publish_docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | release: 5 | types: 6 | - released 7 | - prereleased 8 | 9 | permissions: 10 | contents: read 11 | packages: write 12 | 13 | jobs: 14 | push_to_registry: 15 | name: Push Docker image to Docker Hub 16 | runs-on: ubuntu-latest 17 | steps: 18 | - 19 | name: Check out the repo 20 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 21 | with: 22 | fetch-depth: 0 23 | - 24 | name: Prepare 25 | id: prep 26 | run: | 27 | DOCKER_IMAGE=crowdsecurity/blocklist-mirror 28 | GHCR_IMAGE=ghcr.io/${{ github.repository_owner }}/blocklist-mirror 29 | VERSION=edge 30 | if [[ $GITHUB_REF == refs/tags/* ]]; then 31 | VERSION=${GITHUB_REF#refs/tags/} 32 | elif [[ $GITHUB_REF == refs/heads/* ]]; then 33 | VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -E 's#/+#-#g') 34 | elif [[ $GITHUB_REF == refs/pull/* ]]; then 35 | VERSION=pr-${{ github.event.number }} 36 | fi 37 | TAGS="${DOCKER_IMAGE}:${VERSION},${GHCR_IMAGE}:${VERSION}" 38 | if [[ "${{ github.event_name }}" == "release" && "${{ github.event.release.prerelease }}" == "false" ]]; then 39 | TAGS=$TAGS,${DOCKER_IMAGE}:latest,${GHCR_IMAGE}:latest 40 | fi 41 | echo "version=${VERSION}" >> $GITHUB_OUTPUT 42 | echo "tags=${TAGS}" >> $GITHUB_OUTPUT 43 | echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT 44 | - 45 | name: Set up QEMU 46 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 47 | - 48 | name: Set up Docker Buildx 49 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 50 | - 51 | name: Login to DockerHub 52 | if: github.event_name == 'release' 53 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 54 | with: 55 | username: ${{ secrets.DOCKER_USERNAME }} 56 | password: ${{ secrets.DOCKER_PASSWORD }} 57 | 58 | - name: Login to GitHub Container Registry 59 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 60 | with: 61 | registry: ghcr.io 62 | username: ${{ github.repository_owner }} 63 | password: ${{ secrets.GITHUB_TOKEN }} 64 | - 65 | name: Build and push 66 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 67 | with: 68 | context: . 69 | file: ./Dockerfile 70 | push: ${{ github.event_name == 'release' }} 71 | tags: ${{ steps.prep.outputs.tags }} 72 | # Supported by golang:1.18-alpine: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/s390x 73 | # Supported by alpine: same 74 | platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/s390x 75 | labels: | 76 | org.opencontainers.image.source=${{ github.event.repository.html_url }} 77 | org.opencontainers.image.created=${{ steps.prep.outputs.created }} 78 | org.opencontainers.image.revision=${{ github.sha }} 79 | -------------------------------------------------------------------------------- /test/tests/install/with_crowdsec/test_crowdsec_scripts.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pexpect 3 | import yaml 4 | 5 | import pytest 6 | from pytest_cs.lib import cscli, text 7 | 8 | BOUNCER = "crowdsec-blocklist-mirror" 9 | CONFIG = f"/etc/crowdsec/bouncers/{BOUNCER}.yaml" 10 | 11 | 12 | @pytest.mark.systemd_debug(BOUNCER) 13 | @pytest.mark.dependency 14 | def test_install_crowdsec(project_repo, bouncer_binary, must_be_root): 15 | c = pexpect.spawn( 16 | '/usr/bin/sh', ['scripts/install.sh'], 17 | encoding='utf-8', 18 | cwd=project_repo, 19 | env={"NO_COLOR": "1"} 20 | ) 21 | 22 | c.expect(f"Installing {BOUNCER}") 23 | c.expect("cscli found, generating bouncer api key.") 24 | c.expect("API Key: (.*)") 25 | api_key = text.nocolor(c.match.group(1).strip()) 26 | # XXX: what do we expect here ? 27 | c.wait() 28 | assert c.terminated 29 | # XXX: partial configuration, the service won't start 30 | # assert c.exitstatus == 0 31 | 32 | # installed files 33 | assert os.path.exists(CONFIG) 34 | assert os.stat(CONFIG).st_mode & 0o777 == 0o600 35 | assert os.path.exists(f'/usr/local/bin/{BOUNCER}') 36 | assert os.stat(f'/usr/local/bin/{BOUNCER}').st_mode & 0o777 == 0o755 37 | 38 | # configuration check 39 | with open(CONFIG) as f: 40 | y = yaml.safe_load(f) 41 | assert y['crowdsec_config']['lapi_key'] == api_key 42 | assert y['crowdsec_config']['lapi_url'] == 'http://127.0.0.1:8080' 43 | 44 | # the bouncer is registered 45 | with open(f"{CONFIG}.id") as f: 46 | bouncer_name = f.read().strip() 47 | 48 | assert len(list(cscli.get_bouncers(name=bouncer_name))) == 1 49 | 50 | c = pexpect.spawn( 51 | '/usr/bin/sh', ['scripts/install.sh'], 52 | encoding='utf-8', 53 | cwd=project_repo 54 | ) 55 | 56 | c.expect(f"ERR:.* /usr/local/bin/{BOUNCER} is already installed. Exiting") 57 | 58 | 59 | @pytest.mark.dependency(depends=['test_install_crowdsec']) 60 | def test_upgrade_crowdsec(project_repo, must_be_root): 61 | os.remove(f'/usr/local/bin/{BOUNCER}') 62 | 63 | c = pexpect.spawn( 64 | '/usr/bin/sh', ['scripts/upgrade.sh'], 65 | encoding='utf-8', 66 | cwd=project_repo 67 | ) 68 | 69 | c.expect(f"{BOUNCER} upgraded successfully") 70 | c.wait() 71 | assert c.terminated 72 | assert c.exitstatus == 0 73 | 74 | assert os.path.exists(f'/usr/local/bin/{BOUNCER}') 75 | assert os.stat(f'/usr/local/bin/{BOUNCER}').st_mode & 0o777 == 0o755 76 | 77 | 78 | @pytest.mark.dependency(depends=['test_upgrade_crowdsec']) 79 | def test_uninstall_crowdsec(project_repo, must_be_root): 80 | # the bouncer is registered 81 | with open(f"{CONFIG}.id") as f: 82 | bouncer_name = f.read().strip() 83 | 84 | c = pexpect.spawn( 85 | '/usr/bin/sh', ['scripts/uninstall.sh'], 86 | encoding='utf-8', 87 | cwd=project_repo 88 | ) 89 | 90 | c.expect(f"{BOUNCER} has been successfully uninstalled") 91 | c.wait() 92 | assert c.terminated 93 | assert c.exitstatus == 0 94 | 95 | # installed files 96 | assert not os.path.exists(CONFIG) 97 | assert not os.path.exists(f'/usr/local/bin/{BOUNCER}') 98 | 99 | # the bouncer is unregistered 100 | assert len(list(cscli.get_bouncers(name=bouncer_name))) == 0 101 | -------------------------------------------------------------------------------- /pkg/registry/registry.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "net/url" 5 | "slices" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/prometheus/client_golang/prometheus" 10 | "github.com/prometheus/client_golang/prometheus/promauto" 11 | 12 | "github.com/crowdsecurity/crowdsec/pkg/models" 13 | ) 14 | 15 | var activeDecisionCount prometheus.Gauge = promauto.NewGauge(prometheus.GaugeOpts{ 16 | Name: "active_decision_count", 17 | Help: "Total number of decisions served by any blocklist", 18 | }) 19 | 20 | type Key int 21 | 22 | type DecisionRegistry struct { 23 | ActiveDecisionsByValue map[string]*models.Decision 24 | Key Key 25 | SupportedDecisionTypes []string 26 | } 27 | 28 | func (dr *DecisionRegistry) AddDecisions(decisions []*models.Decision) { 29 | for _, decision := range decisions { 30 | if _, ok := dr.ActiveDecisionsByValue[*decision.Value]; !ok { 31 | activeDecisionCount.Inc() 32 | } 33 | 34 | dr.ActiveDecisionsByValue[*decision.Value] = decision 35 | } 36 | } 37 | 38 | func (dr *DecisionRegistry) GetSupportedDecisionTypesWithFilter(filter url.Values) []string { 39 | // determine allowed types: per-request override or registry default 40 | allowedTypes := make([]string, 0) 41 | if filter.Has("supported_decisions_types") { 42 | for _, v := range filter["supported_decisions_types"] { 43 | for _, t := range strings.Split(v, ",") { 44 | tt := strings.TrimSpace(strings.ToLower(t)) 45 | if tt == "" { 46 | continue 47 | } 48 | allowedTypes = append(allowedTypes, tt) 49 | } 50 | } 51 | } else { 52 | for _, t := range dr.SupportedDecisionTypes { 53 | tt := strings.TrimSpace(strings.ToLower(t)) 54 | if tt == "" { 55 | continue 56 | } 57 | allowedTypes = append(allowedTypes, tt) 58 | } 59 | } 60 | 61 | return allowedTypes 62 | } 63 | 64 | func (dr *DecisionRegistry) GetActiveDecisions(filter url.Values) []*models.Decision { 65 | ret := make([]*models.Decision, 0, len(dr.ActiveDecisionsByValue)) 66 | 67 | allowedTypes := dr.GetSupportedDecisionTypesWithFilter(filter) 68 | 69 | for _, v := range dr.ActiveDecisionsByValue { 70 | // filter by type if allowedTypes is non-empty 71 | if len(allowedTypes) > 0 { 72 | dType := "" 73 | if v.Type != nil { 74 | dType = strings.ToLower(*v.Type) 75 | } 76 | if !slices.Contains(allowedTypes, dType) { 77 | continue 78 | } 79 | } 80 | if filter.Has("ipv6only") && strings.Contains(*v.Value, ".") { 81 | continue 82 | } 83 | 84 | if filter.Has("ipv4only") && strings.Contains(*v.Value, ":") { 85 | continue 86 | } 87 | 88 | if filter.Has("origin") && !strings.EqualFold(*v.Origin, filter.Get("origin")) { 89 | continue 90 | } 91 | 92 | ret = append(ret, v) 93 | } 94 | 95 | if !filter.Has("nosort") { 96 | sort.SliceStable(ret, func(i, j int) bool { 97 | return *ret[i].Value < *ret[j].Value 98 | }) 99 | } 100 | 101 | return ret 102 | } 103 | 104 | func (dr *DecisionRegistry) DeleteDecisions(decisions []*models.Decision) { 105 | for _, decision := range decisions { 106 | if _, ok := dr.ActiveDecisionsByValue[*decision.Value]; ok { 107 | delete(dr.ActiveDecisionsByValue, *decision.Value) 108 | activeDecisionCount.Dec() 109 | } 110 | } 111 | } 112 | 113 | var GlobalDecisionRegistry = DecisionRegistry{ 114 | ActiveDecisionsByValue: make(map[string]*models.Decision), 115 | } 116 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO = go 2 | GOBUILD = $(GO) build 3 | GOTEST = $(GO) test 4 | 5 | BINARY_NAME=crowdsec-blocklist-mirror 6 | TARBALL_NAME=$(BINARY_NAME).tgz 7 | 8 | ifdef BUILD_STATIC 9 | $(warning WARNING: The BUILD_STATIC variable is deprecated and has no effect. Builds are static by default now.) 10 | endif 11 | 12 | # Versioning information can be overridden in the environment 13 | BUILD_VERSION?=$(shell git describe --tags) 14 | BUILD_TIMESTAMP?=$(shell date +%F"_"%T) 15 | BUILD_TAG?=$(shell git rev-parse HEAD) 16 | 17 | LD_OPTS_VARS=\ 18 | -X 'github.com/crowdsecurity/go-cs-lib/version.Version=$(BUILD_VERSION)' \ 19 | -X 'github.com/crowdsecurity/go-cs-lib/version.BuildDate=$(BUILD_TIMESTAMP)' \ 20 | -X 'github.com/crowdsecurity/go-cs-lib/version.Tag=$(BUILD_TAG)' 21 | 22 | ifneq (,$(DOCKER_BUILD)) 23 | LD_OPTS_VARS += -X 'github.com/crowdsecurity/go-cs-lib/version.System=docker' 24 | endif 25 | 26 | export CGO_ENABLED=0 27 | export LD_OPTS=-ldflags "-s -extldflags '-static' $(LD_OPTS_VARS)" \ 28 | -trimpath -tags netgo 29 | 30 | .PHONY: all 31 | all: build test 32 | 33 | # same as "$(MAKE) -f debian/rules clean" but without the dependency on debhelper 34 | .PHONY: clean-debian 35 | clean-debian: 36 | @$(RM) -r debian/$(BINARY_NAME) 37 | @$(RM) -r debian/files 38 | @$(RM) -r debian/.debhelper 39 | @$(RM) -r debian/*.substvars 40 | @$(RM) -r debian/*-stamp 41 | 42 | .PHONY: clean-rpm 43 | clean-rpm: 44 | @$(RM) -r rpm/BUILD 45 | @$(RM) -r rpm/BUILDROOT 46 | @$(RM) -r rpm/RPMS 47 | @$(RM) -r rpm/SOURCES/*.tar.gz 48 | @$(RM) -r rpm/SRPMS 49 | 50 | # Remove everything including all platform binaries and tarballs 51 | .PHONY: clean 52 | clean: clean-release-dir clean-debian clean-rpm 53 | @$(RM) $(BINARY_NAME) 54 | @$(RM) $(TARBALL_NAME) 55 | @$(RM) -r $(BINARY_NAME)-* # platform binary name and leftover release dir 56 | @$(RM) $(BINARY_NAME)-*.tgz # platform release file 57 | 58 | # 59 | # Build binaries 60 | # 61 | 62 | .PHONY: binary 63 | binary: 64 | $(GOBUILD) $(LD_OPTS) -o $(BINARY_NAME) 65 | 66 | .PHONY: build 67 | build: clean binary 68 | 69 | # 70 | # Unit and integration tests 71 | # 72 | 73 | .PHONY: lint 74 | lint: 75 | golangci-lint run 76 | 77 | .PHONY: test 78 | test: 79 | @$(GOTEST) $(LD_OPTS) ./... 80 | 81 | .PHONY: func-tests 82 | func-tests: build 83 | pipenv install --dev 84 | pipenv run pytest -v 85 | 86 | # 87 | # Build release tarballs 88 | # 89 | 90 | RELDIR = $(BINARY_NAME)-$(BUILD_VERSION) 91 | 92 | .PHONY: vendor 93 | vendor: vendor-remove 94 | $(GO) mod vendor 95 | tar czf vendor.tgz vendor 96 | tar --create --auto-compress --file=$(RELDIR)-vendor.tar.xz vendor 97 | 98 | .PHONY: vendor-remove 99 | vendor-remove: 100 | $(RM) -r vendor vendor.tgz *-vendor.tar.xz 101 | 102 | # Called during platform-all, to reuse the directory for other platforms 103 | .PHONY: clean-release-dir 104 | clean-release-dir: 105 | @$(RM) -r $(RELDIR) 106 | 107 | .PHONY: tarball 108 | tarball: binary 109 | @if [ -z $(BUILD_VERSION) ]; then BUILD_VERSION="local" ; fi 110 | @if [ -d $(RELDIR) ]; then echo "$(RELDIR) already exists, please run 'make clean' and retry" ; exit 1 ; fi 111 | @echo Building Release to dir $(RELDIR) 112 | @mkdir -p $(RELDIR)/scripts 113 | @cp $(BINARY_NAME) $(RELDIR)/ 114 | @cp -R ./config $(RELDIR)/ 115 | @cp ./scripts/install.sh $(RELDIR)/ 116 | @cp ./scripts/uninstall.sh $(RELDIR)/ 117 | @cp ./scripts/upgrade.sh $(RELDIR)/ 118 | @cp ./scripts/_bouncer.sh $(RELDIR)/scripts/ 119 | @chmod +x $(RELDIR)/install.sh 120 | @chmod +x $(RELDIR)/uninstall.sh 121 | @chmod +x $(RELDIR)/upgrade.sh 122 | @tar cvzf $(TARBALL_NAME) $(RELDIR) 123 | 124 | .PHONY: release 125 | release: clean tarball 126 | 127 | # 128 | # Build binaries and release tarballs for all platforms 129 | # 130 | 131 | .PHONY: platform-all 132 | platform-all: clean 133 | python3 .github/release.py run-build $(BINARY_NAME) 134 | -------------------------------------------------------------------------------- /test/tests/bouncer/test_tls.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | def test_tls_server(crowdsec, certs_dir, api_key_factory, bouncer, bm_cfg_factory): 4 | """TLS with server-only certificate""" 5 | 6 | api_key = api_key_factory() 7 | 8 | lapi_env = { 9 | 'CACERT_FILE': '/etc/ssl/crowdsec/ca.crt', 10 | 'LAPI_CERT_FILE': '/etc/ssl/crowdsec/lapi.crt', 11 | 'LAPI_KEY_FILE': '/etc/ssl/crowdsec/lapi.key', 12 | 'USE_TLS': 'true', 13 | 'LOCAL_API_URL': 'https://localhost:8080', 14 | 'BOUNCER_KEY_custom': api_key, 15 | } 16 | 17 | certs = certs_dir(lapi_hostname='lapi') 18 | 19 | volumes = { 20 | certs.as_posix(): {'bind': '/etc/ssl/crowdsec', 'mode': 'ro'}, 21 | } 22 | 23 | with crowdsec(environment=lapi_env, volumes=volumes) as cs: 24 | cs.wait_for_log("*CrowdSec Local API listening*") 25 | # TODO: wait_for_https 26 | cs.wait_for_http(8080, '/health', want_status=None) 27 | 28 | port = cs.probe.get_bound_port('8080') 29 | cfg = bm_cfg_factory() 30 | cfg['crowdsec_config']['lapi_url'] = f'https://localhost:{port}' 31 | cfg['crowdsec_config']['lapi_key'] = api_key 32 | 33 | with bouncer(cfg) as bm: 34 | bm.wait_for_lines_fnmatch([ 35 | "*Using API key auth*", 36 | "*auth-api: auth with api key failed*", 37 | "*tls: failed to verify certificate: x509: certificate signed by unknown authority*", 38 | ]) 39 | 40 | cfg['crowdsec_config']['ca_cert_path'] = (certs / 'ca.crt').as_posix() 41 | 42 | with bouncer(cfg) as bm: 43 | bm.wait_for_lines_fnmatch([ 44 | "*Using API key auth*", 45 | "*Starting server at 127.0.0.1:*" 46 | ]) 47 | 48 | 49 | def test_tls_mutual(crowdsec, certs_dir, bouncer, bm_cfg_factory, bouncer_under_test): 50 | """TLS with two-way bouncer/lapi authentication""" 51 | 52 | lapi_env = { 53 | 'CACERT_FILE': '/etc/ssl/crowdsec/ca.crt', 54 | 'LAPI_CERT_FILE': '/etc/ssl/crowdsec/lapi.crt', 55 | 'LAPI_KEY_FILE': '/etc/ssl/crowdsec/lapi.key', 56 | 'USE_TLS': 'true', 57 | 'LOCAL_API_URL': 'https://localhost:8080', 58 | } 59 | 60 | certs = certs_dir(lapi_hostname='lapi') 61 | 62 | volumes = { 63 | certs: {'bind': '/etc/ssl/crowdsec', 'mode': 'ro'}, 64 | } 65 | 66 | with crowdsec(environment=lapi_env, volumes=volumes) as cs: 67 | cs.wait_for_log("*CrowdSec Local API listening*") 68 | # TODO: wait_for_https 69 | cs.wait_for_http(8080, '/health', want_status=None) 70 | 71 | port = cs.probe.get_bound_port('8080') 72 | cfg = bm_cfg_factory() 73 | cfg['crowdsec_config']['lapi_url'] = f'https://localhost:{port}' 74 | cfg['crowdsec_config']['ca_cert_path'] = (certs / 'ca.crt').as_posix() 75 | 76 | cfg['crowdsec_config']['cert_path'] = (certs / 'agent.crt').as_posix() 77 | cfg['crowdsec_config']['key_path'] = (certs / 'agent.key').as_posix() 78 | 79 | with bouncer(cfg) as bm: 80 | bm.wait_for_lines_fnmatch([ 81 | "*Starting crowdsec-blocklist-mirror*", 82 | "*Using CA cert*", 83 | "*Using cert auth with cert * and key *", 84 | "*API error: access forbidden*", 85 | ]) 86 | 87 | cs.wait_for_log("*client certificate OU ?agent-ou? doesn't match expected OU ?bouncer-ou?*") 88 | 89 | cfg['crowdsec_config']['cert_path'] = (certs / 'bouncer.crt').as_posix() 90 | cfg['crowdsec_config']['key_path'] = (certs / 'bouncer.key').as_posix() 91 | 92 | with bouncer(cfg) as bm: 93 | bm.wait_for_lines_fnmatch([ 94 | "*Starting crowdsec-blocklist-mirror*", 95 | "*Using CA cert*", 96 | "*Using cert auth with cert * and key *", 97 | "*Starting server at 127.0.0.1:*" 98 | ]) 99 | 100 | # check that the bouncer is registered 101 | res = cs.cont.exec_run('cscli bouncers list -o json') 102 | assert res.exit_code == 0 103 | bouncers = json.loads(res.output) 104 | assert len(bouncers) == 1 105 | assert bouncers[0]['name'].startswith('@') 106 | assert bouncers[0]['auth_type'] == 'tls' 107 | assert bouncers[0]['type'] == bouncer_under_test 108 | -------------------------------------------------------------------------------- /.github/release.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import json 5 | import os 6 | import shutil 7 | import subprocess 8 | import sys 9 | 10 | 11 | def _goos(): 12 | yield 'linux' 13 | yield 'freebsd' 14 | 15 | 16 | def _goarch(goos): 17 | yield '386' 18 | yield 'amd64' 19 | yield 'arm' 20 | yield 'arm64' 21 | if goos == 'linux': 22 | yield 'ppc64le' 23 | yield 's390x' 24 | yield 'riscv64' 25 | 26 | 27 | def _goarm(goarch): 28 | if goarch != 'arm': 29 | yield '' 30 | return 31 | yield '6' 32 | yield '7' 33 | 34 | 35 | def _build_tarball(os): 36 | if os == 'linux': 37 | yield True 38 | else: 39 | yield False 40 | 41 | 42 | def filename_for_entry(prog_name, entry): 43 | arch = entry['goarch'] 44 | if entry['goarch'] == 'arm': 45 | arch += 'v' + entry['goarm'] 46 | ret = f'{prog_name}-{entry["goos"]}-{arch}' 47 | if entry['build_tarball']: 48 | ret += '.tgz' 49 | return ret 50 | 51 | 52 | def matrix(prog_name): 53 | for goos in _goos(): 54 | for goarch in _goarch(goos): 55 | if (goos, goarch) == ('freebsd', 'riscv64'): 56 | # platform not supported 57 | # gopsutil/v4@v4.25.8/cpu/cpu_freebsd.go:85:13: undefined: cpuTimes 58 | continue 59 | for goarm in _goarm(goarch): 60 | for build_tarball in _build_tarball(goos): 61 | yield { 62 | 'goos': goos, 63 | 'goarch': goarch, 64 | 'goarm': goarm, 65 | 'build_tarball': build_tarball, 66 | } 67 | 68 | 69 | def print_matrix(prog_name): 70 | j = {'include': list(matrix(prog_name))} 71 | 72 | if os.isatty(sys.stdout.fileno()): 73 | print(json.dumps(j, indent=2)) 74 | else: 75 | print(json.dumps(j)) 76 | 77 | 78 | default_tarball = { 79 | 'goos': 'linux', 80 | 'goarch': 'amd64', 81 | 'goarm': '', 82 | 'build_tarball': True, 83 | } 84 | 85 | default_binary = { 86 | 'goos': 'linux', 87 | 'goarch': 'amd64', 88 | 'goarm': '', 89 | 'build_tarball': False, 90 | } 91 | 92 | 93 | def run_build(prog_name): 94 | # call the makefile for each matrix entry 95 | 96 | default_tarball_filename = None 97 | default_binary_filename = None 98 | 99 | for entry in matrix(prog_name): 100 | env = {'GOOS': entry['goos'], 'GOARCH': entry['goarch']} 101 | 102 | if entry['goarm']: 103 | env['GOARM'] = entry['goarm'] 104 | 105 | if entry['build_tarball']: 106 | target = 'tarball' 107 | else: 108 | target = 'binary' 109 | 110 | print(f"Running make {target} for {env}") 111 | 112 | subprocess.run(['make', target], env=os.environ | env, check=True) 113 | 114 | want_filename = filename_for_entry(prog_name, entry) 115 | 116 | if entry['build_tarball']: 117 | os.rename(f'{prog_name}.tgz', want_filename) 118 | else: 119 | os.rename(f'{prog_name}', want_filename) 120 | 121 | # if this is the default tarball or binary, save the filename 122 | # we'll use it later to publish a "default" package 123 | 124 | if entry == default_tarball: 125 | default_tarball_filename = want_filename 126 | 127 | if entry == default_binary: 128 | default_binary_filename = want_filename 129 | 130 | # Remove the directory to reuse it 131 | subprocess.run(['make', 'clean-release-dir'], env=os.environ | env, check=True) 132 | 133 | # publish the default tarball and binary 134 | if default_tarball_filename: 135 | shutil.copy(default_tarball_filename, f'{prog_name}.tgz') 136 | 137 | if default_binary_filename: 138 | shutil.copy(default_binary_filename, f'{prog_name}') 139 | 140 | 141 | def main(): 142 | parser = argparse.ArgumentParser( 143 | description='Build release binaries and tarballs for all supported platforms') 144 | parser.add_argument('action', help='Action to perform (ex. run-build, print-matrix)') 145 | parser.add_argument('prog_name', help='Name of the program (ex. crowdsec-firewall-bouncer)') 146 | 147 | args = parser.parse_args() 148 | 149 | if args.action == 'print-matrix': 150 | print_matrix(args.prog_name) 151 | 152 | if args.action == 'run-build': 153 | run_build(args.prog_name) 154 | 155 | 156 | if __name__ == '__main__': 157 | main() 158 | -------------------------------------------------------------------------------- /test/tests/install/with_crowdsec/test_crowdsec_deb.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import yaml 4 | from pathlib import Path 5 | 6 | import pytest 7 | from zxcvbn import zxcvbn 8 | 9 | pytestmark = pytest.mark.deb 10 | 11 | 12 | # TODO: use fixtures to install/purge and register/unregister bouncers 13 | 14 | 15 | def test_deb_install_purge(deb_package_path, bouncer_under_test, must_be_root): 16 | # test the full install-purge cycle, doing that in separate tests would 17 | # be a bit too much 18 | 19 | # TODO: remove and reinstall 20 | 21 | # use the package built as non-root by test_deb_build() 22 | assert deb_package_path.exists(), f'This test requires {deb_package_path}' 23 | 24 | p = subprocess.check_output( 25 | ['dpkg-deb', '-f', deb_package_path.as_posix(), 'Package'], 26 | encoding='utf-8' 27 | ) 28 | package_name = p.strip() 29 | 30 | subprocess.check_call(['dpkg', '--purge', package_name]) 31 | 32 | bouncer_exe = f"/usr/bin/{bouncer_under_test}" 33 | assert not os.path.exists(bouncer_exe) 34 | 35 | config = f"/etc/crowdsec/bouncers/{bouncer_under_test}.yaml" 36 | assert not os.path.exists(config) 37 | 38 | # install the package 39 | p = subprocess.run( 40 | ['dpkg', '--install', deb_package_path.as_posix()], 41 | stdout=subprocess.PIPE, 42 | stderr=subprocess.PIPE, 43 | encoding='utf-8' 44 | ) 45 | assert p.returncode == 0, f'Failed to install {deb_package_path}' 46 | 47 | assert os.path.exists(bouncer_exe) 48 | assert os.stat(bouncer_exe).st_mode & 0o777 == 0o755 49 | 50 | assert os.path.exists(config) 51 | assert os.stat(config).st_mode & 0o777 == 0o600 52 | 53 | with open(config) as f: 54 | cfg = yaml.safe_load(f) 55 | api_key = cfg['crowdsec_config']['lapi_key'] 56 | # the api key has been set to a random value 57 | assert zxcvbn(api_key)['score'] == 4, f"weak api_key: '{api_key}'" 58 | 59 | with open(config+'.id') as f: 60 | bouncer_name = f.read().strip() 61 | 62 | p = subprocess.check_output(['cscli', 'bouncers', 'list', '-o', 'json']) 63 | bouncers = yaml.safe_load(p) 64 | assert len([b for b in bouncers if b['name'] == bouncer_name]) == 1 65 | 66 | p = subprocess.run( 67 | ['dpkg', '--purge', package_name], 68 | stdout=subprocess.PIPE, 69 | stderr=subprocess.PIPE, 70 | encoding='utf-8' 71 | ) 72 | assert p.returncode == 0, f'Failed to purge {package_name}' 73 | 74 | assert not os.path.exists(bouncer_exe) 75 | assert not os.path.exists(config) 76 | 77 | 78 | def test_deb_install_purge_yaml_local(deb_package_path, bouncer_under_test, must_be_root): 79 | """ 80 | Check .deb package installation with: 81 | 82 | - a pre-existing .yaml.local file with an api key 83 | - a pre-registered bouncer 84 | 85 | => the configuration files are not touched (no new api key) 86 | """ 87 | 88 | assert deb_package_path.exists(), f'This test requires {deb_package_path}' 89 | 90 | p = subprocess.check_output( 91 | ['dpkg-deb', '-f', deb_package_path.as_posix(), 'Package'], 92 | encoding='utf-8' 93 | ) 94 | package_name = p.strip() 95 | 96 | subprocess.check_call(['dpkg', '--purge', package_name]) 97 | subprocess.run(['cscli', 'bouncers', 'delete', 'testbouncer']) 98 | 99 | bouncer_exe = f"/usr/bin/{bouncer_under_test}" 100 | config = Path(f"/etc/crowdsec/bouncers/{bouncer_under_test}.yaml") 101 | config.parent.mkdir(parents=True, exist_ok=True) 102 | 103 | subprocess.check_call(['cscli', 'bouncers', 'add', 'testbouncer', '-k', '123456']) 104 | 105 | with open(config.with_suffix('.yaml.local'), 'w') as f: 106 | f.write(yaml.dump({"crowdsec_config": {"lapi_key": "123456"}})) 107 | 108 | p = subprocess.run( 109 | ['dpkg', '--install', deb_package_path.as_posix()], 110 | stdout=subprocess.PIPE, 111 | stderr=subprocess.PIPE, 112 | encoding='utf-8' 113 | ) 114 | assert p.returncode == 0, f'Failed to install {deb_package_path}' 115 | 116 | assert os.path.exists(bouncer_exe) 117 | assert os.path.exists(config) 118 | 119 | with open(config) as f: 120 | cfg = yaml.safe_load(f) 121 | api_key = cfg['crowdsec_config']['lapi_key'] 122 | # the api key has not been set 123 | assert api_key == '${API_KEY}' 124 | 125 | p = subprocess.check_output([bouncer_exe, '-c', config, '-T']) 126 | merged_config = yaml.safe_load(p) 127 | assert merged_config['crowdsec_config']['lapi_key'] == '123456' 128 | 129 | os.unlink(config.with_suffix('.yaml.local')) 130 | 131 | p = subprocess.run( 132 | ['dpkg', '--purge', package_name], 133 | stdout=subprocess.PIPE, 134 | stderr=subprocess.PIPE, 135 | encoding='utf-8' 136 | ) 137 | assert p.returncode == 0, f'Failed to purge {package_name}' 138 | 139 | assert not os.path.exists(bouncer_exe) 140 | assert not os.path.exists(config) 141 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "os" 10 | "os/signal" 11 | "strings" 12 | "syscall" 13 | 14 | log "github.com/sirupsen/logrus" 15 | "golang.org/x/sync/errgroup" 16 | 17 | csbouncer "github.com/crowdsecurity/go-cs-bouncer" 18 | "github.com/crowdsecurity/go-cs-lib/csdaemon" 19 | "github.com/crowdsecurity/go-cs-lib/ptr" 20 | "github.com/crowdsecurity/go-cs-lib/version" 21 | 22 | "github.com/crowdsecurity/crowdsec/pkg/apiclient" 23 | 24 | "github.com/crowdsecurity/cs-blocklist-mirror/pkg/cfg" 25 | "github.com/crowdsecurity/cs-blocklist-mirror/pkg/registry" 26 | "github.com/crowdsecurity/cs-blocklist-mirror/pkg/server" 27 | ) 28 | 29 | func HandleSignals(ctx context.Context) error { 30 | signalChan := make(chan os.Signal, 1) 31 | signal.Notify(signalChan, syscall.SIGTERM, os.Interrupt) 32 | 33 | select { 34 | case s := <-signalChan: 35 | switch s { 36 | case syscall.SIGTERM: 37 | return errors.New("received SIGTERM") 38 | case os.Interrupt: // cross-platform SIGINT 39 | return errors.New("received interrupt") 40 | } 41 | case <-ctx.Done(): 42 | return ctx.Err() 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func Execute() error { 49 | configPath := flag.String("c", "", "path to crowdsec-blocklist-mirror.yaml") 50 | bouncerVersion := flag.Bool("version", false, "display version and exit") 51 | traceMode := flag.Bool("trace", false, "set trace mode") 52 | debugMode := flag.Bool("debug", false, "set debug mode") 53 | testConfig := flag.Bool("t", false, "test config and exit") 54 | showConfig := flag.Bool("T", false, "show full config (.yaml + .yaml.local) and exit") 55 | 56 | flag.Parse() 57 | 58 | if *bouncerVersion { 59 | fmt.Fprintf(os.Stdout, "%s", version.FullString()) 60 | return nil 61 | } 62 | 63 | if configPath == nil || *configPath == "" { 64 | return errors.New("configuration file is required") 65 | } 66 | 67 | configBytes, err := cfg.MergedConfig(*configPath) 68 | if err != nil { 69 | return fmt.Errorf("unable to read config file: %w", err) 70 | } 71 | 72 | if *showConfig { 73 | fmt.Fprintln(os.Stdout, string(configBytes)) 74 | return nil 75 | } 76 | 77 | config, err := cfg.NewConfig(bytes.NewReader(configBytes)) 78 | if err != nil { 79 | return fmt.Errorf("unable to load configuration: %w", err) 80 | } 81 | 82 | log.Infof("Starting crowdsec-blocklist-mirror %s", version.Version) 83 | 84 | if err := config.ValidateAndSetDefaults(); err != nil { 85 | return err 86 | } 87 | 88 | // propagate supported decision types to the registry for runtime filtering 89 | registry.GlobalDecisionRegistry.SupportedDecisionTypes = config.CrowdsecConfig.SupportedDecisionsTypes 90 | 91 | if debugMode != nil && *debugMode { 92 | log.SetLevel(log.DebugLevel) 93 | } 94 | 95 | if traceMode != nil && *traceMode { 96 | log.SetLevel(log.TraceLevel) 97 | } 98 | 99 | decisionStreamer := csbouncer.StreamBouncer{ 100 | APIKey: config.CrowdsecConfig.LapiKey, 101 | APIUrl: config.CrowdsecConfig.LapiURL, 102 | TickerInterval: config.CrowdsecConfig.UpdateFrequency, 103 | Opts: apiclient.DecisionsStreamOpts{ 104 | ScenariosContaining: strings.Join(config.CrowdsecConfig.IncludeScenariosContaining, ","), 105 | ScenariosNotContaining: strings.Join(config.CrowdsecConfig.ExcludeScenariosContaining, ","), 106 | Origins: strings.Join(config.CrowdsecConfig.OnlyIncludeDecisionsFrom, ","), 107 | Scopes: strings.Join(config.CrowdsecConfig.Scopes, ","), 108 | }, 109 | UserAgent: "crowdsec-blocklist-mirror/" + version.String(), 110 | CertPath: config.CrowdsecConfig.CertPath, 111 | KeyPath: config.CrowdsecConfig.KeyPath, 112 | CAPath: config.CrowdsecConfig.CAPath, 113 | InsecureSkipVerify: ptr.Of(config.CrowdsecConfig.InsecureSkipVerify), 114 | } 115 | 116 | if err := decisionStreamer.Init(); err != nil { 117 | return err 118 | } 119 | 120 | if *testConfig { 121 | log.Info("config is valid") 122 | return nil 123 | } 124 | 125 | g, ctx := errgroup.WithContext(context.Background()) 126 | 127 | g.Go(func() error { 128 | return decisionStreamer.Run(ctx) 129 | }) 130 | 131 | g.Go(func() error { 132 | if err := server.RunServer(ctx, g, config); err != nil { 133 | return fmt.Errorf("blocklist server failed: %w", err) 134 | } 135 | 136 | return nil 137 | }) 138 | 139 | csdaemon.Notify(csdaemon.Ready, log.StandardLogger()) 140 | 141 | g.Go(func() error { 142 | return HandleSignals(ctx) 143 | }) 144 | 145 | g.Go(func() error { 146 | for { 147 | select { 148 | case <-ctx.Done(): 149 | log.Info("terminating bouncer process") 150 | return nil 151 | case decisions := <-decisionStreamer.Stream: 152 | if decisions == nil { 153 | continue 154 | } 155 | 156 | if len(decisions.New) > 0 { 157 | log.Infof("received %d new decisions", len(decisions.New)) 158 | registry.GlobalDecisionRegistry.AddDecisions(decisions.New) 159 | } 160 | 161 | if len(decisions.Deleted) > 0 { 162 | log.Infof("received %d expired decisions", len(decisions.Deleted)) 163 | registry.GlobalDecisionRegistry.DeleteDecisions(decisions.Deleted) 164 | } 165 | } 166 | } 167 | }) 168 | 169 | if err := g.Wait(); err != nil { 170 | return fmt.Errorf("process terminated with error: %w", err) 171 | } 172 | 173 | return nil 174 | } 175 | -------------------------------------------------------------------------------- /pkg/cfg/config.go: -------------------------------------------------------------------------------- 1 | package cfg 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "slices" 9 | "strings" 10 | 11 | "github.com/sirupsen/logrus" 12 | "gopkg.in/yaml.v3" 13 | 14 | "github.com/crowdsecurity/go-cs-lib/csstring" 15 | "github.com/crowdsecurity/go-cs-lib/csyaml" 16 | 17 | "github.com/crowdsecurity/cs-blocklist-mirror/pkg/formatters" 18 | ) 19 | 20 | var blocklistMirrorLogFilePath = "crowdsec-blocklist-mirror.log" 21 | 22 | type CrowdsecConfig struct { 23 | LapiKey string `yaml:"lapi_key"` 24 | LapiURL string `yaml:"lapi_url"` 25 | UpdateFrequency string `yaml:"update_frequency"` 26 | InsecureSkipVerify bool `yaml:"insecure_skip_verify"` 27 | CertPath string `yaml:"cert_path"` 28 | KeyPath string `yaml:"key_path"` 29 | CAPath string `yaml:"ca_cert_path"` 30 | IncludeScenariosContaining []string `yaml:"include_scenarios_containing"` 31 | ExcludeScenariosContaining []string `yaml:"exclude_scenarios_containing"` 32 | OnlyIncludeDecisionsFrom []string `yaml:"only_include_decisions_from"` 33 | Scopes []string `yaml:"scopes,omitempty"` 34 | SupportedDecisionsTypes []string `yaml:"supported_decisions_types"` 35 | } 36 | 37 | type BlockListConfig struct { 38 | Format string `yaml:"format"` 39 | Endpoint string `yaml:"endpoint"` 40 | Authentication struct { 41 | Type string `yaml:"type"` 42 | User string `yaml:"user"` 43 | Password string `yaml:"password"` 44 | Token string `yaml:"token"` 45 | TrustedIPs []string `yaml:"trusted_ips"` 46 | } `yaml:"authentication"` 47 | } 48 | 49 | type MetricConfig struct { 50 | Enabled bool `yaml:"enabled"` 51 | Endpoint string `yaml:"endpoint"` 52 | } 53 | 54 | type TLSConfig struct { 55 | CertFile string `yaml:"cert_file"` 56 | KeyFile string `yaml:"key_file"` 57 | } 58 | 59 | type Config struct { 60 | CrowdsecConfig CrowdsecConfig `yaml:"crowdsec_config"` 61 | Blocklists []*BlockListConfig `yaml:"blocklists"` 62 | ListenURI string `yaml:"listen_uri"` 63 | TLS TLSConfig `yaml:"tls"` 64 | Metrics MetricConfig `yaml:"metrics"` 65 | Logging LoggingConfig `yaml:",inline"` 66 | ConfigVersion string `yaml:"config_version"` 67 | EnableAccessLogs bool `yaml:"enable_access_logs"` 68 | } 69 | 70 | func (cfg *Config) ValidateAndSetDefaults() error { 71 | if cfg.CrowdsecConfig.LapiKey == "" && cfg.CrowdsecConfig.CertPath == "" { 72 | return errors.New("one of lapi_key or cert_path is required") 73 | } 74 | 75 | if cfg.CrowdsecConfig.LapiURL == "" { 76 | return errors.New("lapi_url is required") 77 | } 78 | 79 | if !strings.HasSuffix(cfg.CrowdsecConfig.LapiURL, "/") { 80 | cfg.CrowdsecConfig.LapiURL += "/" 81 | } 82 | 83 | if cfg.CrowdsecConfig.UpdateFrequency == "" { 84 | logrus.Warn("update_frequency is not provided") 85 | 86 | cfg.CrowdsecConfig.UpdateFrequency = "10s" 87 | } 88 | 89 | if cfg.ConfigVersion == "" { 90 | logrus.Warn("config version is not provided; assuming v1.0") 91 | 92 | cfg.ConfigVersion = "v1.0" 93 | } 94 | 95 | if cfg.ListenURI == "" { 96 | logrus.Warn("listen_uri is not provided ; assuming 127.0.0.1:41412") 97 | 98 | cfg.ListenURI = "127.0.0.1:41412" 99 | } 100 | 101 | validAuthenticationTypes := []string{"basic", "ip_based", "none"} 102 | alreadyUsedEndpoint := make(map[string]struct{}) 103 | validFormats := []string{} 104 | 105 | for format := range formatters.ByName { 106 | validFormats = append(validFormats, format) 107 | } 108 | 109 | for _, blockList := range cfg.Blocklists { 110 | if _, ok := alreadyUsedEndpoint[blockList.Endpoint]; ok { 111 | return fmt.Errorf("%s endpoint used more than once", blockList.Endpoint) 112 | } 113 | 114 | alreadyUsedEndpoint[blockList.Endpoint] = struct{}{} 115 | 116 | if !slices.Contains(validFormats, blockList.Format) { 117 | return fmt.Errorf("%s format is not supported. Supported formats are '%s'", blockList.Format, strings.Join(validFormats, ",")) 118 | } 119 | 120 | if !slices.Contains(validAuthenticationTypes, strings.ToLower(blockList.Authentication.Type)) && blockList.Authentication.Type != "" { 121 | return fmt.Errorf( 122 | "%s authentication type is not supported. Supported authentication types are '%s'", 123 | blockList.Authentication.Type, 124 | strings.Join(validAuthenticationTypes, ","), 125 | ) 126 | } 127 | } 128 | 129 | return nil 130 | } 131 | 132 | func MergedConfig(configPath string) ([]byte, error) { 133 | patcher := csyaml.NewPatcher(configPath, ".local") 134 | 135 | data, err := patcher.MergedPatchContent() 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | return data, nil 141 | } 142 | 143 | func NewConfig(reader io.Reader) (Config, error) { 144 | config := Config{} 145 | 146 | fcontent, err := io.ReadAll(reader) 147 | if err != nil { 148 | return config, err 149 | } 150 | 151 | configBuff := csstring.StrictExpand(string(fcontent), os.LookupEnv) 152 | 153 | err = yaml.Unmarshal([]byte(configBuff), &config) 154 | if err != nil { 155 | return config, fmt.Errorf("failed to unmarshal: %w", err) 156 | } 157 | 158 | if err := config.Logging.setup(blocklistMirrorLogFilePath); err != nil { 159 | return config, err 160 | } 161 | 162 | return config, nil 163 | } 164 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | linters: 4 | default: all 5 | disable: 6 | - errcheck 7 | - forcetypeassert 8 | - promlinter 9 | - embeddedstructfieldcheck 10 | - funcorder 11 | - noinlineerr 12 | - wsl_v5 13 | 14 | # 15 | # Redundant 16 | # 17 | - cyclop # revive 18 | - funlen # revive 19 | - gocognit # revive 20 | - gocyclo # revive 21 | - lll # revive 22 | 23 | # 24 | # Recommended? (easy) 25 | # 26 | 27 | - godot # Check if comments end in a period 28 | - gosec # (gas): Inspects source code for security problems 29 | - inamedparam # reports interfaces with unnamed method parameters 30 | - wrapcheck # Checks that errors returned from external packages are wrapped 31 | 32 | # 33 | # Recommended? (requires some work) 34 | # 35 | 36 | - mnd # An analyzer to detect magic numbers. 37 | 38 | # 39 | # Formatting only, useful in IDE but should not be forced on CI? 40 | # 41 | 42 | - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity 43 | - whitespace # Whitespace is a linter that checks for unnecessary newlines at the start and end of functions, if, for, etc. 44 | - wsl # add or remove empty lines 45 | 46 | # 47 | # Well intended, but not ready for this 48 | # 49 | - godox # Tool for detection of FIXME, TODO and other comment keywords 50 | - err113 # Go linter to check the errors handling expressions 51 | - paralleltest # Detects missing usage of t.Parallel() method in your Go test 52 | - testpackage # linter that makes you use a separate _test package 53 | 54 | # 55 | # Too strict / too many false positives (for now?) 56 | # 57 | - exhaustruct # Checks if all structure fields are initialized 58 | - gochecknoglobals # Check that no global variables exist. 59 | - goconst # Finds repeated strings that could be replaced by a constant 60 | - tagliatelle # Checks the struct tags. 61 | - varnamelen # checks that the length of a variable's name matches its scope 62 | - prealloc 63 | 64 | settings: 65 | 66 | depguard: 67 | rules: 68 | yaml: 69 | deny: 70 | - pkg: gopkg.in/yaml.v2 71 | desc: yaml.v2 is deprecated for new code in favor of yaml.v3 72 | 73 | errcheck: 74 | check-type-assertions: false 75 | 76 | gocritic: 77 | enable-all: true 78 | disabled-checks: 79 | - appendCombine 80 | - hugeParam 81 | - importShadow 82 | - paramTypeCombine 83 | - whyNoLint 84 | 85 | govet: 86 | disable: 87 | - fieldalignment 88 | enable-all: true 89 | 90 | misspell: 91 | locale: US 92 | 93 | nlreturn: 94 | block-size: 5 95 | 96 | nolintlint: 97 | require-explanation: false # don't require an explanation for nolint directives 98 | require-specific: false # don't require nolint directives to be specific about which linter is being skipped 99 | allow-unused: false # report any unused nolint directives 100 | 101 | revive: 102 | severity: error 103 | enable-all-rules: true 104 | rules: 105 | - name: add-constant 106 | disabled: true 107 | - name: cognitive-complexity 108 | arguments: 109 | # lower this after refactoring 110 | - 33 111 | - name: comment-spacings 112 | disabled: true 113 | - name: cyclomatic 114 | arguments: 115 | # lower this after refactoring 116 | - 22 117 | - name: empty-lines 118 | disabled: true 119 | - name: enforce-switch-style 120 | disabled: true 121 | - name: flag-parameter 122 | disabled: true 123 | - name: function-length 124 | arguments: 125 | # lower this after refactoring 126 | - 44 127 | - 126 128 | - name: import-shadowing 129 | disabled: true 130 | - name: line-length-limit 131 | arguments: 132 | # lower this after refactoring 133 | - 142 134 | - name: nested-structs 135 | disabled: true 136 | - name: package-comments 137 | disabled: true 138 | - name: unchecked-type-assertion 139 | disabled: true 140 | - name: useless-break 141 | disabled: true 142 | 143 | staticcheck: 144 | checks: 145 | - all 146 | 147 | exclusions: 148 | presets: 149 | - comments 150 | - common-false-positives 151 | - legacy 152 | - std-error-handling 153 | rules: 154 | 155 | # `err` is often shadowed, we may continue to do it 156 | 157 | - linters: 158 | - govet 159 | text: 'shadow: declaration of "(err|ctx)" shadows declaration' 160 | 161 | paths: 162 | - third_party$ 163 | - builtin$ 164 | - examples$ 165 | 166 | issues: 167 | max-issues-per-linter: 0 168 | max-same-issues: 0 169 | 170 | formatters: 171 | settings: 172 | gci: 173 | sections: 174 | - standard 175 | - default 176 | - prefix(github.com/crowdsecurity) 177 | - prefix(github.com/crowdsecurity/cs-blocklist-mirror) 178 | 179 | exclusions: 180 | paths: 181 | - third_party$ 182 | - builtin$ 183 | - examples$ 184 | -------------------------------------------------------------------------------- /scripts/_bouncer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #shellcheck disable=SC3043 3 | 4 | set -eu 5 | 6 | BOUNCER="crowdsec-blocklist-mirror" 7 | BOUNCER_PREFIX=$(echo "$BOUNCER" | sed 's/crowdsec-/cs-/g') 8 | 9 | # This is a library of functions that can be sourced by other scripts 10 | # to install and configure bouncers. 11 | # 12 | # While not requiring bash, it is not strictly POSIX-compliant because 13 | # it uses local variables, but it should work with every modern shell. 14 | # 15 | # Since passing/parsing arguments in posix sh is tricky, we share 16 | # some environment variables with the functions. It's a matter of 17 | # readability balance between shorter vs cleaner code. 18 | 19 | if [ -n "${NO_COLOR-}" ] || [ ! -t 1 ]; then 20 | # terminal is not interactive; no colors 21 | FG_RED="" 22 | FG_GREEN="" 23 | FG_YELLOW="" 24 | FG_CYAN="" 25 | RESET="" 26 | elif [ -n "${TERM-}" ] && tput sgr0 >/dev/null 2>&1; then 27 | # terminfo 28 | FG_RED=$(tput setaf 1) 29 | FG_GREEN=$(tput setaf 2) 30 | FG_YELLOW=$(tput setaf 3) 31 | FG_CYAN=$(tput setaf 6) 32 | RESET=$(tput sgr0) 33 | else 34 | FG_RED=$(printf '%b' '\033[31m') 35 | FG_GREEN=$(printf '%b' '\033[32m') 36 | FG_YELLOW=$(printf '%b' '\033[33m') 37 | FG_CYAN=$(printf '%b' '\033[36m') 38 | RESET=$(printf '%b' '\033[0m') 39 | fi 40 | 41 | msg() { 42 | case "$1" in 43 | info) echo "${FG_CYAN}$2${RESET}" >&2 ;; 44 | warn) echo "${FG_YELLOW}WARN:${RESET} $2" >&2 ;; 45 | err) echo "${FG_RED}ERR:${RESET} $2" >&2 ;; 46 | succ) echo "${FG_GREEN}$2${RESET}" >&2 ;; 47 | *) echo "$1" >&2 ;; 48 | esac 49 | } 50 | 51 | require() { 52 | set | grep -q "^$1=" || { 53 | msg err "missing required variable \$$1" 54 | exit 1 55 | } 56 | shift 57 | [ "$#" -eq 0 ] || require "$@" 58 | } 59 | 60 | # shellcheck disable=SC2034 61 | { 62 | SERVICE="$BOUNCER.service" 63 | BIN_PATH_INSTALLED="/usr/local/bin/$BOUNCER" 64 | BIN_PATH="./$BOUNCER" 65 | CONFIG_DIR="/etc/crowdsec/bouncers" 66 | CONFIG_FILE="$BOUNCER.yaml" 67 | CONFIG="$CONFIG_DIR/$CONFIG_FILE" 68 | SYSTEMD_PATH_FILE="/etc/systemd/system/$SERVICE" 69 | } 70 | 71 | assert_root() { 72 | #shellcheck disable=SC2312 73 | if [ "$(id -u)" -ne 0 ]; then 74 | msg err "This script must be run as root" 75 | exit 1 76 | fi 77 | } 78 | 79 | # Check if the configuration file contains a variable 80 | # which has not yet been interpolated, like "$API_KEY", 81 | # and return true if it does. 82 | config_not_set() { 83 | require 'CONFIG' 84 | local varname before after 85 | 86 | varname=$1 87 | if [ "$varname" = "" ]; then 88 | msg err "missing required variable name" 89 | exit 1 90 | fi 91 | 92 | before=$("$BOUNCER" -c "$CONFIG" -T) 93 | # shellcheck disable=SC2016 94 | after=$(echo "$before" | envsubst "\$$varname") 95 | 96 | if [ "$before" = "$after" ]; then 97 | return 1 98 | fi 99 | return 0 100 | } 101 | 102 | need_api_key() { 103 | if config_not_set 'API_KEY'; then 104 | return 0 105 | fi 106 | return 1 107 | } 108 | 109 | # Interpolate a variable in the config file with a value. 110 | set_config_var_value() { 111 | require 'CONFIG' 112 | local varname value before 113 | 114 | varname=$1 115 | if [ "$varname" = "" ]; then 116 | msg err "missing required variable name" 117 | exit 1 118 | fi 119 | 120 | value=$2 121 | if [ "$value" = "" ]; then 122 | msg err "missing required variable value" 123 | exit 1 124 | fi 125 | 126 | before=$(cat "$CONFIG") 127 | (umask 177 && echo "$before" | \ 128 | env "$varname=$value" envsubst "\$$varname" >"$CONFIG") 129 | } 130 | 131 | set_api_key() { 132 | require 'CONFIG' 'BOUNCER_PREFIX' 133 | local api_key ret bouncer_id before 134 | # if we can't set the key, the user will take care of it 135 | ret=0 136 | 137 | if command -v cscli >/dev/null; then 138 | echo "cscli/crowdsec is present, generating API key" >&2 139 | bouncer_id="$BOUNCER_PREFIX-$(date +%s)" 140 | api_key=$(cscli -oraw bouncers add "$bouncer_id" || true) 141 | if [ "$api_key" = "" ]; then 142 | echo "failed to create API key" >&2 143 | api_key="" 144 | ret=1 145 | else 146 | echo "API Key successfully created" >&2 147 | echo "$bouncer_id" >"$CONFIG.id" 148 | fi 149 | else 150 | echo "cscli/crowdsec is not present, please set the API key manually" >&2 151 | api_key="" 152 | ret=1 153 | fi 154 | 155 | if [ "$api_key" != "" ]; then 156 | set_config_var_value 'API_KEY' "$api_key" 157 | fi 158 | 159 | return "$ret" 160 | } 161 | 162 | set_local_port() { 163 | require 'CONFIG' 164 | local port 165 | command -v cscli >/dev/null || return 0 166 | # the following will fail with a non-LAPI local crowdsec, leaving empty port 167 | port=$(cscli config show -oraw --key "Config.API.Server.ListenURI" 2>/dev/null | cut -d ":" -f2 || true) 168 | if [ "$port" != "" ]; then 169 | sed -i "s/localhost:8080/127.0.0.1:$port/g" "$CONFIG" 170 | sed -i "s/127.0.0.1:8080/127.0.0.1:$port/g" "$CONFIG" 171 | fi 172 | } 173 | 174 | set_local_lapi_url() { 175 | require 'CONFIG' 176 | local port before varname 177 | # $varname is the name of the variable to interpolate 178 | # in the config file with the URL of the LAPI server, 179 | # assuming it is running on the same host as the 180 | # bouncer. 181 | varname=$1 182 | if [ "$varname" = "" ]; then 183 | msg err "missing required variable name" 184 | exit 1 185 | fi 186 | command -v cscli >/dev/null || return 0 187 | 188 | port=$(cscli config show -oraw --key "Config.API.Server.ListenURI" 2>/dev/null | cut -d ":" -f2 || true) 189 | if [ "$port" = "" ]; then 190 | port=8080 191 | fi 192 | 193 | set_config_var_value "$varname" "http://127.0.0.1:$port" 194 | } 195 | 196 | delete_bouncer() { 197 | require 'CONFIG' 198 | local bouncer_id 199 | if [ -f "$CONFIG.id" ]; then 200 | bouncer_id=$(cat "$CONFIG.id") 201 | cscli -oraw bouncers delete "$bouncer_id" 2>/dev/null || true 202 | rm -f "$CONFIG.id" 203 | fi 204 | } 205 | 206 | upgrade_bin() { 207 | require 'BIN_PATH' 'BIN_PATH_INSTALLED' 208 | rm "$BIN_PATH_INSTALLED" 209 | install -v -m 0755 -D "$BIN_PATH" "$BIN_PATH_INSTALLED" 210 | } 211 | -------------------------------------------------------------------------------- /pkg/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "compress/gzip" 5 | "context" 6 | "encoding/base64" 7 | "errors" 8 | "fmt" 9 | "net" 10 | "net/http" 11 | "strings" 12 | "time" 13 | 14 | "github.com/prometheus/client_golang/prometheus" 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | log "github.com/sirupsen/logrus" 17 | "golang.org/x/sync/errgroup" 18 | 19 | "github.com/crowdsecurity/cs-blocklist-mirror/pkg/cfg" 20 | "github.com/crowdsecurity/cs-blocklist-mirror/pkg/formatters" 21 | "github.com/crowdsecurity/cs-blocklist-mirror/pkg/registry" 22 | ) 23 | 24 | var BlocklistMirrorAccessLogFilePath = "crowdsec-blocklist-mirror_access.log" 25 | 26 | func RunServer(ctx context.Context, g *errgroup.Group, config cfg.Config) error { 27 | for _, blockListCFG := range config.Blocklists { 28 | f, err := getHandlerForBlockList(blockListCFG) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | http.HandleFunc(blockListCFG.Endpoint, f) 34 | log.Infof("serving blocklist in format %s at endpoint %s", blockListCFG.Format, blockListCFG.Endpoint) 35 | } 36 | 37 | if config.Metrics.Enabled { 38 | prometheus.MustRegister(RouteHits) 39 | log.Infof("Enabling metrics at endpoint '%s' ", config.Metrics.Endpoint) 40 | http.Handle(config.Metrics.Endpoint, promhttp.Handler()) 41 | } 42 | 43 | var logHandler http.Handler 44 | 45 | if config.EnableAccessLogs { 46 | logger, err := config.Logging.LoggerForFile(BlocklistMirrorAccessLogFilePath) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | logHandler = CombinedLoggingHandler(logger, http.DefaultServeMux) 52 | } 53 | 54 | server := &http.Server{ 55 | Addr: config.ListenURI, 56 | Handler: logHandler, 57 | } 58 | 59 | g.Go(func() error { 60 | err := listenAndServe(server, config) 61 | if err != nil && !errors.Is(err, http.ErrServerClosed) { 62 | return err 63 | } 64 | 65 | return nil 66 | }) 67 | 68 | <-ctx.Done() 69 | 70 | serverCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 71 | defer cancel() 72 | server.Shutdown(serverCtx) //nolint: contextcheck 73 | 74 | return nil 75 | } 76 | 77 | func listenAndServe(server *http.Server, config cfg.Config) error { 78 | if config.TLS.CertFile != "" && config.TLS.KeyFile != "" { 79 | log.Infof("Starting server with TLS at %s", config.ListenURI) 80 | return server.ListenAndServeTLS(config.TLS.CertFile, config.TLS.KeyFile) 81 | } 82 | 83 | log.Infof("Starting server at %s", config.ListenURI) 84 | 85 | return server.ListenAndServe() 86 | } 87 | 88 | var RouteHits = prometheus.NewCounterVec( 89 | prometheus.CounterOpts{ 90 | Name: "blocklist_requests_total", 91 | Help: "Number of calls to each blocklist", 92 | }, 93 | []string{"route"}, 94 | ) 95 | 96 | // var apiCountersByFormatName map[string]prometheus. = map[string]prometheus.Counter{ 97 | // "plain_text": promauto.NewCounter(prometheus.CounterOpts{ 98 | // Name: "total_api_calls_for_plain_text", 99 | // Help: "Total number of times blocklist in plain_text format was requested", 100 | // }), 101 | // } 102 | 103 | func basicAuth(username, password string) string { 104 | auth := username + ":" + password 105 | return base64.StdEncoding.EncodeToString([]byte(auth)) 106 | } 107 | 108 | func satisfiesBasicAuth(r *http.Request, user, password string) bool { 109 | expectedVal := "Basic " + basicAuth(user, password) 110 | foundVal := r.Header.Get("Authorization") 111 | return expectedVal == foundVal 112 | } 113 | 114 | func toValidCIDR(ip string) string { 115 | if strings.Contains(ip, "/") { 116 | return ip 117 | } 118 | 119 | if strings.Contains(ip, ":") { 120 | return ip + "/128" 121 | } 122 | 123 | return ip + "/32" 124 | } 125 | 126 | func getTrustedIPs(ips []string) ([]net.IPNet, error) { 127 | trustedIPs := make([]net.IPNet, 0) 128 | 129 | for _, ip := range ips { 130 | cidr := toValidCIDR(ip) 131 | 132 | _, ipNet, err := net.ParseCIDR(cidr) 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | trustedIPs = append(trustedIPs, *ipNet) 138 | } 139 | 140 | return trustedIPs, nil 141 | } 142 | 143 | func networksContainIP(networks []net.IPNet, ip string) bool { 144 | parsedIP := net.ParseIP(ip) 145 | for _, network := range networks { 146 | if network.Contains(parsedIP) { 147 | return true 148 | } 149 | } 150 | 151 | return false 152 | } 153 | 154 | func metricsMiddleware(blockListCfg *cfg.BlockListConfig, next http.HandlerFunc) func(w http.ResponseWriter, r *http.Request) { 155 | return func(w http.ResponseWriter, r *http.Request) { 156 | RouteHits.WithLabelValues( 157 | blockListCfg.Endpoint, 158 | ).Inc() 159 | next.ServeHTTP(w, r) 160 | } 161 | } 162 | 163 | func decisionMiddleware(next http.HandlerFunc) func(w http.ResponseWriter, r *http.Request) { 164 | return func(w http.ResponseWriter, r *http.Request) { 165 | decisions := registry.GlobalDecisionRegistry.GetActiveDecisions(r.URL.Query()) 166 | if len(decisions) == 0 { 167 | http.Error(w, "no decisions available", http.StatusNotFound) 168 | return 169 | } 170 | 171 | ctx := context.WithValue(r.Context(), registry.GlobalDecisionRegistry.Key, decisions) 172 | next.ServeHTTP(w, r.WithContext(ctx)) 173 | } 174 | } 175 | 176 | func authMiddleware(blockListCfg *cfg.BlockListConfig, next http.HandlerFunc) func(w http.ResponseWriter, r *http.Request) { 177 | return func(w http.ResponseWriter, r *http.Request) { 178 | ip, _, err := net.SplitHostPort(r.RemoteAddr) 179 | if err != nil { 180 | log.Errorf("error while spliting hostport for %s: %v", r.RemoteAddr, err) 181 | http.Error(w, "internal error", http.StatusInternalServerError) 182 | 183 | return 184 | } 185 | 186 | trustedIPs, err := getTrustedIPs(blockListCfg.Authentication.TrustedIPs) 187 | if err != nil { 188 | log.Errorf("error while parsing trusted IPs: %v", err) 189 | http.Error(w, "internal error", http.StatusInternalServerError) 190 | 191 | return 192 | } 193 | 194 | switch strings.ToLower(blockListCfg.Authentication.Type) { 195 | case "ip_based": 196 | if !networksContainIP(trustedIPs, ip) { 197 | http.Error(w, "access denied", http.StatusForbidden) 198 | return 199 | } 200 | case "basic": 201 | if r.Header.Get("Authorization") == "" { 202 | w.Header().Set("WWW-Authenticate", "Basic realm=\"crowdsec-blocklist-mirror\"") 203 | http.Error(w, "access denied", http.StatusUnauthorized) 204 | return 205 | } 206 | if !satisfiesBasicAuth(r, blockListCfg.Authentication.User, blockListCfg.Authentication.Password) { 207 | http.Error(w, "access denied", http.StatusForbidden) 208 | return 209 | } 210 | case "", "none": 211 | } 212 | 213 | next.ServeHTTP(w, r) 214 | } 215 | } 216 | 217 | // gzipResponseWriter wraps http.ResponseWriter and gzip.Writer 218 | type gzipResponseWriter struct { 219 | http.ResponseWriter 220 | gz *gzip.Writer 221 | } 222 | 223 | func (w *gzipResponseWriter) Write(b []byte) (int, error) { 224 | return w.gz.Write(b) 225 | } 226 | 227 | // gzipMiddleware checks for gzip support and wraps the response if needed 228 | func gzipMiddleware(next http.HandlerFunc) http.HandlerFunc { 229 | return func(w http.ResponseWriter, r *http.Request) { 230 | // Check if client accepts gzip encoding 231 | if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { 232 | // Set appropriate headers 233 | w.Header().Set("Content-Encoding", "gzip") 234 | 235 | // Create gzip writer 236 | gz := gzip.NewWriter(w) 237 | defer gz.Close() 238 | 239 | // Wrap the response writer 240 | grw := &gzipResponseWriter{ 241 | ResponseWriter: w, 242 | gz: gz, 243 | } 244 | 245 | next.ServeHTTP(grw, r) 246 | return 247 | } 248 | 249 | // Fall back to normal response writer 250 | next.ServeHTTP(w, r) 251 | } 252 | } 253 | 254 | func getHandlerForBlockList(blockListCfg *cfg.BlockListConfig) (func(w http.ResponseWriter, r *http.Request), error) { 255 | if _, ok := formatters.ByName[blockListCfg.Format]; !ok { 256 | return nil, fmt.Errorf("unknown format %s", blockListCfg.Format) 257 | } 258 | 259 | return gzipMiddleware( 260 | authMiddleware(blockListCfg, 261 | metricsMiddleware(blockListCfg, 262 | decisionMiddleware(formatters.ByName[blockListCfg.Format])))), nil 263 | } 264 | -------------------------------------------------------------------------------- /pkg/server/logging.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package server 6 | 7 | import ( 8 | "bufio" 9 | "io" 10 | "net" 11 | "net/http" 12 | "net/url" 13 | "strconv" 14 | "time" 15 | "unicode/utf8" 16 | 17 | "github.com/felixge/httpsnoop" 18 | ) 19 | 20 | // responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP 21 | // status code and body size 22 | type responseLogger struct { 23 | w http.ResponseWriter 24 | status int 25 | size int 26 | } 27 | 28 | func (l *responseLogger) Write(b []byte) (int, error) { 29 | size, err := l.w.Write(b) 30 | l.size += size 31 | 32 | return size, err 33 | } 34 | 35 | func (l *responseLogger) WriteHeader(s int) { 36 | l.w.WriteHeader(s) 37 | l.status = s 38 | } 39 | 40 | func (l *responseLogger) Status() int { 41 | return l.status 42 | } 43 | 44 | func (l *responseLogger) Size() int { 45 | return l.size 46 | } 47 | 48 | func (l *responseLogger) Hijack() (net.Conn, *bufio.ReadWriter, error) { 49 | conn, rw, err := l.w.(http.Hijacker).Hijack() 50 | if err == nil && l.status == 0 { 51 | // The status will be StatusSwitchingProtocols if there was no error and 52 | // WriteHeader has not been called yet 53 | l.status = http.StatusSwitchingProtocols 54 | } 55 | 56 | return conn, rw, err 57 | } 58 | 59 | // Logging 60 | 61 | // LogFormatterParams is the structure any formatter will be handed when time to log comes 62 | type LogFormatterParams struct { 63 | Request *http.Request 64 | URL url.URL 65 | TimeStamp time.Time 66 | StatusCode int 67 | Size int 68 | } 69 | 70 | // LogFormatter gives the signature of the formatter function passed to CustomLoggingHandler 71 | type LogFormatter func(writer io.Writer, params LogFormatterParams) 72 | 73 | // loggingHandler is the http.Handler implementation for LoggingHandlerTo and its 74 | // friends 75 | 76 | type loggingHandler struct { 77 | writer io.Writer 78 | handler http.Handler 79 | formatter LogFormatter 80 | } 81 | 82 | func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 83 | t := time.Now() 84 | logger, w := makeLogger(w) 85 | url := *req.URL 86 | 87 | h.handler.ServeHTTP(w, req) 88 | 89 | if req.MultipartForm != nil { 90 | req.MultipartForm.RemoveAll() 91 | } 92 | 93 | params := LogFormatterParams{ 94 | Request: req, 95 | URL: url, 96 | TimeStamp: t, 97 | StatusCode: logger.Status(), 98 | Size: logger.Size(), 99 | } 100 | 101 | h.formatter(h.writer, params) 102 | } 103 | 104 | func makeLogger(w http.ResponseWriter) (*responseLogger, http.ResponseWriter) { 105 | logger := &responseLogger{w: w, status: http.StatusOK} 106 | 107 | return logger, httpsnoop.Wrap(w, httpsnoop.Hooks{ 108 | Write: func(httpsnoop.WriteFunc) httpsnoop.WriteFunc { 109 | return logger.Write 110 | }, 111 | WriteHeader: func(httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc { 112 | return logger.WriteHeader 113 | }, 114 | }) 115 | } 116 | 117 | const lowerhex = "0123456789abcdef" 118 | 119 | func appendQuoted(buf []byte, s string) []byte { 120 | var runeTmp [utf8.UTFMax]byte 121 | //nolint:wastedassign 122 | for width := 0; s != ""; s = s[width:] { 123 | r := rune(s[0]) 124 | width = 1 125 | 126 | if r >= utf8.RuneSelf { 127 | r, width = utf8.DecodeRuneInString(s) 128 | } 129 | 130 | if width == 1 && r == utf8.RuneError { 131 | buf = append(buf, `\x`...) 132 | buf = append(buf, lowerhex[s[0]>>4]) 133 | buf = append(buf, lowerhex[s[0]&0xF]) 134 | 135 | continue 136 | } 137 | 138 | if r == rune('"') || r == '\\' { // always backslashed 139 | buf = append(buf, '\\') 140 | buf = append(buf, byte(r)) 141 | 142 | continue 143 | } 144 | 145 | if strconv.IsPrint(r) { 146 | n := utf8.EncodeRune(runeTmp[:], r) 147 | buf = append(buf, runeTmp[:n]...) 148 | 149 | continue 150 | } 151 | 152 | switch r { 153 | case '\a': 154 | buf = append(buf, `\a`...) 155 | case '\b': 156 | buf = append(buf, `\b`...) 157 | case '\f': 158 | buf = append(buf, `\f`...) 159 | case '\n': 160 | buf = append(buf, `\n`...) 161 | case '\r': 162 | buf = append(buf, `\r`...) 163 | case '\t': 164 | buf = append(buf, `\t`...) 165 | case '\v': 166 | buf = append(buf, `\v`...) 167 | default: 168 | switch { 169 | case r < ' ': 170 | buf = append(buf, `\x`...) 171 | buf = append(buf, lowerhex[s[0]>>4]) 172 | buf = append(buf, lowerhex[s[0]&0xF]) 173 | case r > utf8.MaxRune: 174 | r = 0xFFFD 175 | fallthrough 176 | case r < 0x10000: 177 | buf = append(buf, `\u`...) 178 | for s := 12; s >= 0; s -= 4 { 179 | buf = append(buf, lowerhex[r>>uint(s)&0xF]) 180 | } 181 | default: 182 | buf = append(buf, `\U`...) 183 | for s := 28; s >= 0; s -= 4 { 184 | buf = append(buf, lowerhex[r>>uint(s)&0xF]) 185 | } 186 | } 187 | } 188 | } 189 | 190 | return buf 191 | } 192 | 193 | // buildCommonLogLine builds a log entry for req in Apache Common Log Format. 194 | // ts is the timestamp with which the entry should be logged. 195 | // status and size are used to provide the response HTTP status and size. 196 | func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int, size int) []byte { 197 | username := "-" 198 | 199 | if url.User != nil { 200 | if name := url.User.Username(); name != "" { 201 | username = name 202 | } 203 | } 204 | 205 | host, _, err := net.SplitHostPort(req.RemoteAddr) 206 | if err != nil { 207 | host = req.RemoteAddr 208 | } 209 | 210 | uri := req.RequestURI 211 | 212 | // Requests using the CONNECT method over HTTP/2.0 must use 213 | // the authority field (aka r.Host) to identify the target. 214 | // Refer: https://httpwg.github.io/specs/rfc7540.html#CONNECT 215 | if req.ProtoMajor == 2 && req.Method == http.MethodConnect { 216 | uri = req.Host 217 | } 218 | 219 | if uri == "" { 220 | uri = url.RequestURI() 221 | } 222 | 223 | buf := make([]byte, 0, 3*(len(host)+len(username)+len(req.Method)+len(uri)+len(req.Proto)+50)/2) 224 | buf = append(buf, host...) 225 | buf = append(buf, " - "...) 226 | buf = append(buf, username...) 227 | buf = append(buf, " ["...) 228 | buf = append(buf, ts.Format("02/Jan/2006:15:04:05 -0700")...) 229 | buf = append(buf, `] "`...) 230 | buf = append(buf, req.Method...) 231 | buf = append(buf, " "...) 232 | buf = appendQuoted(buf, uri) 233 | buf = append(buf, " "...) 234 | buf = append(buf, req.Proto...) 235 | buf = append(buf, `" `...) 236 | buf = append(buf, strconv.Itoa(status)...) 237 | buf = append(buf, " "...) 238 | buf = append(buf, strconv.Itoa(size)...) 239 | 240 | return buf 241 | } 242 | 243 | // writeLog writes a log entry for req to w in Apache Common Log Format. 244 | // ts is the timestamp with which the entry should be logged. 245 | // status and size are used to provide the response HTTP status and size. 246 | func writeLog(writer io.Writer, params LogFormatterParams) { 247 | buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size) 248 | buf = append(buf, '\n') 249 | writer.Write(buf) 250 | } 251 | 252 | // writeCombinedLog writes a log entry for req to w in Apache Combined Log Format. 253 | // ts is the timestamp with which the entry should be logged. 254 | // status and size are used to provide the response HTTP status and size. 255 | func writeCombinedLog(writer io.Writer, params LogFormatterParams) { 256 | buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size) 257 | buf = append(buf, ` "`...) 258 | buf = appendQuoted(buf, params.Request.Referer()) 259 | buf = append(buf, `" "`...) 260 | buf = appendQuoted(buf, params.Request.UserAgent()) 261 | buf = append(buf, '"', '\n') 262 | writer.Write(buf) 263 | } 264 | 265 | // CombinedLoggingHandler return a http.Handler that wraps h and logs requests to out in 266 | // Apache Combined Log Format. 267 | // 268 | // See http://httpd.apache.org/docs/2.2/logs.html#combined for a description of this format. 269 | // 270 | // LoggingHandler always sets the ident field of the log to - 271 | func CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler { 272 | return loggingHandler{out, h, writeCombinedLog} 273 | } 274 | 275 | // LoggingHandler return a http.Handler that wraps h and logs requests to out in 276 | // Apache Common Log Format (CLF). 277 | // 278 | // See http://httpd.apache.org/docs/2.2/logs.html#common for a description of this format. 279 | // 280 | // LoggingHandler always sets the ident field of the log to - 281 | // 282 | // Example: 283 | // 284 | // r := mux.NewRouter() 285 | // r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 286 | // w.Write([]byte("This is a catch-all route")) 287 | // }) 288 | // loggedRouter := handlers.LoggingHandler(os.Stdout, r) 289 | // http.ListenAndServe(":1123", loggedRouter) 290 | func LoggingHandler(out io.Writer, h http.Handler) http.Handler { 291 | return loggingHandler{out, h, writeLog} 292 | } 293 | 294 | // CustomLoggingHandler provides a way to supply a custom log formatter 295 | // while taking advantage of the mechanisms in this package 296 | func CustomLoggingHandler(out io.Writer, h http.Handler, f LogFormatter) http.Handler { 297 | return loggingHandler{out, h, f} 298 | } 299 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # blocklist-mirror 2 | 3 | ## Installation 4 | 5 | 1. Create a config file, `cfg.yaml` with the contents below: 6 | 7 | ```yaml 8 | config_version: v1.0 9 | crowdsec_config: 10 | lapi_key: ${API_KEY} 11 | lapi_url: http://127.0.0.1:8080/ 12 | update_frequency: 10s 13 | include_scenarios_containing: [] 14 | exclude_scenarios_containing: [] 15 | only_include_decisions_from: [] 16 | insecure_skip_verify: false 17 | 18 | blocklists: 19 | - format: plain_text # Supported formats are either of "plain_text", "mikrotik", "juniper" 20 | endpoint: /security/blocklist 21 | authentication: 22 | type: none # Supported types are either of "none", "ip_based", "basic" 23 | user: 24 | password: 25 | trusted_ips: # IP ranges, or IPs which don't require auth to access this blocklist 26 | - 127.0.0.1 27 | - ::1 28 | 29 | listen_uri: 0.0.0.0:41412 30 | tls: 31 | cert_file: 32 | key_file: 33 | 34 | metrics: 35 | enabled: true 36 | endpoint: /metrics 37 | 38 | log_media: stdout 39 | log_level: info 40 | ``` 41 | 42 | Please find the full config reference below. 43 | 44 | 2. Set the `lapi_key` and `lapi_url`. The LAPI must be accessible from the docker container. 45 | 46 | `lapi_key` can be obtained by running the following on the machine running LAPI: 47 | ```bash 48 | sudo cscli -oraw bouncers add blocklistMirror 49 | ``` 50 | 51 | 3. Modify the blocklists section as required. 52 | 53 | Run the image with the config file mounted and port mapped as desired: 54 | ```bash 55 | docker run \ 56 | -v $PWD/cfg.yaml:/etc/crowdsec/bouncers/crowdsec-blocklist-mirror.yaml \ 57 | -p 41412:41412 \ 58 | crowdsecurity/blocklist-mirror 59 | ``` 60 | 61 | 4. If you want to enable TLS, then set `cert_file` and `key_file` config. While running the container mount these from host to the provided path. 62 | 63 | ## Configuration Reference 64 | 65 | ### `crowdsec_config` 66 | 67 | | Parameter | Description | 68 | |--------------------------------|-----------------------------------------------------------------------------------------------------------------| 69 | | `lapi_url` | The URL of CrowdSec LAPI. It should be accessible from whichever network the bouncer has access. | 70 | | `lapi_key` | It can be obtained by running the following on the machine CrowdSec LAPI is deployed on. | 71 | | `update_frequency` | The bouncer will poll the CrowdSec every `update_frequency` interval. | 72 | | `include_scenarios_containing` | Ignore IPs banned for triggering scenarios not containing either of the provided words. | 73 | | `exclude_scenarios_containing` | Ignore IPs banned for triggering scenarios containing either of the provided words. | 74 | | `only_include_decisions_from` | Only include IPs banned due to decisions originating from provided sources. e.g., value `["cscli", "crowdsec"]` | 75 | | `insecure_skip_verify` | Set to true to skip verifying the certificate. | 76 | | `listen_uri` | Location where the mirror will start the server. | 77 | 78 | ### `tls_config` 79 | 80 | | Parameter | Description | 81 | |-------------|---------------------------------------------------------| 82 | | `cert_file` | Path to the certificate to use if TLS is to be enabled. | 83 | | `key_file` | Path to the certificate key file. | 84 | 85 | ### `metrics` 86 | 87 | | Parameter | Description | 88 | |------------|-----------------------------------------------------------------------------| 89 | | `enabled` | Boolean (true/false). Set to true to enable serving and collecting metrics. | 90 | | `endpoint` | Endpoint to serve the metrics on. | 91 | 92 | ### `blocklists` 93 | 94 | Each blocklist has the following configuration: 95 | 96 | | Parameter | Description | 97 | |------------------|-------------------------------------------------------------------------------------| 98 | | `format` | Format of the blocklist. Currently, only `plain_text` and `mikrotik` are supported. | 99 | | `endpoint` | Endpoint to serve the blocklist on. | 100 | | `authentication` | Authentication related config. See the table below for `authentication` parameters. | 101 | 102 | #### `authentication` 103 | 104 | | Parameter | Description | 105 | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| 106 | | `type` | Authentication type. Currently "basic" and "ip_based" authentication are supported. You can disable authentication completely by setting this to 'none'. | 107 | | `user` | Valid username if using `basic` authentication. | 108 | | `password` | Password for the provided user when using `basic` authentication. | 109 | | `trusted_ips` | List of valid IPv4 and IPv6 IPs and ranges which have access to blocklist. Only applicable when authentication `type` is `ip_based`. | 110 | 111 | ## Global RunTime Query Parameters 112 | 113 | | Parameter | Description | Requires Value | Example Usage | 114 | |------------|-------------------------------------------------------------------------------------------------------------------|----------------|----------------------------------------------------------| 115 | | `ipv4only` | Only return IPv4 addresses | No | `http://localhost:41412/security/blocklist?ipv4only` | 116 | | `ipv6only` | Only return IPv6 addresses | No | `http://localhost:41412/security/blocklist?ipv6only` | 117 | | `nosort` | Do not sort IPs. Only use if you do not care about the sorting of the list; can result in average 1ms improvement | No | `http://localhost:41412/security/blocklist?nosort` | 118 | | `origin` | Only return IPs by origin | Yes | `http://localhost:41412/security/blocklist?origin=cscli` | 119 | 120 | ## Formats 121 | 122 | The bouncer can expose the blocklist in the following formats. You can configure the format of the blocklist by setting its `format` parameter to any of the supported formats described below. 123 | 124 | ### plain_text 125 | 126 | Example: 127 | ```text 128 | 1.2.3.4 129 | 4.3.2.1 130 | ``` 131 | 132 | ### mikrotik 133 | 134 | Generates a MikroTik Script that the device can execute to populate the specified firewall address list. 135 | 136 | #### MikroTik query parameters 137 | 138 | | Parameter | Description | 139 | |----------------|--------------------------------------------------------------------------| 140 | | `listname=foo` | Set the list name to `foo`. By default, `listname` is set to `CrowdSec`. | 141 | 142 | Example output: 143 | ```bash 144 | /ip/firewall/address-list/remove [ find where list="foo" ]; 145 | :global CrowdSecAddIP; 146 | :set CrowdSecAddIP do={ 147 | :do { /ip/firewall/address-list/add list=foo address=$1 comment="$2" timeout=$3; } on-error={ } 148 | } 149 | $CrowdSecAddIP 1.2.3.4 "ssh-bf" 152h40m24s 150 | $CrowdSecAddIP 4.3.2.1 "postfix-spam" 166h40m25s 151 | $CrowdSecAddIP 2001:470:1:c84::17 "ssh-bf" 165h13m42s 152 | ``` 153 | 154 | #### Example: MikroTik import script 155 | 156 | Using on device [MikroTik scripting](https://help.mikrotik.com/docs/display/ROS/Scripting) following is a starting point to download and import the blocklist. Ensure to adjust the [global query parameters](#global-runtime-query-parameters) according to your needs! 157 | 158 | ```bash 159 | :local name "[crowdsec]" 160 | :local url "http://:41412/security/blocklist?ipv4only&nosort" 161 | :local fileName "blocklist.rsc" 162 | :log info "$name fetch blocklist from $url" 163 | /tool fetch url="$url" mode=http dst-path=$fileName 164 | :if ([:len [/file find name=$fileName]] > 0) do={ 165 | :log info "$name import;start" 166 | /import file-name=$fileName 167 | :log info "$name import:done" 168 | } else={ 169 | :log error "$name failed to fetch the blocklist" 170 | } 171 | ``` 172 | 173 | ### Juniper SRX 174 | 175 | Generates a .txt file with all IP addresses (single host and subnets) in the CIDR notation format supported by the Juniper Networks SRX firewall platform. 176 | 177 | Example: 178 | ```text 179 | 1.2.3.4/32 180 | 4.3.2.1/32 181 | ``` 182 | 183 | #### SRX Dynamic Address configuration sample 184 | 185 | Using the blocklist on a Juniper SRX requires that the published url ends in .txt. This can be acieved by altering the endpoint config in `cfg.yaml` as follows: 186 | 187 | Sample `cfg.yaml` 188 | ```yaml 189 | #### 190 | blocklists: 191 | - format: plain_text # Supported formats are either of "plain_text", "mikrotik", "juniper 192 | endpoint: /security/blocklist.txt #Modify to .txt for juniper formatter. 193 | authentication: 194 | type: none # Supported types are either of "none", "ip_based", "basic" 195 | user: 196 | password: 197 | trusted_ips: # IP ranges, or IPs which don't require auth to access this blocklist 198 | - 127.0.0.1 199 | - ::1 200 | #### 201 | ``` 202 | 203 | This can then be configured on the SRX firewall as follows: 204 | 205 | Sample SRX config: 206 | ```test 207 | user@srx> show configuration security dynamic-address | display set 208 | 209 | set security dynamic-address feed-server crowdsec url http://192.168.1.2:41412 210 | set security dynamic-address feed-server crowdsec update-interval 30 211 | set security dynamic-address feed-server crowdsec feed-name crowdsec path /security/blocklist.txt 212 | set security dynamic-address address-name crowdsec-blocklist profile feed-name crowdsec 213 | ``` 214 | Further information here: https://www.juniper.net/documentation/us/en/software/junos/cli-reference/topics/ref/statement/dynamic-address.html 215 | 216 | A successful configuration should return a similar result when queried: 217 | 218 | ```text 219 | user@srx> show security dynamic-address summary 220 | 221 | 222 | Dynamic-address session scan status : Disable 223 | Hold-interval for dynamic-address session scan : 10 seconds 224 | 225 | 226 | Server Name : crowdsec 227 | Hostname/IP : http://192.168.1.2:41412 228 | Update interval : 30 229 | Hold interval : 86400 230 | TLS Profile Name : --- 231 | User Name : --- 232 | 233 | 234 | Feed Name : crowdsec 235 | Mapped dynamic address name : crowdsec-blocklist 236 | URL : http://192.168.1.2:41412/security/blocklist.txt 237 | Feed update interval : 30 Feed hold interval :86400 238 | Total update : 16310 239 | Total IPv4 entries : 16240 240 | Total IPv6 entries : 0 241 | ``` 242 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= 2 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 3 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 | github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= 6 | github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 7 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 8 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 | github.com/corazawaf/coraza/v3 v3.3.3 h1:kqjStHAgWqwP5dh7n0vhTOF0a3t+VikNS/EaMiG0Fhk= 10 | github.com/corazawaf/coraza/v3 v3.3.3/go.mod h1:xSaXWOhFMSbrV8qOOfBKAyw3aOqfwaSaOy5BgSF8XlA= 11 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 12 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 13 | github.com/crowdsecurity/crowdsec v1.7.3 h1:UYAs2OJMclv8JoHC3s/x33fHjAUsXnI41ZYmAakh/cU= 14 | github.com/crowdsecurity/crowdsec v1.7.3/go.mod h1:m2LcM8LtBIN+6Ifs2Ib21bmH/B49AZJhHwPLAxcvKr4= 15 | github.com/crowdsecurity/go-cs-bouncer v0.0.19 h1:N4B6dz00IIapTM1nPhHYklbWdbxPivcD/FoD7VYcfwc= 16 | github.com/crowdsecurity/go-cs-bouncer v0.0.19/go.mod h1:Mq3KnK44qG+UIVKuQP4gxCOY6uiylTZ8Ty3pCAkupKg= 17 | github.com/crowdsecurity/go-cs-lib v0.0.23 h1:9YPJG97uXZh95uwvPWeFeAZPXjZ7HaUdtcyFCLSYxu8= 18 | github.com/crowdsecurity/go-cs-lib v0.0.23/go.mod h1:X0GMJY2CxdA1S09SpuqIKaWQsvRGxXmecUp9cP599dE= 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 22 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= 24 | github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 25 | github.com/expr-lang/expr v1.17.5 h1:i1WrMvcdLF249nSNlpQZN1S6NXuW9WaOfF5tPi3aw3k= 26 | github.com/expr-lang/expr v1.17.5/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= 27 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 28 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 29 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 30 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 31 | github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= 32 | github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= 33 | github.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrYnZg= 34 | github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= 35 | github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= 36 | github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= 37 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 38 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 39 | github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= 40 | github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= 41 | github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= 42 | github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= 43 | github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= 44 | github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= 45 | github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= 46 | github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 47 | github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= 48 | github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= 49 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 50 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 51 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 52 | github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= 53 | github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 54 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 55 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 56 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 57 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 58 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 59 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 60 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 61 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 62 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 63 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 64 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 65 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 66 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 67 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 68 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 69 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 70 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 71 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 72 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 73 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 74 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= 75 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 76 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 77 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 78 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 79 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 80 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 81 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 82 | github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= 83 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 84 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 85 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 86 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 87 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= 88 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 89 | github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 90 | github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 91 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 92 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 93 | github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= 94 | github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= 95 | github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= 96 | github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= 97 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 98 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 99 | github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970= 100 | github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI= 101 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 102 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 103 | github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= 104 | github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= 105 | github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= 106 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 107 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 108 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 109 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 110 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 111 | github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= 112 | github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= 113 | github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= 114 | github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= 115 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 116 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 117 | go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= 118 | go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= 119 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 120 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 121 | go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= 122 | go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 123 | golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= 124 | golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= 125 | golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= 126 | golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 127 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 128 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 129 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 130 | golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= 131 | golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 132 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 133 | google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= 134 | google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 135 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 136 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 137 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 138 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 139 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 140 | gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs= 141 | gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk= 142 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 143 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 144 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 145 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 146 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 147 | -------------------------------------------------------------------------------- /test/uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 3 3 | requires-python = ">=3.12" 4 | 5 | [[package]] 6 | name = "asttokens" 7 | version = "3.0.0" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" }, 12 | ] 13 | 14 | [[package]] 15 | name = "basedpyright" 16 | version = "1.31.2" 17 | source = { registry = "https://pypi.org/simple" } 18 | dependencies = [ 19 | { name = "nodejs-wheel-binaries" }, 20 | ] 21 | sdist = { url = "https://files.pythonhosted.org/packages/74/32/561d61dc99789b999b86f5e8683658ea7d096b16d2886aacffb3482ab637/basedpyright-1.31.2.tar.gz", hash = "sha256:dd18ed85770f80723d4378b0a0f05f24ef205b71ba4b525242abf1782ed16d8f", size = 22068420, upload-time = "2025-08-13T14:05:41.28Z" } 22 | wheels = [ 23 | { url = "https://files.pythonhosted.org/packages/46/70/96e39d0724a08622a248ddc8dfd56c1cf3465b5aaeff414dc39ba7b679ee/basedpyright-1.31.2-py3-none-any.whl", hash = "sha256:b3541fba56a69de826f77a15f8b864648d1cfbcb11a3ca530d82982e65e78d19", size = 11540670, upload-time = "2025-08-13T14:05:38.631Z" }, 24 | ] 25 | 26 | [[package]] 27 | name = "certifi" 28 | version = "2025.8.3" 29 | source = { registry = "https://pypi.org/simple" } 30 | sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } 31 | wheels = [ 32 | { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, 33 | ] 34 | 35 | [[package]] 36 | name = "cffi" 37 | version = "1.17.1" 38 | source = { registry = "https://pypi.org/simple" } 39 | dependencies = [ 40 | { name = "pycparser" }, 41 | ] 42 | sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } 43 | wheels = [ 44 | { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, 45 | { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, 46 | { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, 47 | { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, 48 | { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, 49 | { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, 50 | { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, 51 | { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, 52 | { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, 53 | { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, 54 | { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, 55 | { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, 56 | { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, 57 | { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, 58 | { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, 59 | { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, 60 | { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, 61 | { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, 62 | { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, 63 | { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, 64 | { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, 65 | { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, 66 | ] 67 | 68 | [[package]] 69 | name = "charset-normalizer" 70 | version = "3.4.3" 71 | source = { registry = "https://pypi.org/simple" } 72 | sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } 73 | wheels = [ 74 | { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, 75 | { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, 76 | { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, 77 | { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, 78 | { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, 79 | { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, 80 | { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, 81 | { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, 82 | { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, 83 | { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, 84 | { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, 85 | { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, 86 | { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, 87 | { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, 88 | { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, 89 | { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, 90 | { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, 91 | { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, 92 | { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, 93 | { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, 94 | { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, 95 | { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, 96 | { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, 97 | { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, 98 | { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, 99 | { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, 100 | { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, 101 | { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, 102 | { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, 103 | { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, 104 | { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, 105 | { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, 106 | { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, 107 | { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, 108 | ] 109 | 110 | [[package]] 111 | name = "colorama" 112 | version = "0.4.6" 113 | source = { registry = "https://pypi.org/simple" } 114 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 115 | wheels = [ 116 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 117 | ] 118 | 119 | [[package]] 120 | name = "cryptography" 121 | version = "45.0.6" 122 | source = { registry = "https://pypi.org/simple" } 123 | dependencies = [ 124 | { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, 125 | ] 126 | sdist = { url = "https://files.pythonhosted.org/packages/d6/0d/d13399c94234ee8f3df384819dc67e0c5ce215fb751d567a55a1f4b028c7/cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719", size = 744949, upload-time = "2025-08-05T23:59:27.93Z" } 127 | wheels = [ 128 | { url = "https://files.pythonhosted.org/packages/8c/29/2793d178d0eda1ca4a09a7c4e09a5185e75738cc6d526433e8663b460ea6/cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74", size = 7042702, upload-time = "2025-08-05T23:58:23.464Z" }, 129 | { url = "https://files.pythonhosted.org/packages/b3/b6/cabd07410f222f32c8d55486c464f432808abaa1f12af9afcbe8f2f19030/cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f", size = 4206483, upload-time = "2025-08-05T23:58:27.132Z" }, 130 | { url = "https://files.pythonhosted.org/packages/8b/9e/f9c7d36a38b1cfeb1cc74849aabe9bf817990f7603ff6eb485e0d70e0b27/cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf", size = 4429679, upload-time = "2025-08-05T23:58:29.152Z" }, 131 | { url = "https://files.pythonhosted.org/packages/9c/2a/4434c17eb32ef30b254b9e8b9830cee4e516f08b47fdd291c5b1255b8101/cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5", size = 4210553, upload-time = "2025-08-05T23:58:30.596Z" }, 132 | { url = "https://files.pythonhosted.org/packages/ef/1d/09a5df8e0c4b7970f5d1f3aff1b640df6d4be28a64cae970d56c6cf1c772/cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2", size = 3894499, upload-time = "2025-08-05T23:58:32.03Z" }, 133 | { url = "https://files.pythonhosted.org/packages/79/62/120842ab20d9150a9d3a6bdc07fe2870384e82f5266d41c53b08a3a96b34/cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08", size = 4458484, upload-time = "2025-08-05T23:58:33.526Z" }, 134 | { url = "https://files.pythonhosted.org/packages/fd/80/1bc3634d45ddfed0871bfba52cf8f1ad724761662a0c792b97a951fb1b30/cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402", size = 4210281, upload-time = "2025-08-05T23:58:35.445Z" }, 135 | { url = "https://files.pythonhosted.org/packages/7d/fe/ffb12c2d83d0ee625f124880a1f023b5878f79da92e64c37962bbbe35f3f/cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42", size = 4456890, upload-time = "2025-08-05T23:58:36.923Z" }, 136 | { url = "https://files.pythonhosted.org/packages/8c/8e/b3f3fe0dc82c77a0deb5f493b23311e09193f2268b77196ec0f7a36e3f3e/cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05", size = 4333247, upload-time = "2025-08-05T23:58:38.781Z" }, 137 | { url = "https://files.pythonhosted.org/packages/b3/a6/c3ef2ab9e334da27a1d7b56af4a2417d77e7806b2e0f90d6267ce120d2e4/cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453", size = 4565045, upload-time = "2025-08-05T23:58:40.415Z" }, 138 | { url = "https://files.pythonhosted.org/packages/31/c3/77722446b13fa71dddd820a5faab4ce6db49e7e0bf8312ef4192a3f78e2f/cryptography-45.0.6-cp311-abi3-win32.whl", hash = "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159", size = 2928923, upload-time = "2025-08-05T23:58:41.919Z" }, 139 | { url = "https://files.pythonhosted.org/packages/38/63/a025c3225188a811b82932a4dcc8457a26c3729d81578ccecbcce2cb784e/cryptography-45.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec", size = 3403805, upload-time = "2025-08-05T23:58:43.792Z" }, 140 | { url = "https://files.pythonhosted.org/packages/5b/af/bcfbea93a30809f126d51c074ee0fac5bd9d57d068edf56c2a73abedbea4/cryptography-45.0.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0", size = 7020111, upload-time = "2025-08-05T23:58:45.316Z" }, 141 | { url = "https://files.pythonhosted.org/packages/98/c6/ea5173689e014f1a8470899cd5beeb358e22bb3cf5a876060f9d1ca78af4/cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394", size = 4198169, upload-time = "2025-08-05T23:58:47.121Z" }, 142 | { url = "https://files.pythonhosted.org/packages/ba/73/b12995edc0c7e2311ffb57ebd3b351f6b268fed37d93bfc6f9856e01c473/cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9", size = 4421273, upload-time = "2025-08-05T23:58:48.557Z" }, 143 | { url = "https://files.pythonhosted.org/packages/f7/6e/286894f6f71926bc0da67408c853dd9ba953f662dcb70993a59fd499f111/cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3", size = 4199211, upload-time = "2025-08-05T23:58:50.139Z" }, 144 | { url = "https://files.pythonhosted.org/packages/de/34/a7f55e39b9623c5cb571d77a6a90387fe557908ffc44f6872f26ca8ae270/cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3", size = 3883732, upload-time = "2025-08-05T23:58:52.253Z" }, 145 | { url = "https://files.pythonhosted.org/packages/f9/b9/c6d32edbcba0cd9f5df90f29ed46a65c4631c4fbe11187feb9169c6ff506/cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301", size = 4450655, upload-time = "2025-08-05T23:58:53.848Z" }, 146 | { url = "https://files.pythonhosted.org/packages/77/2d/09b097adfdee0227cfd4c699b3375a842080f065bab9014248933497c3f9/cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5", size = 4198956, upload-time = "2025-08-05T23:58:55.209Z" }, 147 | { url = "https://files.pythonhosted.org/packages/55/66/061ec6689207d54effdff535bbdf85cc380d32dd5377173085812565cf38/cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016", size = 4449859, upload-time = "2025-08-05T23:58:56.639Z" }, 148 | { url = "https://files.pythonhosted.org/packages/41/ff/e7d5a2ad2d035e5a2af116e1a3adb4d8fcd0be92a18032917a089c6e5028/cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3", size = 4320254, upload-time = "2025-08-05T23:58:58.833Z" }, 149 | { url = "https://files.pythonhosted.org/packages/82/27/092d311af22095d288f4db89fcaebadfb2f28944f3d790a4cf51fe5ddaeb/cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9", size = 4554815, upload-time = "2025-08-05T23:59:00.283Z" }, 150 | { url = "https://files.pythonhosted.org/packages/7e/01/aa2f4940262d588a8fdf4edabe4cda45854d00ebc6eaac12568b3a491a16/cryptography-45.0.6-cp37-abi3-win32.whl", hash = "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02", size = 2912147, upload-time = "2025-08-05T23:59:01.716Z" }, 151 | { url = "https://files.pythonhosted.org/packages/0a/bc/16e0276078c2de3ceef6b5a34b965f4436215efac45313df90d55f0ba2d2/cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", size = 3390459, upload-time = "2025-08-05T23:59:03.358Z" }, 152 | ] 153 | 154 | [[package]] 155 | name = "cs-blocklist-mirror-tests" 156 | version = "0.1.0" 157 | source = { virtual = "." } 158 | dependencies = [ 159 | { name = "pexpect" }, 160 | { name = "pytest" }, 161 | { name = "pytest-cs" }, 162 | { name = "pytest-dependency" }, 163 | { name = "pytest-dotenv" }, 164 | { name = "zxcvbn" }, 165 | ] 166 | 167 | [package.dev-dependencies] 168 | dev = [ 169 | { name = "basedpyright" }, 170 | { name = "ipdb" }, 171 | { name = "ruff" }, 172 | ] 173 | 174 | [package.metadata] 175 | requires-dist = [ 176 | { name = "pexpect", specifier = ">=4.9.0" }, 177 | { name = "pytest", specifier = ">=8.3.5" }, 178 | { name = "pytest-cs", git = "https://github.com/crowdsecurity/pytest-cs" }, 179 | { name = "pytest-dependency", specifier = ">=0.6.0" }, 180 | { name = "pytest-dotenv", specifier = ">=0.5.2" }, 181 | { name = "zxcvbn", specifier = ">=4.5.0" }, 182 | ] 183 | 184 | [package.metadata.requires-dev] 185 | dev = [ 186 | { name = "basedpyright", specifier = ">=1.28.4" }, 187 | { name = "ipdb", specifier = ">=0.13.13" }, 188 | { name = "ruff", specifier = ">=0.11.2" }, 189 | ] 190 | 191 | [[package]] 192 | name = "decorator" 193 | version = "5.2.1" 194 | source = { registry = "https://pypi.org/simple" } 195 | sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } 196 | wheels = [ 197 | { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, 198 | ] 199 | 200 | [[package]] 201 | name = "docker" 202 | version = "7.1.0" 203 | source = { registry = "https://pypi.org/simple" } 204 | dependencies = [ 205 | { name = "pywin32", marker = "sys_platform == 'win32'" }, 206 | { name = "requests" }, 207 | { name = "urllib3" }, 208 | ] 209 | sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } 210 | wheels = [ 211 | { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, 212 | ] 213 | 214 | [[package]] 215 | name = "executing" 216 | version = "2.2.0" 217 | source = { registry = "https://pypi.org/simple" } 218 | sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693, upload-time = "2025-01-22T15:41:29.403Z" } 219 | wheels = [ 220 | { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload-time = "2025-01-22T15:41:25.929Z" }, 221 | ] 222 | 223 | [[package]] 224 | name = "idna" 225 | version = "3.10" 226 | source = { registry = "https://pypi.org/simple" } 227 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } 228 | wheels = [ 229 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, 230 | ] 231 | 232 | [[package]] 233 | name = "iniconfig" 234 | version = "2.1.0" 235 | source = { registry = "https://pypi.org/simple" } 236 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } 237 | wheels = [ 238 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, 239 | ] 240 | 241 | [[package]] 242 | name = "ipdb" 243 | version = "0.13.13" 244 | source = { registry = "https://pypi.org/simple" } 245 | dependencies = [ 246 | { name = "decorator" }, 247 | { name = "ipython" }, 248 | ] 249 | sdist = { url = "https://files.pythonhosted.org/packages/3d/1b/7e07e7b752017f7693a0f4d41c13e5ca29ce8cbcfdcc1fd6c4ad8c0a27a0/ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726", size = 17042, upload-time = "2023-03-09T15:40:57.487Z" } 250 | wheels = [ 251 | { url = "https://files.pythonhosted.org/packages/0c/4c/b075da0092003d9a55cf2ecc1cae9384a1ca4f650d51b00fc59875fe76f6/ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4", size = 12130, upload-time = "2023-03-09T15:40:55.021Z" }, 252 | ] 253 | 254 | [[package]] 255 | name = "ipython" 256 | version = "9.4.0" 257 | source = { registry = "https://pypi.org/simple" } 258 | dependencies = [ 259 | { name = "colorama", marker = "sys_platform == 'win32'" }, 260 | { name = "decorator" }, 261 | { name = "ipython-pygments-lexers" }, 262 | { name = "jedi" }, 263 | { name = "matplotlib-inline" }, 264 | { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, 265 | { name = "prompt-toolkit" }, 266 | { name = "pygments" }, 267 | { name = "stack-data" }, 268 | { name = "traitlets" }, 269 | ] 270 | sdist = { url = "https://files.pythonhosted.org/packages/54/80/406f9e3bde1c1fd9bf5a0be9d090f8ae623e401b7670d8f6fdf2ab679891/ipython-9.4.0.tar.gz", hash = "sha256:c033c6d4e7914c3d9768aabe76bbe87ba1dc66a92a05db6bfa1125d81f2ee270", size = 4385338, upload-time = "2025-07-01T11:11:30.606Z" } 271 | wheels = [ 272 | { url = "https://files.pythonhosted.org/packages/63/f8/0031ee2b906a15a33d6bfc12dd09c3dfa966b3cb5b284ecfb7549e6ac3c4/ipython-9.4.0-py3-none-any.whl", hash = "sha256:25850f025a446d9b359e8d296ba175a36aedd32e83ca9b5060430fe16801f066", size = 611021, upload-time = "2025-07-01T11:11:27.85Z" }, 273 | ] 274 | 275 | [[package]] 276 | name = "ipython-pygments-lexers" 277 | version = "1.1.1" 278 | source = { registry = "https://pypi.org/simple" } 279 | dependencies = [ 280 | { name = "pygments" }, 281 | ] 282 | sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } 283 | wheels = [ 284 | { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, 285 | ] 286 | 287 | [[package]] 288 | name = "jedi" 289 | version = "0.19.2" 290 | source = { registry = "https://pypi.org/simple" } 291 | dependencies = [ 292 | { name = "parso" }, 293 | ] 294 | sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } 295 | wheels = [ 296 | { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, 297 | ] 298 | 299 | [[package]] 300 | name = "matplotlib-inline" 301 | version = "0.1.7" 302 | source = { registry = "https://pypi.org/simple" } 303 | dependencies = [ 304 | { name = "traitlets" }, 305 | ] 306 | sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" } 307 | wheels = [ 308 | { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, 309 | ] 310 | 311 | [[package]] 312 | name = "nodejs-wheel-binaries" 313 | version = "22.18.0" 314 | source = { registry = "https://pypi.org/simple" } 315 | wheels = [ 316 | { url = "https://files.pythonhosted.org/packages/7e/6d/773e09de4a052cc75c129c3766a3cf77c36bff8504a38693b735f4a1eb55/nodejs_wheel_binaries-22.18.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b04495857755c5d5658f7ac969d84f25898fe0b0c1bdc41172e5e0ac6105ca", size = 50873051, upload-time = "2025-08-01T11:10:29.475Z" }, 317 | { url = "https://files.pythonhosted.org/packages/ae/fc/3d6fd4ad5d26c9acd46052190d6a8895dc5050297b03d9cce03def53df0d/nodejs_wheel_binaries-22.18.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:bd4d016257d4dfe604ed526c19bd4695fdc4f4cc32e8afc4738111447aa96d03", size = 51814481, upload-time = "2025-08-01T11:10:33.086Z" }, 318 | { url = "https://files.pythonhosted.org/packages/10/f9/7be44809a861605f844077f9e731a117b669d5ca6846a7820e7dd82c9fad/nodejs_wheel_binaries-22.18.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b125f94f3f5e8ab9560d3bd637497f02e45470aeea74cf6fe60afe751cfa5f", size = 57804907, upload-time = "2025-08-01T11:10:36.83Z" }, 319 | { url = "https://files.pythonhosted.org/packages/e9/67/563e74a0dff653ec7ddee63dc49b3f37a20df39f23675cfc801d7e8e4bb7/nodejs_wheel_binaries-22.18.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bbb81b6e67c15f04e2a9c6c220d7615fb46ae8f1ad388df0d66abac6bed5f8", size = 58335587, upload-time = "2025-08-01T11:10:40.716Z" }, 320 | { url = "https://files.pythonhosted.org/packages/b6/b1/ec45fefef60223dd40e7953e2ff087964e200d6ec2d04eae0171d6428679/nodejs_wheel_binaries-22.18.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5d3ea8b7f957ae16b73241451f6ce831d6478156f363cce75c7ea71cbe6c6f7", size = 59662356, upload-time = "2025-08-01T11:10:44.795Z" }, 321 | { url = "https://files.pythonhosted.org/packages/a2/ed/6de2c73499eebf49d0d20e0704f64566029a3441c48cd4f655d49befd28b/nodejs_wheel_binaries-22.18.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bcda35b07677039670102a6f9b78c2313fd526111d407cb7ffc2a4c243a48ef9", size = 60706806, upload-time = "2025-08-01T11:10:48.985Z" }, 322 | { url = "https://files.pythonhosted.org/packages/2b/f5/487434b1792c4f28c63876e4a896f2b6e953e2dc1f0b3940e912bd087755/nodejs_wheel_binaries-22.18.0-py2.py3-none-win_amd64.whl", hash = "sha256:0f55e72733f1df2f542dce07f35145ac2e125408b5e2051cac08e5320e41b4d1", size = 39998139, upload-time = "2025-08-01T11:10:52.676Z" }, 323 | ] 324 | 325 | [[package]] 326 | name = "packaging" 327 | version = "25.0" 328 | source = { registry = "https://pypi.org/simple" } 329 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } 330 | wheels = [ 331 | { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, 332 | ] 333 | 334 | [[package]] 335 | name = "parso" 336 | version = "0.8.4" 337 | source = { registry = "https://pypi.org/simple" } 338 | sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609, upload-time = "2024-04-05T09:43:55.897Z" } 339 | wheels = [ 340 | { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload-time = "2024-04-05T09:43:53.299Z" }, 341 | ] 342 | 343 | [[package]] 344 | name = "pexpect" 345 | version = "4.9.0" 346 | source = { registry = "https://pypi.org/simple" } 347 | dependencies = [ 348 | { name = "ptyprocess" }, 349 | ] 350 | sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } 351 | wheels = [ 352 | { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, 353 | ] 354 | 355 | [[package]] 356 | name = "pluggy" 357 | version = "1.6.0" 358 | source = { registry = "https://pypi.org/simple" } 359 | sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } 360 | wheels = [ 361 | { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, 362 | ] 363 | 364 | [[package]] 365 | name = "prompt-toolkit" 366 | version = "3.0.51" 367 | source = { registry = "https://pypi.org/simple" } 368 | dependencies = [ 369 | { name = "wcwidth" }, 370 | ] 371 | sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } 372 | wheels = [ 373 | { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, 374 | ] 375 | 376 | [[package]] 377 | name = "psutil" 378 | version = "7.0.0" 379 | source = { registry = "https://pypi.org/simple" } 380 | sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } 381 | wheels = [ 382 | { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, 383 | { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, 384 | { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, 385 | { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, 386 | { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, 387 | { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, 388 | { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, 389 | ] 390 | 391 | [[package]] 392 | name = "ptyprocess" 393 | version = "0.7.0" 394 | source = { registry = "https://pypi.org/simple" } 395 | sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } 396 | wheels = [ 397 | { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, 398 | ] 399 | 400 | [[package]] 401 | name = "pure-eval" 402 | version = "0.2.3" 403 | source = { registry = "https://pypi.org/simple" } 404 | sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } 405 | wheels = [ 406 | { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, 407 | ] 408 | 409 | [[package]] 410 | name = "pycparser" 411 | version = "2.22" 412 | source = { registry = "https://pypi.org/simple" } 413 | sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } 414 | wheels = [ 415 | { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, 416 | ] 417 | 418 | [[package]] 419 | name = "pygments" 420 | version = "2.19.2" 421 | source = { registry = "https://pypi.org/simple" } 422 | sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } 423 | wheels = [ 424 | { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, 425 | ] 426 | 427 | [[package]] 428 | name = "pytest" 429 | version = "8.4.1" 430 | source = { registry = "https://pypi.org/simple" } 431 | dependencies = [ 432 | { name = "colorama", marker = "sys_platform == 'win32'" }, 433 | { name = "iniconfig" }, 434 | { name = "packaging" }, 435 | { name = "pluggy" }, 436 | { name = "pygments" }, 437 | ] 438 | sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } 439 | wheels = [ 440 | { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, 441 | ] 442 | 443 | [[package]] 444 | name = "pytest-cs" 445 | version = "0.7.24" 446 | source = { git = "https://github.com/crowdsecurity/pytest-cs#0c085d717f4f9df8dc4b0387f11c0c1f7fdbc6b4" } 447 | dependencies = [ 448 | { name = "docker" }, 449 | { name = "psutil" }, 450 | { name = "pytest" }, 451 | { name = "pytest-datadir" }, 452 | { name = "pytest-dotenv" }, 453 | { name = "pyyaml" }, 454 | { name = "requests" }, 455 | { name = "trustme" }, 456 | ] 457 | 458 | [[package]] 459 | name = "pytest-datadir" 460 | version = "1.8.0" 461 | source = { registry = "https://pypi.org/simple" } 462 | dependencies = [ 463 | { name = "pytest" }, 464 | ] 465 | sdist = { url = "https://files.pythonhosted.org/packages/b4/46/db060b291999ca048edd06d6fa9ee95945d088edc38b1172c59eeb46ec45/pytest_datadir-1.8.0.tar.gz", hash = "sha256:7a15faed76cebe87cc91941dd1920a9a38eba56a09c11e9ddf1434d28a0f78eb", size = 11848, upload-time = "2025-07-30T13:52:12.518Z" } 466 | wheels = [ 467 | { url = "https://files.pythonhosted.org/packages/8f/7a/33895863aec26ac3bb5068a73583f935680d6ab6af2a9567d409430c3ee1/pytest_datadir-1.8.0-py3-none-any.whl", hash = "sha256:5c677bc097d907ac71ca418109adc3abe34cf0bddfe6cf78aecfbabd96a15cf0", size = 6512, upload-time = "2025-07-30T13:52:11.525Z" }, 468 | ] 469 | 470 | [[package]] 471 | name = "pytest-dependency" 472 | version = "0.6.0" 473 | source = { registry = "https://pypi.org/simple" } 474 | dependencies = [ 475 | { name = "pytest" }, 476 | { name = "setuptools" }, 477 | ] 478 | sdist = { url = "https://files.pythonhosted.org/packages/7e/3b/317cc04e77d707d338540ca67b619df8f247f3f4c9f40e67bf5ea503ad94/pytest-dependency-0.6.0.tar.gz", hash = "sha256:934b0e6a39d95995062c193f7eaeed8a8ffa06ff1bcef4b62b0dc74a708bacc1", size = 19499, upload-time = "2023-12-31T20:38:54.991Z" } 479 | 480 | [[package]] 481 | name = "pytest-dotenv" 482 | version = "0.5.2" 483 | source = { registry = "https://pypi.org/simple" } 484 | dependencies = [ 485 | { name = "pytest" }, 486 | { name = "python-dotenv" }, 487 | ] 488 | sdist = { url = "https://files.pythonhosted.org/packages/cd/b0/cafee9c627c1bae228eb07c9977f679b3a7cb111b488307ab9594ba9e4da/pytest-dotenv-0.5.2.tar.gz", hash = "sha256:2dc6c3ac6d8764c71c6d2804e902d0ff810fa19692e95fe138aefc9b1aa73732", size = 3782, upload-time = "2020-06-16T12:38:03.4Z" } 489 | wheels = [ 490 | { url = "https://files.pythonhosted.org/packages/d0/da/9da67c67b3d0963160e3d2cbc7c38b6fae342670cc8e6d5936644b2cf944/pytest_dotenv-0.5.2-py3-none-any.whl", hash = "sha256:40a2cece120a213898afaa5407673f6bd924b1fa7eafce6bda0e8abffe2f710f", size = 3993, upload-time = "2020-06-16T12:38:01.139Z" }, 491 | ] 492 | 493 | [[package]] 494 | name = "python-dotenv" 495 | version = "1.1.1" 496 | source = { registry = "https://pypi.org/simple" } 497 | sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } 498 | wheels = [ 499 | { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, 500 | ] 501 | 502 | [[package]] 503 | name = "pywin32" 504 | version = "311" 505 | source = { registry = "https://pypi.org/simple" } 506 | wheels = [ 507 | { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, 508 | { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, 509 | { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, 510 | { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, 511 | { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, 512 | { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, 513 | { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, 514 | { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, 515 | { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, 516 | ] 517 | 518 | [[package]] 519 | name = "pyyaml" 520 | version = "6.0.2" 521 | source = { registry = "https://pypi.org/simple" } 522 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } 523 | wheels = [ 524 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, 525 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, 526 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, 527 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, 528 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, 529 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, 530 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, 531 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, 532 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, 533 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, 534 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, 535 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, 536 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, 537 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, 538 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, 539 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, 540 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, 541 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, 542 | ] 543 | 544 | [[package]] 545 | name = "requests" 546 | version = "2.32.4" 547 | source = { registry = "https://pypi.org/simple" } 548 | dependencies = [ 549 | { name = "certifi" }, 550 | { name = "charset-normalizer" }, 551 | { name = "idna" }, 552 | { name = "urllib3" }, 553 | ] 554 | sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } 555 | wheels = [ 556 | { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, 557 | ] 558 | 559 | [[package]] 560 | name = "ruff" 561 | version = "0.12.9" 562 | source = { registry = "https://pypi.org/simple" } 563 | sdist = { url = "https://files.pythonhosted.org/packages/4a/45/2e403fa7007816b5fbb324cb4f8ed3c7402a927a0a0cb2b6279879a8bfdc/ruff-0.12.9.tar.gz", hash = "sha256:fbd94b2e3c623f659962934e52c2bea6fc6da11f667a427a368adaf3af2c866a", size = 5254702, upload-time = "2025-08-14T16:08:55.2Z" } 564 | wheels = [ 565 | { url = "https://files.pythonhosted.org/packages/ad/20/53bf098537adb7b6a97d98fcdebf6e916fcd11b2e21d15f8c171507909cc/ruff-0.12.9-py3-none-linux_armv6l.whl", hash = "sha256:fcebc6c79fcae3f220d05585229463621f5dbf24d79fdc4936d9302e177cfa3e", size = 11759705, upload-time = "2025-08-14T16:08:12.968Z" }, 566 | { url = "https://files.pythonhosted.org/packages/20/4d/c764ee423002aac1ec66b9d541285dd29d2c0640a8086c87de59ebbe80d5/ruff-0.12.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aed9d15f8c5755c0e74467731a007fcad41f19bcce41cd75f768bbd687f8535f", size = 12527042, upload-time = "2025-08-14T16:08:16.54Z" }, 567 | { url = "https://files.pythonhosted.org/packages/8b/45/cfcdf6d3eb5fc78a5b419e7e616d6ccba0013dc5b180522920af2897e1be/ruff-0.12.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b15ea354c6ff0d7423814ba6d44be2807644d0c05e9ed60caca87e963e93f70", size = 11724457, upload-time = "2025-08-14T16:08:18.686Z" }, 568 | { url = "https://files.pythonhosted.org/packages/72/e6/44615c754b55662200c48bebb02196dbb14111b6e266ab071b7e7297b4ec/ruff-0.12.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d596c2d0393c2502eaabfef723bd74ca35348a8dac4267d18a94910087807c53", size = 11949446, upload-time = "2025-08-14T16:08:21.059Z" }, 569 | { url = "https://files.pythonhosted.org/packages/fd/d1/9b7d46625d617c7df520d40d5ac6cdcdf20cbccb88fad4b5ecd476a6bb8d/ruff-0.12.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b15599931a1a7a03c388b9c5df1bfa62be7ede6eb7ef753b272381f39c3d0ff", size = 11566350, upload-time = "2025-08-14T16:08:23.433Z" }, 570 | { url = "https://files.pythonhosted.org/packages/59/20/b73132f66f2856bc29d2d263c6ca457f8476b0bbbe064dac3ac3337a270f/ruff-0.12.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d02faa2977fb6f3f32ddb7828e212b7dd499c59eb896ae6c03ea5c303575756", size = 13270430, upload-time = "2025-08-14T16:08:25.837Z" }, 571 | { url = "https://files.pythonhosted.org/packages/a2/21/eaf3806f0a3d4c6be0a69d435646fba775b65f3f2097d54898b0fd4bb12e/ruff-0.12.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:17d5b6b0b3a25259b69ebcba87908496e6830e03acfb929ef9fd4c58675fa2ea", size = 14264717, upload-time = "2025-08-14T16:08:27.907Z" }, 572 | { url = "https://files.pythonhosted.org/packages/d2/82/1d0c53bd37dcb582b2c521d352fbf4876b1e28bc0d8894344198f6c9950d/ruff-0.12.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72db7521860e246adbb43f6ef464dd2a532ef2ef1f5dd0d470455b8d9f1773e0", size = 13684331, upload-time = "2025-08-14T16:08:30.352Z" }, 573 | { url = "https://files.pythonhosted.org/packages/3b/2f/1c5cf6d8f656306d42a686f1e207f71d7cebdcbe7b2aa18e4e8a0cb74da3/ruff-0.12.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a03242c1522b4e0885af63320ad754d53983c9599157ee33e77d748363c561ce", size = 12739151, upload-time = "2025-08-14T16:08:32.55Z" }, 574 | { url = "https://files.pythonhosted.org/packages/47/09/25033198bff89b24d734e6479e39b1968e4c992e82262d61cdccaf11afb9/ruff-0.12.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fc83e4e9751e6c13b5046d7162f205d0a7bac5840183c5beebf824b08a27340", size = 12954992, upload-time = "2025-08-14T16:08:34.816Z" }, 575 | { url = "https://files.pythonhosted.org/packages/52/8e/d0dbf2f9dca66c2d7131feefc386523404014968cd6d22f057763935ab32/ruff-0.12.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:881465ed56ba4dd26a691954650de6ad389a2d1fdb130fe51ff18a25639fe4bb", size = 12899569, upload-time = "2025-08-14T16:08:36.852Z" }, 576 | { url = "https://files.pythonhosted.org/packages/a0/bd/b614d7c08515b1428ed4d3f1d4e3d687deffb2479703b90237682586fa66/ruff-0.12.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:43f07a3ccfc62cdb4d3a3348bf0588358a66da756aa113e071b8ca8c3b9826af", size = 11751983, upload-time = "2025-08-14T16:08:39.314Z" }, 577 | { url = "https://files.pythonhosted.org/packages/58/d6/383e9f818a2441b1a0ed898d7875f11273f10882f997388b2b51cb2ae8b5/ruff-0.12.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:07adb221c54b6bba24387911e5734357f042e5669fa5718920ee728aba3cbadc", size = 11538635, upload-time = "2025-08-14T16:08:41.297Z" }, 578 | { url = "https://files.pythonhosted.org/packages/20/9c/56f869d314edaa9fc1f491706d1d8a47747b9d714130368fbd69ce9024e9/ruff-0.12.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f5cd34fabfdea3933ab85d72359f118035882a01bff15bd1d2b15261d85d5f66", size = 12534346, upload-time = "2025-08-14T16:08:43.39Z" }, 579 | { url = "https://files.pythonhosted.org/packages/bd/4b/d8b95c6795a6c93b439bc913ee7a94fda42bb30a79285d47b80074003ee7/ruff-0.12.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6be1d2ca0686c54564da8e7ee9e25f93bdd6868263805f8c0b8fc6a449db6d7", size = 13017021, upload-time = "2025-08-14T16:08:45.889Z" }, 580 | { url = "https://files.pythonhosted.org/packages/c7/c1/5f9a839a697ce1acd7af44836f7c2181cdae5accd17a5cb85fcbd694075e/ruff-0.12.9-py3-none-win32.whl", hash = "sha256:cc7a37bd2509974379d0115cc5608a1a4a6c4bff1b452ea69db83c8855d53f93", size = 11734785, upload-time = "2025-08-14T16:08:48.062Z" }, 581 | { url = "https://files.pythonhosted.org/packages/fa/66/cdddc2d1d9a9f677520b7cfc490d234336f523d4b429c1298de359a3be08/ruff-0.12.9-py3-none-win_amd64.whl", hash = "sha256:6fb15b1977309741d7d098c8a3cb7a30bc112760a00fb6efb7abc85f00ba5908", size = 12840654, upload-time = "2025-08-14T16:08:50.158Z" }, 582 | { url = "https://files.pythonhosted.org/packages/ac/fd/669816bc6b5b93b9586f3c1d87cd6bc05028470b3ecfebb5938252c47a35/ruff-0.12.9-py3-none-win_arm64.whl", hash = "sha256:63c8c819739d86b96d500cce885956a1a48ab056bbcbc61b747ad494b2485089", size = 11949623, upload-time = "2025-08-14T16:08:52.233Z" }, 583 | ] 584 | 585 | [[package]] 586 | name = "setuptools" 587 | version = "80.9.0" 588 | source = { registry = "https://pypi.org/simple" } 589 | sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } 590 | wheels = [ 591 | { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, 592 | ] 593 | 594 | [[package]] 595 | name = "stack-data" 596 | version = "0.6.3" 597 | source = { registry = "https://pypi.org/simple" } 598 | dependencies = [ 599 | { name = "asttokens" }, 600 | { name = "executing" }, 601 | { name = "pure-eval" }, 602 | ] 603 | sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } 604 | wheels = [ 605 | { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, 606 | ] 607 | 608 | [[package]] 609 | name = "traitlets" 610 | version = "5.14.3" 611 | source = { registry = "https://pypi.org/simple" } 612 | sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } 613 | wheels = [ 614 | { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, 615 | ] 616 | 617 | [[package]] 618 | name = "trustme" 619 | version = "1.2.1" 620 | source = { registry = "https://pypi.org/simple" } 621 | dependencies = [ 622 | { name = "cryptography" }, 623 | { name = "idna" }, 624 | ] 625 | sdist = { url = "https://files.pythonhosted.org/packages/4b/c5/931476f4cf1cd9e736f32651005078061a50dc164a2569fb874e00eb2786/trustme-1.2.1.tar.gz", hash = "sha256:6528ba2bbc7f2db41f33825c8dd13e3e3eb9d334ba0f909713c8c3139f4ae47f", size = 26844, upload-time = "2025-01-02T01:55:32.632Z" } 626 | wheels = [ 627 | { url = "https://files.pythonhosted.org/packages/b5/f3/c34dbabf6da5eda56fe923226769d40e11806952cd7f46655dd06e10f018/trustme-1.2.1-py3-none-any.whl", hash = "sha256:d768e5fc57c86dfc5ec9365102e9b092541cd6954b35d8c1eea01a84f35a762a", size = 16530, upload-time = "2025-01-02T01:55:30.181Z" }, 628 | ] 629 | 630 | [[package]] 631 | name = "urllib3" 632 | version = "2.5.0" 633 | source = { registry = "https://pypi.org/simple" } 634 | sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } 635 | wheels = [ 636 | { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, 637 | ] 638 | 639 | [[package]] 640 | name = "wcwidth" 641 | version = "0.2.13" 642 | source = { registry = "https://pypi.org/simple" } 643 | sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } 644 | wheels = [ 645 | { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, 646 | ] 647 | 648 | [[package]] 649 | name = "zxcvbn" 650 | version = "4.5.0" 651 | source = { registry = "https://pypi.org/simple" } 652 | sdist = { url = "https://files.pythonhosted.org/packages/ae/40/9366940b1484fd4e9423c8decbbf34a73bf52badb36281e082fe02b57aca/zxcvbn-4.5.0.tar.gz", hash = "sha256:70392c0fff39459d7f55d0211151401e79e76fcc6e2c22b61add62900359c7c1", size = 411249, upload-time = "2025-02-19T19:03:02.699Z" } 653 | wheels = [ 654 | { url = "https://files.pythonhosted.org/packages/c2/16/7410f8e714a109d43d17f4e27c8eabb351557653a9b570db1bd7dfdfd822/zxcvbn-4.5.0-py2.py3-none-any.whl", hash = "sha256:2b6eed621612ce6d65e6e4c7455b966acee87d0280e257956b1f06ccc66bd5ff", size = 409397, upload-time = "2025-02-19T19:03:00.521Z" }, 655 | ] 656 | --------------------------------------------------------------------------------